1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        手寫一個webpack,看看AST怎么用

        共 22990字,需瀏覽 46分鐘

         ·

        2021-03-21 21:36

        本文開始我會圍繞webpackbabel寫一系列的工程化文章,這兩個工具我雖然天天用,但是對他們的原理理解的其實不是很深入,寫這些文章的過程其實也是我深入學習的過程。由于webpackbabel的體系太大,知識點眾多,不可能一篇文章囊括所有知識點,目前我的計劃是從簡單入手,先實現(xiàn)一個最簡單的可以運行的webpack,然后再看看plugin, loadertree shaking等功能。目前我計劃會有這些文章:

        1. 手寫最簡webpack,也就是本文
        2. webpackplugin實現(xiàn)原理
        3. webpackloader實現(xiàn)原理
        4. webpacktree shaking實現(xiàn)原理
        5. webpackHMR實現(xiàn)原理
        6. babelast原理

        所有文章都是原理或者源碼解析,歡迎關(guān)注~

        本文可運行代碼已經(jīng)上傳GitHub,大家可以拿下來玩玩:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Engineering/mini-webpack[1]

        注意:本文主要講webpack原理,在實現(xiàn)時并不嚴謹,而且只處理了importexportdefault情況,如果你想在生產(chǎn)環(huán)境使用,請自己添加其他情況的處理和邊界判斷

        為什么要用webpack

        筆者剛開始做前端時,其實不知道什么webpack,也不懂模塊化,都是html里面直接寫script,引入jquery直接干。所以如果一個頁面的JS需要依賴jquerylodash,那html可能就長這樣:

        <!DOCTYPE html>
        <html>
          <head>
            <meta charset="utf-8" />
            <script src="https://unpkg.com/[email protected]"></script>
            <script src="https://unpkg.com/[email protected]"></script>
            <script src="./src/index.js"></script>
          </head>
          <body>
          </body>
        </html>

        這樣寫會導致幾個問題:

        1. 單獨看index.js不能清晰的找到他到底依賴哪些外部庫
        2. script的順序必須寫正確,如果錯了就會導致找不到依賴,直接報錯
        3. 模塊間通信困難,基本都靠往window上注入變量來暴露給外部
        4. 瀏覽器嚴格按照script標簽來下載代碼,有些沒用到的代碼也會下載下來
        5. 當前端規(guī)模變大,JS腳本會顯得很雜亂,項目管理混亂

        webpack的一個最基本的功能就是來解決上述的情況,允許在JS里面通過import或者require等關(guān)鍵字來顯式申明依賴,可以引用第三方庫,自己的JS代碼間也可以相互引用,這樣在實質(zhì)上就實現(xiàn)了前端代碼的模塊化。由于歷史問題,老版的JS并沒有自己模塊管理方案,所以社區(qū)提出了很多模塊管理方案,比如ES2015import,CommonJSrequire,另外還有AMDCMD等等。就目前我見到的情況來說,import因為已經(jīng)成為ES2015標準,所以在客戶端廣泛使用,而requireNode.js的自帶模塊管理機制,也有很廣泛的用途,而AMDCMD的使用已經(jīng)很少見了。

        但是webpack作為一個開放的模塊化工具,他是支持ES6,CommonJSAMD等多種標準的,不同的模塊化標準有不同的解析方法,本文只會講ES6標準的import方案,這也是客戶端JS使用最多的方案。

        簡單例子

        按照業(yè)界慣例,我也用hello world作為一個簡單的例子,但是我將這句話拆成了幾部分,放到了不同的文件里面。

        先來建一個hello.js,只導出一個簡單的字符串:

        const hello = 'hello';

        export default hello;

        然后再來一個helloWorld.js,將helloworld拼成一句話,并導出拼接的這個方法:

        import hello from './hello';

        const world = 'world';

        const helloWorld = () => `${hello} ${world}`;

        export default helloWorld;

        最后再來個index.js,將拼好的hello world插入到頁面上去:

        import helloWorld from "./helloWorld";

        const helloWorldStr = helloWorld();

        function component({
          const element = document.createElement("div");

          element.innerHTML = helloWorldStr;

          return element;
        }

        document.body.appendChild(component());

        現(xiàn)在如果你直接在html里面引用index.js是不能運行成功的,因為大部分瀏覽器都不支持import這種模塊導入。而webpack就是來解決這個問題的,它會將我們模塊化的代碼轉(zhuǎn)換成瀏覽器認識的普通JS來執(zhí)行。

        引入webpack

        我們印象中webpack的配置很多,很麻煩,但那是因為我們需要開啟的功能很多,如果只是解析轉(zhuǎn)換import,配置起來非常簡單。

        1. 先把依賴裝上吧,這沒什么好說的:

          // package.json
          {
            "devDependencies": {
              "webpack""^5.4.0",
              "webpack-cli""^4.2.0"
            },
          }
        2. 為了使用方便,再加個build腳本吧:

          // package.json
          {
            "scripts": {
              "build""webpack"
            },
          }
        3. 最后再簡單寫下webpack的配置文件就好了:

          // webpack.config.js

          const path = require("path");

          module.exports = {
            mode"development",
            devtool'source-map',
            entry"./src/index.js",
            output: {
              filename"main.js",
              path: path.resolve(__dirname, "dist"),
            },
          };

          這個配置文件里面其實只要指定了入口文件entry和編譯后的輸出文件目錄output就可以正常工作了,這里這個配置的意思是讓webpack./src/index.js開始編譯,編譯后的文件輸出到dist/main.js這個文件里面。

          這個配置文件上還有兩個配置modedevtool只是我用來方便調(diào)試編譯后的代碼的,mode指定用哪種模式編譯,默認是production,會對代碼進行壓縮和混淆,不好讀,所以我設(shè)置為development;而devtool是用來控制生成哪種粒度的source map,簡單來說,想要更好調(diào)試,就要更好的,更清晰的source map,但是編譯速度變慢;反之,想要編譯速度快,就要選擇粒度更粗,更不好讀的source map,webpack提供了很多可供選擇的source map,具體的可以看他的文檔[2]

        4. 然后就可以在dist下面建個index.html來引用編譯后的代碼了:

          // index.html

          <!DOCTYPE html>
          <html>
            <head>
              <meta charset="utf-8" />
            </head>
            <body>
              <script src="main.js"></script>
            </body>
          </html>
        5. 運行下yarn build就會編譯我們的代碼,然后打開index.html就可以看到效果了。

          image-20210203154111168

        深入原理

        前面講的這個例子很簡單,一般也滿足不了我們實際工程中的需求,但是對于我們理解原理卻是一個很好的突破口,畢竟webpack這么龐大的一個體系,我們也不能一口吃個胖子,得一點一點來。

        webpack把代碼編譯成了啥?

        為了弄懂他的原理,我們可以直接從編譯后的代碼入手,先看看他長啥樣子,有的朋友可能一提到去看源碼,心理就沒底,其實我以前也是這樣的。但是完全沒有必要懼怕,他編譯后的代碼瀏覽器能夠執(zhí)行,那肯定就是普通的JS代碼,不會藏著這么黑科技。

        下面是編譯完的代碼截圖:

        image-20210203155553091

        雖然我們只有三個簡單的JS文件,但是加上webpack自己的邏輯,編譯后的文件還是有一百多行代碼,所以即使我把具體邏輯折疊起來了,這個截圖還是有點長,為了能夠看清楚他的結(jié)構(gòu),我將它分成了4個部分,標記在了截圖上,下面我們分別來看看這幾個部分吧。

        1. 第一部分其實就是一個對象__webpack_modules__,這個對象里面有三個屬性,屬性名字是我們?nèi)齻€模塊的文件路徑,屬性的值是一個函數(shù),我們隨便展開一個./src/helloWorld.js看下:

          image-20210203161613636

          我們發(fā)現(xiàn)這個代碼內(nèi)容跟我們自己寫的helloWorld.js非常像:

          image-20210203161902647

          他只是在我們的代碼前先調(diào)用了__webpack_require__.r__webpack_require__.d,這兩個輔助函數(shù)我們在后面會看到。

          然后對我們的代碼進行了一點修改,將我們的import關(guān)鍵字改成了__webpack_require__函數(shù),并用一個變量_hello__WEBPACK_IMPORTED_MODULE_0__來接收了import進來的內(nèi)容,后面引用的地方也改成了這個,其他跟這個無關(guān)的代碼,比如const world = 'world';還是保持原樣的。

          這個__webpack_modules__對象存了所有的模塊代碼,其實對于模塊代碼的保存,在不同版本的webpack里面實現(xiàn)的方式并不一樣,我這個版本是5.4.0,在4.x的版本里面好像是作為數(shù)組存下來,然后在最外層的立即執(zhí)行函數(shù)里面以參數(shù)的形式傳進來的。但是不管是哪種方式,都只是轉(zhuǎn)換然后保存一下模塊代碼而已。

        2. 第二塊代碼的核心是__webpack_require__,這個代碼展開,瞬間給了我一種熟悉感:

          image-20210203162542359

          來看一下這個流程吧:

          這個流程我太熟悉了,因為他簡直跟Node.jsCommonJS實現(xiàn)思路一模一樣,具體的可以看我之前寫的這篇文章:深入Node.js的模塊加載機制,手寫require函數(shù)[3]

          1. 先定義一個變量__webpack_module_cache__作為加載了的模塊的緩存
          2. __webpack_require__其實就是用來加載模塊的
          3. 加載模塊時,先檢查緩存中有沒有,如果有,就直接返回緩存
          4. 如果緩存沒有,就從__webpack_modules__將對應的模塊取出來執(zhí)行
          5. __webpack_modules__就是上面第一塊代碼里的那個對象,取出的模塊其實就是我們自己寫的代碼,取出執(zhí)行的也是我們每個模塊的代碼
          6. 每個模塊執(zhí)行除了執(zhí)行我們的邏輯外,還會將export的內(nèi)容添加到module.exports上,這就是前面說的__webpack_require__.d輔助方法的作用。添加到module.exports上其實就是添加到了__webpack_module_cache__緩存上,后面再引用這個模塊就直接從緩存拿了。
        3. 第三塊代碼其實就是我們前面看到過的幾個輔助函數(shù)的定義,具體干啥的,其實他的注釋已經(jīng)寫了:

          1. __webpack_require__.d:核心其實是Object.defineProperty,主要是用來將我們模塊導出的內(nèi)容添加到全局的__webpack_module_cache__緩存上。

            image-20210203164427116
          2. __webpack_require__.o:其實就是Object.prototype.hasOwnProperty的一個簡寫而已。

            image-20210203164450385
          3. __webpack_require__.r:這個方法就是給每個模塊添加一個屬性__esModule,來表明他是一個ES6的模塊。

            image-20210203164658054
          4. 第四塊就一行代碼,調(diào)用__webpack_require__加載入口模塊,啟動執(zhí)行。

        這樣我們將代碼分成了4塊,每塊的作用都搞清楚,其實webpack干的事情就清晰了:

        1. import這種瀏覽器不認識的關(guān)鍵字替換成了__webpack_require__函數(shù)調(diào)用。
        2. __webpack_require__在實現(xiàn)時采用了類似CommonJS的模塊思想。
        3. 一個文件就是一個模塊,對應模塊緩存上的一個對象。
        4. 當模塊代碼執(zhí)行時,會將export的內(nèi)容添加到這個模塊對象上。
        5. 當再次引用一個以前引用過的模塊時,會直接從緩存上讀取模塊。

        自己實現(xiàn)一個webpack

        現(xiàn)在webpack到底干了什么事情我們已經(jīng)清楚了,接下來我們就可以自己動手實現(xiàn)一個了。根據(jù)前面最終生成的代碼結(jié)果,我們要實現(xiàn)的代碼其實主要分兩塊:

        1. 遍歷所有模塊,將每個模塊代碼讀取出來,替換掉importexport關(guān)鍵字,放到__webpack_modules__對象上。
        2. 整個代碼里面除了__webpack_modules__和最后啟動的入口是變化的,其他代碼,像__webpack_require__,__webpack_require__.r這些方法其實都是固定的,整個代碼結(jié)構(gòu)也是固定的,所以完全可以先定義好一個模板。

        使用AST解析代碼

        由于我們需要將import這種代碼轉(zhuǎn)換成瀏覽器能識別的普通JS代碼,所以我們首先要能夠?qū)⒋a解析出來。在解析代碼的時候,可以將它讀出來當成字符串替換,也可以使用更專業(yè)的AST來解析。AST全稱叫Abstract Syntax Trees,也就是抽象語法樹,是一個將代碼用樹來表示的數(shù)據(jù)結(jié)構(gòu),一個代碼可以轉(zhuǎn)換成AST,AST又可以轉(zhuǎn)換成代碼,而我們熟知的babel其實就可以做這個工作。要生成AST很復雜,涉及到編譯原理,但是如果僅僅拿來用就比較簡單了,本文就先不涉及復雜的編譯原理,而是直接將babel生成好的AST拿來使用。

        注意:webpack源碼解析AST并不是使用的babel,而是使用的acorn[4],webpack繼承acornParser,自己實現(xiàn)了一個JavascriptParser[5],本文寫作時采用了babel,這也是一個大家更熟悉的工具。

        比如我先將入口文件讀出來,然后用babel轉(zhuǎn)換成AST可以直接這樣寫:

        const fs = require("fs");
        const parser = require("@babel/parser");

        const config = require("../webpack.config"); // 引入配置文件

        // 讀取入口文件
        const fileContent = fs.readFileSync(config.entry, "utf-8");

        // 使用babel parser解析AST
        const ast = parser.parse(fileContent, { sourceType"module" });

        console.log(ast);   // 把ast打印出來看看

        上面代碼可以將生成好的ast打印在控制臺:

        image-20210207153459699

        這雖然是一個完整的AST,但是看起來并不清晰,關(guān)鍵數(shù)據(jù)其實是body字段,這里的body也只是展示了類型名字。所以照著這個寫代碼其實不好寫,這里推薦一個在線工具https://astexplorer.net/[6],可以很清楚的看到每個節(jié)點的內(nèi)容:

        image-20210207154116026

        從這個解析出來的AST我們可以看到,body主要有4塊代碼:

        1. ImportDeclaration:就是第一行的import定義
        2. VariableDeclaration:第三行的一個變量申明
        3. FunctionDeclaration:第五行的一個函數(shù)定義
        4. ExpressionStatement:第十三行的一個普通語句

        你如果把每個節(jié)點展開,會發(fā)現(xiàn)他們下面又嵌套了很多其他節(jié)點,比如第三行的VariableDeclaration展開后,其實還有個函數(shù)調(diào)用helloWorld()

        image-20210207154741847

        使用traverse遍歷AST

        對于這樣一個生成好的AST,我們可以使用@babel/traverse來對他進行遍歷和操作,比如我想拿到ImportDeclaration進行操作,就直接這樣寫:

        // 使用babel traverse來遍歷ast上的節(jié)點
        traverse(ast, {
          ImportDeclaration(path) {
            console.log(path.node);
          },
        });

        上面代碼可以拿到所有的import語句:

        image-20210207162114290

        import轉(zhuǎn)換為函數(shù)調(diào)用

        前面我們說了,我們的目標是將ES6的import

        import helloWorld from "./helloWorld";

        轉(zhuǎn)換成普通瀏覽器能識別的函數(shù)調(diào)用:

        var _helloWorld__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/helloWorld.js");

        為了實現(xiàn)這個功能,我們還需要引入@babel/types,這個庫可以幫我們創(chuàng)建新的AST節(jié)點,所以這個轉(zhuǎn)換代碼寫出來就是這樣:

        const t = require("@babel/types");

        // 使用babel traverse來遍歷ast上的節(jié)點
        traverse(ast, {
          ImportDeclaration(p) {
            // 獲取被import的文件
            const importFile = p.node.source.value;

            // 獲取文件路徑
            let importFilePath = path.join(path.dirname(config.entry), importFile);
            importFilePath = `./${importFilePath}.js`;

            // 構(gòu)建一個變量定義的AST節(jié)點
            const variableDeclaration = t.variableDeclaration("var", [
              t.variableDeclarator(
                t.identifier(
                  `__${path.basename(importFile)}__WEBPACK_IMPORTED_MODULE_0__`
                ),
                t.callExpression(t.identifier("__webpack_require__"), [
                  t.stringLiteral(importFilePath),
                ])
              ),
            ]);

            // 將當前節(jié)點替換為變量定義節(jié)點
            p.replaceWith(variableDeclaration);
          },
        });

        上面這段代碼我們用了很多@babel/types下面的API,比如t.variableDeclaration,t.variableDeclarator,這些都是用來創(chuàng)建對應的節(jié)點的,具體的API可以看這里[7]。注意這個代碼里面我有很多寫死的地方,比如importFilePath生成邏輯,還應該處理多種后綴名的,還有最終生成的變量名_${path.basename(importFile)}__WEBPACK_IMPORTED_MODULE_0__,最后的數(shù)字我也是直接寫了0,按理來說應該是根據(jù)不同的import順序來生成的,但是本文主要講webpack的原理,這些細節(jié)上我就沒花過多時間了。

        上面的代碼其實是修改了我們的AST,修改后的AST可以用@babel/generator又轉(zhuǎn)換為代碼:

        const generate  = require('@babel/generator').default;

        const newCode = generate(ast).code;
        console.log(newCode);

        這個打印結(jié)果是:

        image-20210207172310114

        可以看到這個結(jié)果里面import helloWorld from "./helloWorld";已經(jīng)被轉(zhuǎn)換為var __helloWorld__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/helloWorld.js");。

        替換import進來的變量

        前面我們將import語句替換成了一個變量定義,變量名字也改為了__helloWorld__WEBPACK_IMPORTED_MODULE_0__,自然要將調(diào)用的地方也改了。為了更好的管理,我們將AST遍歷,操作以及最后的生成新代碼都封裝成一個函數(shù)吧。

        function parseFile(file{
          // 讀取入口文件
          const fileContent = fs.readFileSync(file, "utf-8");

          // 使用babel parser解析AST
          const ast = parser.parse(fileContent, { sourceType"module" });

          let importFilePath = "";

          // 使用babel traverse來遍歷ast上的節(jié)點
          traverse(ast, {
            ImportDeclaration(p) {
              // 跟之前一樣的
            },
          });

          const newCode = generate(ast).code;

          // 返回一個包含必要信息的新對象
          return {
            file,
            dependcies: [importFilePath],
            code: newCode,
          };
        }

        然后啟動執(zhí)行的時候就可以調(diào)這個函數(shù)了

        parseFile(config.entry);

        拿到的結(jié)果跟之前的差不多:

        image-20210207173744463

        好了,現(xiàn)在需要將使用import的地方也替換了,因為我們已經(jīng)知道了這個地方是將它作為函數(shù)調(diào)用的,也就是要將

        const helloWorldStr = helloWorld();

        轉(zhuǎn)為這個樣子:

        const helloWorldStr = (0,_helloWorld__WEBPACK_IMPORTED_MODULE_0__.default)();

        這行代碼的效果其實跟_helloWorld__WEBPACK_IMPORTED_MODULE_0__.default()是一樣的,為啥在前面包個(0, ),我也不知道,有知道的大佬告訴下我唄。

        所以我們在traverse里面加一個CallExpression

          traverse(ast, {
            ImportDeclaration(p) {
              // 跟前面的差不多,省略了
            },
            CallExpression(p) {
              // 如果調(diào)用的是import進來的函數(shù)
              if (p.node.callee.name === importVarName) {
                // 就將它替換為轉(zhuǎn)換后的函數(shù)名字
                p.node.callee.name = `${importCovertVarName}.default`;
              }
            },
          });

        這樣轉(zhuǎn)換后,我們再重新生成一下代碼,已經(jīng)像那么個樣子了:

        image-20210207175649607

        遞歸解析多個文件

        現(xiàn)在我們有了一個parseFile方法來解析處理入口文件,但是我們的文件其實不止一個,我們應該依據(jù)模塊的依賴關(guān)系,遞歸的將所有的模塊都解析了。要實現(xiàn)遞歸解析也不復雜,因為前面的parseFile的依賴dependcies已經(jīng)返回了:

        1. 我們創(chuàng)建一個數(shù)組存放文件的解析結(jié)果,初始狀態(tài)下他只有入口文件的解析結(jié)果
        2. 根據(jù)入口文件的解析結(jié)果,可以拿到入口文件的依賴
        3. 解析所有的依賴,將結(jié)果繼續(xù)加到解析結(jié)果數(shù)組里面
        4. 一直循環(huán)這個解析結(jié)果數(shù)組,將里面的依賴文件解析完
        5. 最后將解析結(jié)果數(shù)組返回就行

        寫成代碼就是這樣:

        function parseFiles(entryFile{
          const entryRes = parseFile(entryFile); // 解析入口文件
          const results = [entryRes]; // 將解析結(jié)果放入一個數(shù)組

          // 循環(huán)結(jié)果數(shù)組,將它的依賴全部拿出來解析
          for (const res of results) {
            const dependencies = res.dependencies;
            dependencies.map((dependency) => {
              if (dependency) {
                const ast = parseFile(dependency);
                results.push(ast);
              }
            });
          }

          return results;
        }

        然后就可以調(diào)用這個方法解析所有文件了:

        const allAst = parseFiles(config.entry);
        console.log(allAst);

        看看解析結(jié)果吧:

        image-20210208152330212

        這個結(jié)果其實跟我們最終需要生成的__webpack_modules__已經(jīng)很像了,但是還有兩塊沒有處理:

        1. 一個是import進來的內(nèi)容作為變量使用,比如

          import hello from './hello';

          const world = 'world';

          const helloWorld = () => `${hello} ${world}`;
        2. 另一個就是export語句還沒處理

        替換import進來的變量(作為變量調(diào)用)

        前面我們已經(jīng)用CallExpression處理過作為函數(shù)使用的import變量了,現(xiàn)在要處理作為變量使用的其實用Identifier處理下就行了,處理邏輯跟之前的CallExpression差不多:

          traverse(ast, {
            ImportDeclaration(p) {
              // 跟以前一樣的
            },
            CallExpression(p) {
           // 跟以前一樣的
            },
            Identifier(p) {
              // 如果調(diào)用的是import進來的變量
              if (p.node.name === importVarName) {
                // 就將它替換為轉(zhuǎn)換后的變量名字
                p.node.name = `${importCovertVarName}.default`;
              }
            },
          });

        現(xiàn)在再運行下,import進來的變量名字已經(jīng)變掉了:

        image-20210208153942630

        替換export語句

        從我們需要生成的結(jié)果來看,export需要進行兩個處理:

        1. 如果一個文件有export default,需要添加一個__webpack_require__.d的輔助方法調(diào)用,內(nèi)容都是固定的,加上就行。
        2. export語句轉(zhuǎn)換為普通的變量定義。

        對應生成結(jié)果上的這兩個:

        image-20210208154959592

        要處理export語句,在遍歷ast的時候添加ExportDefaultDeclaration就行了:

          traverse(ast, {
            ImportDeclaration(p) {
              // 跟以前一樣的
            },
            CallExpression(p) {
           // 跟以前一樣的
            },
            Identifier(p) {
              // 跟以前一樣的
            },
            ExportDefaultDeclaration(p) {
              hasExport = true// 先標記是否有export

              // 跟前面import類似的,創(chuàng)建一個變量定義節(jié)點
              const variableDeclaration = t.variableDeclaration("const", [
                t.variableDeclarator(
                  t.identifier("__WEBPACK_DEFAULT_EXPORT__"),
                  t.identifier(p.node.declaration.name)
                ),
              ]);

              // 將當前節(jié)點替換為變量定義節(jié)點
              p.replaceWith(variableDeclaration);
            },
          });

        然后再運行下就可以看到export語句被替換了:

        image-20210208160244276

        然后就是根據(jù)hasExport變量判斷在AST轉(zhuǎn)換為代碼的時候要不要加__webpack_require__.d輔助函數(shù):

        const EXPORT_DEFAULT_FUN = `
        __webpack_require__.d(__webpack_exports__, {
           "default": () => (__WEBPACK_DEFAULT_EXPORT__)
        });\n
        `
        ;

        function parseFile(file{
          // 省略其他代碼
          // ......
          
          let newCode = generate(ast).code;

          if (hasExport) {
            newCode = `${EXPORT_DEFAULT_FUN} ${newCode}`;
          }
        }

        最后生成的代碼里面export也就處理好了:

        image-20210208161030554

        __webpack_require__.r的調(diào)用添上吧

        前面說了,最終生成的代碼,每個模塊前面都有個__webpack_require__.r的調(diào)用

        image-20210208161321401

        這個只是拿來給模塊添加一個__esModule標記的,我們也給他加上吧,直接在前面export輔助方法后面加點代碼就行了:

        const ESMODULE_TAG_FUN = `
        __webpack_require__.r(__webpack_exports__);\n
        `
        ;

        function parseFile(file{
          // 省略其他代碼
          // ......
          
          let newCode = generate(ast).code;

          if (hasExport) {
            newCode = `${EXPORT_DEFAULT_FUN} ${newCode}`;
          }
          
          // 下面添加模塊標記代碼
          newCode = `${ESMODULE_TAG_FUN} ${newCode}`;
        }

        再運行下看看,這個代碼也加上了:

        image-20210208161721369

        創(chuàng)建代碼模板

        到現(xiàn)在,最難的一塊,模塊代碼的解析和轉(zhuǎn)換我們其實已經(jīng)完成了。下面要做的工作就比較簡單了,因為最終生成的代碼里面,各種輔助方法都是固定的,動態(tài)的部分就是前面解析的模塊和入口文件。所以我們可以創(chuàng)建一個這樣的模板,將動態(tài)的部分標記出來就行,其他不變的部分寫死。這個模板文件的處理,你可以將它讀進來作為字符串處理,也可以用模板引擎,我這里采用ejs模板引擎:

        // 模板文件,直接從webpack生成結(jié)果抄過來,改改就行
        /******/ (() => { // webpackBootstrap
        /******/  "use strict";
        // 需要替換的__TO_REPLACE_WEBPACK_MODULES__
        /******/  var __webpack_modules__ = ({
                        <% __TO_REPLACE_WEBPACK_MODULES__.map(item => { %>
                            '<%- item.file %>' : 
                            ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
                                <%- item.code %>
                            }),
                        <% }) %>
                    });
        // 省略中間的輔助方法
            /************************************************************************/
            /******/  // startup
            /******/  // Load entry module
        // 需要替換的__TO_REPLACE_WEBPACK_ENTRY
            /******/  __webpack_require__('<%- __TO_REPLACE_WEBPACK_ENTRY__ %>');
            /******/  // This entry module used 'exports' so it can't be inlined
            /******/ })()
            ;
            //# sourceMappingURL=main.js.map

        生成最終的代碼

        生成最終代碼的思路就是:

        1. 模板里面用__TO_REPLACE_WEBPACK_MODULES__來生成最終的__webpack_modules__
        2. 模板里面用__TO_REPLACE_WEBPACK_ENTRY__來替代動態(tài)的入口文件
        3. webpack代碼里面使用前面生成好的AST數(shù)組來替換模板的__TO_REPLACE_WEBPACK_MODULES__
        4. webpack代碼里面使用前面拿到的入口文件來替代模板的__TO_REPLACE_WEBPACK_ENTRY__
        5. 使用ejs來生成最終的代碼

        所以代碼就是:

        // 使用ejs將上面解析好的ast傳遞給模板
        // 返回最終生成的代碼
        function generateCode(allAst, entry{
          const temlateFile = fs.readFileSync(
            path.join(__dirname, "./template.js"),
            "utf-8"
          );

          const codes = ejs.render(temlateFile, {
            __TO_REPLACE_WEBPACK_MODULES__: allAst,
            __TO_REPLACE_WEBPACK_ENTRY__: entry,
          });

          return codes;
        }

        大功告成

        最后將ejs生成好的代碼寫入配置的輸出路徑就行了:

        const codes = generateCode(allAst, config.entry);

        fs.writeFileSync(path.join(config.output.path, config.output.filename), codes);

        然后就可以使用我們自己的webpack來編譯代碼,最后就可以像之前那樣打開我們的html看看效果了:

        image-20210218160539306

        總結(jié)

        本文使用簡單質(zhì)樸的方式講述了webpack的基本原理,并自己手寫實現(xiàn)了一個基本的支持importexportdefaultwebpack。

        本文可運行代碼已經(jīng)上傳GitHub,大家可以拿下來玩玩:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Engineering/mini-webpack[8]

        下面再就本文的要點進行下總結(jié):

        1. webpack最基本的功能其實是將JS的高級模塊化語句,importrequire之類的轉(zhuǎn)換為瀏覽器能認識的普通函數(shù)調(diào)用語句。
        2. 要進行語言代碼的轉(zhuǎn)換,我們需要對代碼進行解析。
        3. 常用的解析手段是AST,也就是將代碼轉(zhuǎn)換為抽象語法樹
        4. AST是一個描述代碼結(jié)構(gòu)的樹形數(shù)據(jù)結(jié)構(gòu),代碼可以轉(zhuǎn)換為AST,AST也可以轉(zhuǎn)換為代碼。
        5. babel可以將代碼轉(zhuǎn)換為AST,但是webpack官方并沒有使用babel,而是基于acorn[9]自己實現(xiàn)了一個JavascriptParser[10]。
        6. 本文從webpack構(gòu)建的結(jié)果入手,也使用AST自己生成了一個類似的代碼。
        7. webpack最終生成的代碼其實分為動態(tài)和固定的兩部分,我們將固定的部分寫入一個模板,動態(tài)的部分在模板里面使用ejs占位。
        8. 生成代碼動態(tài)部分需要借助babel來生成AST,并對其進行修改,最后再使用babel將其生成新的代碼。
        9. 在生成AST時,我們從配置的入口文件開始,遞歸的解析所有文件。即解析入口文件的時候,將它的依賴記錄下來,入口文件解析完后就去解析他的依賴文件,在解析他的依賴文件時,將依賴的依賴也記錄下來,后面繼續(xù)解析。重復這種步驟,直到所有依賴解析完。
        10. 動態(tài)代碼生成好后,使用ejs將其寫入模板,以生成最終的代碼。
        11. 如果要支持require或者AMD,其實思路是類似的,最終生成的代碼也是差不多的,主要的差別在AST解析那一塊。

        參考資料

        1. babel操作AST文檔[11]
        2. webpack源碼[12]
        3. webpack官方文檔[13]

        參考資料

        [1]

        https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Engineering/mini-webpack: https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Engineering/mini-webpack

        [2]

        具體的可以看他的文檔: https://webpack.docschina.org/configuration/devtool/

        [3]

        深入Node.js的模塊加載機制,手寫require函數(shù): https://juejin.cn/post/6866973719634542606

        [4]

        acorn: https://github.com/acornjs/acorn

        [5]

        JavascriptParser: https://github.com/webpack/webpack/blob/a07a1269f0a0b23d40de6c9565eeaf962fbc8904/lib/javascript/JavascriptParser.js

        [6]

        https://astexplorer.net/: https://astexplorer.net/

        [7]

        具體的API可以看這里: https://babeljs.io/docs/en/babel-types#variabledeclaration

        [8]

        https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Engineering/mini-webpack: https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Engineering/mini-webpack

        [9]

        acorn: https://github.com/acornjs/acorn

        [10]

        JavascriptParser: https://github.com/webpack/webpack/blob/a07a1269f0a0b23d40de6c9565eeaf962fbc8904/lib/javascript/JavascriptParser.js

        [11]

        babel操作AST文檔: https://babeljs.io/docs/en/babel-types

        [12]

        webpack源碼: https://github.com/webpack/webpack/

        [13]

        webpack官方文檔: https://webpack.js.org/concepts/

        瀏覽 46
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            无码免费高清视频| 免费在线观看黄色视频网站| 日韩中文字幕无码中字字幕| 欧美午夜激情视频| 91丨精品丨国产丨丝袜| 亚洲一区日韩| 中日美朝美女一级片免费看| 黄色片免费观看| 91小宝寻花一区二区三区三级 | 亚洲精品乱码久久久久久蜜桃91| 国产—a毛—a毛A免费看图| 国产一区二区电影| 国产亚洲欧美视频| 午夜专区| 二级黄色视频| 影音先锋乱伦电影| 五月婷婷色欲| 人人狠狠综合婷婷| 日韩18禁| 亚洲性爱在线观看| 中文字幕欧美在线| 人妻FrXXeeXXee护士| 国产无码免费在线观看| 99高清无码| 中文字幕一区二区三区日本在线| 亚洲欧美日韩另类| 永久m3u8在线观看| www,久久久| 不卡的AV| 日韩综合精品| 一级成人电影| 91国产视频网站| 国产精品爽爽久久久| 激情日逼| 久久嫩草精品久久久久精| 亚洲无码精品视频| 亚洲有码中文字幕| 亚洲无码电影在线| 深爱婷婷| 黄色A级毛片| 韩国精品无码一区二区三区18| 天天舔天天日| 大黑人荫蒂BBBBBBBBB| 一级内射视频| 婷婷狠狠干| 欧美操BB| 在线观看av网站| 十八禁视频在线观看网站.www| 亚洲成人精品一区二区| 影音先锋成人在线资源| 影音先锋成人AV资源| 超碰欧美在线| 伊人久久无码| 亚洲精品无码免费| 91理论片| 成人69AV| 少妇中文字幕| 免费黄色成人| A片在线免费| 婷婷成人视频| 欧美激情四射| 欧美色图另类图片| 国产嘿嘿| 亚洲伊人成人| 日本爱爱网址| 国产精品码一本A片| 色天天干| 日韩天堂在线观看| 亚洲中文幕| 青青在线免费视频| www.91com| 热99在线| 99久久婷婷国产综合精品漫| 闺蜜av| 国产综合网站| 福利三区| 黄色一级在线观看| 欧美成人免费网站| av先锋资源| 午夜免费视频1000| 韩国精品无码一区二区三区18| 亚洲精品国产AV| 国产ts在线| 久久国产无码| 成人无码在线观看免费视频| 中文字幕免费在线观看| 怡红影院美乳| 中文字幕高清无码免费视频| 大香蕉伊人成人网| 欧美一级片免费看| 欧美成人精品一级| 好逼天天操| 无码不卡在线播放| AV无码网| 亚欧一区二区| 色婷五月| 欧美综合第一页| 欧美XXX黑人XYX性爽| 亚洲av| 伊人久久艹| 人妻少妇无码| 日韩AV在线电影| 久久久一区二区三区四区免费听| 丁香六月婷婷久久综合| 中文字幕三级片在线观看| 天天视频狠狠狠狠| 久艹99| 国产又粗又长又硬又大毛苴茸图片| 亚洲欧美在线成人| 另类老妇极品BBWBBw| 97人妻人人澡人人爽人人| A级免费毛片| 久操视频在线观看免费| 91人妻网| 久久久久久亚洲AV无码专区| 无码av在线观看| 亚洲精品区| 日韩东京热中文字幕| 日韩性爱一区二区| 先锋影音亚洲AV每日资源网站| 星空AV| 国产性爱免费视频| 99re在线观看观看这里只有精品 | 日韩精品一区二区亚洲AV观看| 久久婷婷婬片A片AAA| 911香蕉视频| 欧美日韩视频在线播放| 亚洲成人av在线观看| 麻豆免费成人传媒| 91在线无码精品秘国产三年| 亚洲性爱AV网站| 亚洲1234区| 97人妻人人澡人人| 国产亚洲成人综合| 四虎成人精品在永久免费| 热99视频| 操屄影院| 午夜aaa| 91大长腿美女花外围在线观看| 日韩精品成人AV| 日韩午夜无码| 亚洲黄色在线看| 久久99精品久久久久久水蜜桃 | 免费在线观看黄| 狠狠色一区| av日韩在线播放| 日韩激情一区二区| 亚洲激情无码视频| 国产美女全裸网站| 亚洲综合91| 四川BBBB擦BBBB| 天堂AV色| 国模无码在线| 伊人成人在线视频观看| 国产亚洲欧洲| 亚洲成人精品视频| 动漫3d啪啪成人h动漫| 亚洲看片| 波多野结衣高清av久久直播免| 大蕉伊人网| 色婷婷久综合久久一本国产AV| 亚洲无码手机在线观看| 91亚洲国产成人精品一区| 国产精品一区在线观看| AV天堂国产| 91AV一区二区三区| 五月丁香婷婷啪啪| 亚洲性爱小说| 夜色福利视频| 亚洲无码高清在线观看| AV天堂国产| 91麻豆电影| 色爱av| jizz视频| 免费视频a| 懂色av粉嫩AV蜜臀AV| 婷婷二区| 91美女被操| 美日韩免费视频| 翔田千里无码XXXXXX| 久久精品99久久久久久久久| 好逼123| 一区二区三区四区av| 欧美级毛片一进一出夜本色| 去干网欧美| 黄色一级A片| 无码精品人妻一区二区欧美| 免费无码婬片aaaa| 日本一区二区三区在线播放| 在线视频日韩| 91中文字幕| 麻豆911精一区二区| 激情综| 国产一级女婬乱免费看| 成人在线视频免费| 国精产品秘一区二区| 久久久久亚洲AV无码专区成人| 开心五月激情网| 日本爱爱视频免费| 日韩欧美不卡| 啊哈嗯| 欧美A级成人婬片免费看| 午夜精品视频在线观看| 99精品视频在线播放免费| 亚洲图片激情乱伦小说| 91AV天天在线观看| 丰满人妻一区二区三区四区54| 人人搞人人摸| 超碰人人干| 骚视频网站| 一区免费在线| 亚洲第一毛片| 欧美一区二区在线观看| 日韩网站在线| 综合色色婷婷| 国产一级A片久久久免费看快餐| 国产操b| 免费看黄色电影| aaa三级片| 国产综合久久777777麻豆| 影音先锋成人在线| 亚洲伦理一区二区| 成年人黄色视频网站| 精品免费| 日韩无码成人| 婷婷综合av| 麻豆黄色| 亚洲无码高清视频在线观看| 黑人无码一二三四五区| a片在线免费播放| 91乱子伦国产乱| 18禁91| 欧美h网站| 久草电影在线观看| 日韩AV免费在线| 国产三级在线观看| 日本黄色大片网站| AV资源网站| 欧美黄色性爱视频| 免费AV观看| 国产做爰XXXⅩ久久久骚妇| 国产香蕉在线观看| 亚洲无码免费观看| 日本免费在线观看视频| 欧美日韩成人一区二区三区| 婷婷综合色| 亚洲第一色在线| 欧美女人日逼视频| 国产17c精品视频一二三区| 久草福利| 91人妻人人澡人人爽人人DVD | 国产精品你懂的| 老司机福利在线视频| 操操片| 久久婷五月| 日本黄色视频官网| 日韩色逼| 无码精品人妻一区二区三区漫画| 韩国毛片基地久久| 荫蒂添的高潮免费视频| 日韩色综合| 亚洲福利在线观看| 人妻无码中文字幕蜜桃| 操屄视频在线| 成人视频网站18| 精品一区二区三区四区学生| 成人激情综合| 成人aaa| 99re99热| 国产精品99久久久久久成人| 日本黄色免费网站| 激情五月天激情网| 无码人妻精品一区二区三区蜜臀百度| 黄网站免费观看| 在线一区观看| 国产不卡视频| 久久亚洲视频| 日韩做爱网站| 2025av天堂| 日本高清色清di免费观看| 欧美国产在线观看| 亚洲无码影片| 午夜免费性爱视频| AV在线导航| 91人妻人人澡人人爽人人| 99日韩无码| 久久婷婷国产综合| 日韩A级毛片| 99久久精彩视频| 中文在线字幕免费观看| 中文字幕在线视频观看| AA无码| 色五月婷婷丁香五月| 成人片毛片| 亚洲AV成人无码久久精品麻豆| 成人免费视频一区| 青青草中文字幕| 国产一级一片免费播放放a| 亚洲影音先锋在线| 天天操人妻|