<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          手寫(xiě)一個(gè)webpack,看看AST怎么用

          共 16587字,需瀏覽 34分鐘

           ·

          2021-02-20 14:36

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

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

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

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

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

          為什么要用webpack

          筆者剛開(kāi)始做前端時(shí),其實(shí)不知道什么webpack,也不懂模塊化,都是html里面直接寫(xiě)script,引入jquery直接干。所以如果一個(gè)頁(yè)面的JS需要依賴(lài)jquerylodash,那html可能就長(zhǎng)這樣:

          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>

          這樣寫(xiě)會(huì)導(dǎo)致幾個(gè)問(wèn)題:

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

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

          但是webpack作為一個(gè)開(kāi)放的模塊化工具,他是支持ES6,CommonJSAMD等多種標(biāo)準(zhǔn)的,不同的模塊化標(biāo)準(zhǔn)有不同的解析方法,本文只會(huì)講ES6標(biāo)準(zhǔn)的import方案,這也是客戶(hù)端JS使用最多的方案。

          簡(jiǎn)單例子

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

          先來(lái)建一個(gè)hello.js,只導(dǎo)出一個(gè)簡(jiǎn)單的字符串:

          const?hello?=?'hello';

          export?default?hello;

          然后再來(lái)一個(gè)helloWorld.js,將helloworld拼成一句話(huà),并導(dǎo)出拼接的這個(gè)方法:

          import?hello?from?'./hello';

          const?world?=?'world';

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

          export?default?helloWorld;

          最后再來(lái)個(gè)index.js,將拼好的hello world插入到頁(yè)面上去:

          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是不能運(yùn)行成功的,因?yàn)榇蟛糠譃g覽器都不支持import這種模塊導(dǎo)入。而webpack就是來(lái)解決這個(gè)問(wèn)題的,它會(huì)將我們模塊化的代碼轉(zhuǎn)換成瀏覽器認(rèn)識(shí)的普通JS來(lái)執(zhí)行。

          引入webpack

          我們印象中webpack的配置很多,很麻煩,但那是因?yàn)槲覀冃枰_(kāi)啟的功能很多,如果只是解析轉(zhuǎn)換import,配置起來(lái)非常簡(jiǎn)單。

          1. 先把依賴(lài)裝上吧,這沒(méi)什么好說(shuō)的:

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

            //?package.json
            {
            ??"scripts":?{
            ????"build":?"webpack"
            ??},
            }
          3. 最后再簡(jiǎn)單寫(xiě)下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"),
            ??},
            };

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

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

          4. 然后就可以在dist下面建個(gè)index.html來(lái)引用編譯后的代碼了:

            //?index.html

            html>
            <html>
            ??<head>
            ????<meta?charset="utf-8"?/>
            ??head>
            ??<body>
            ????<script?src="main.js">script>
            ??body>
            html>
          5. 運(yùn)行下yarn build就會(huì)編譯我們的代碼,然后打開(kāi)index.html就可以看到效果了。

            image-20210203154111168

          深入原理

          前面講的這個(gè)例子很簡(jiǎn)單,一般也滿(mǎn)足不了我們實(shí)際工程中的需求,但是對(duì)于我們理解原理卻是一個(gè)很好的突破口,畢竟webpack這么龐大的一個(gè)體系,我們也不能一口吃個(gè)胖子,得一點(diǎn)一點(diǎn)來(lái)。

          webpack把代碼編譯成了啥?

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

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

          image-20210203155553091

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

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

            image-20210203161613636

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

            image-20210203161902647

            他只是在我們的代碼前先調(diào)用了__webpack_require__.r__webpack_require__.d,這兩個(gè)輔助函數(shù)我們?cè)诤竺鏁?huì)看到。

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

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

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

            image-20210203162542359

            來(lái)看一下這個(gè)流程吧:

            這個(gè)流程我太熟悉了,因?yàn)樗?jiǎn)直跟Node.jsCommonJS實(shí)現(xiàn)思路一模一樣,具體的可以看我之前寫(xiě)的這篇文章:深入Node.js的模塊加載機(jī)制,手寫(xiě)require函數(shù)[3]。

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

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

              image-20210203164427116
            2. __webpack_require__.o:其實(shí)就是Object.prototype.hasOwnProperty的一個(gè)簡(jiǎn)寫(xiě)而已。

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

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

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

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

          自己實(shí)現(xiàn)一個(gè)webpack

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

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

          使用AST解析代碼

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

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

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

          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);???//?把a(bǔ)st打印出來(lái)看看

          上面代碼可以將生成好的ast打印在控制臺(tái):

          image-20210207153459699

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

          image-20210207154116026

          從這個(gè)解析出來(lái)的AST我們可以看到,body主要有4塊代碼:

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

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

          image-20210207154741847

          使用traverse遍歷AST

          對(duì)于這樣一個(gè)生成好的AST,我們可以使用@babel/traverse來(lái)對(duì)他進(jìn)行遍歷和操作,比如我想拿到ImportDeclaration進(jìn)行操作,就直接這樣寫(xiě):

          //?使用babel?traverse來(lái)遍歷ast上的節(jié)點(diǎn)
          traverse(ast,?{
          ??ImportDeclaration(path)?{
          ????console.log(path.node);
          ??},
          });

          上面代碼可以拿到所有的import語(yǔ)句:

          image-20210207162114290

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

          前面我們說(shuō)了,我們的目標(biāo)是將ES6的import

          import?helloWorld?from?"./helloWorld";

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

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

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

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

          //?使用babel?traverse來(lái)遍歷ast上的節(jié)點(diǎn)
          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)建一個(gè)變量定義的AST節(jié)點(diǎn)
          ????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),
          ????????])
          ??????),
          ????]);

          ????//?將當(dāng)前節(jié)點(diǎn)替換為變量定義節(jié)點(diǎn)
          ????p.replaceWith(variableDeclaration);
          ??},
          });

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

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

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

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

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

          image-20210207172310114

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

          替換import進(jìn)來(lái)的變量

          前面我們將import語(yǔ)句替換成了一個(gè)變量定義,變量名字也改為了__helloWorld__WEBPACK_IMPORTED_MODULE_0__,自然要將調(diào)用的地方也改了。為了更好的管理,我們將AST遍歷,操作以及最后的生成新代碼都封裝成一個(gè)函數(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來(lái)遍歷ast上的節(jié)點(diǎn)
          ??traverse(ast,?{
          ????ImportDeclaration(p)?{
          ??????//?跟之前一樣的
          ????},
          ??});

          ??const?newCode?=?generate(ast).code;

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

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

          parseFile(config.entry);

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

          image-20210207173744463

          好了,現(xiàn)在需要將使用import的地方也替換了,因?yàn)槲覀円呀?jīng)知道了這個(gè)地方是將它作為函數(shù)調(diào)用的,也就是要將

          const?helloWorldStr?=?helloWorld();

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

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

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

          所以我們?cè)?code style="font-size: 14px;overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;color: rgb(30, 107, 184);background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;">traverse里面加一個(gè)CallExpression

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

          這樣轉(zhuǎn)換后,我們?cè)僦匦律梢幌麓a,已經(jīng)像那么個(gè)樣子了:

          image-20210207175649607

          遞歸解析多個(gè)文件

          現(xiàn)在我們有了一個(gè)parseFile方法來(lái)解析處理入口文件,但是我們的文件其實(shí)不止一個(gè),我們應(yīng)該依據(jù)模塊的依賴(lài)關(guān)系,遞歸的將所有的模塊都解析了。要實(shí)現(xiàn)遞歸解析也不復(fù)雜,因?yàn)榍懊娴?code style="font-size: 14px;overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;color: rgb(30, 107, 184);background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;">parseFile的依賴(lài)dependcies已經(jīng)返回了:

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

          寫(xiě)成代碼就是這樣:

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

          ??//?循環(huán)結(jié)果數(shù)組,將它的依賴(lài)全部拿出來(lái)解析
          ??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)用這個(gè)方法解析所有文件了:

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

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

          image-20210208152330212

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

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

            import?hello?from?'./hello';

            const?world?=?'world';

            const?helloWorld?=?()?=>?`${hello}?${world}`;
          2. 另一個(gè)就是export語(yǔ)句還沒(méi)處理

          替換import進(jìn)來(lái)的變量(作為變量調(diào)用)

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

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

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

          image-20210208153942630

          替換export語(yǔ)句

          從我們需要生成的結(jié)果來(lái)看,export需要進(jìn)行兩個(gè)處理:

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

          對(duì)應(yīng)生成結(jié)果上的這兩個(gè):

          image-20210208154959592

          要處理export語(yǔ)句,在遍歷ast的時(shí)候添加ExportDefaultDeclaration就行了:

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

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

          ??????//?將當(dāng)前節(jié)點(diǎn)替換為變量定義節(jié)點(diǎn)
          ??????p.replaceWith(variableDeclaration);
          ????},
          ??});

          然后再運(yùn)行下就可以看到export語(yǔ)句被替換了:

          image-20210208160244276

          然后就是根據(jù)hasExport變量判斷在AST轉(zhuǎn)換為代碼的時(shí)候要不要加__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)用添上吧

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

          image-20210208161321401

          這個(gè)只是拿來(lái)給模塊添加一個(gè)__esModule標(biāo)記的,我們也給他加上吧,直接在前面export輔助方法后面加點(diǎn)代碼就行了:

          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}`;
          ??}
          ??
          ??//?下面添加模塊標(biāo)記代碼
          ??newCode?=?`${ESMODULE_TAG_FUN}?${newCode}`;
          }

          再運(yùn)行下看看,這個(gè)代碼也加上了:

          image-20210208161721369

          創(chuàng)建代碼模板

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

          //?模板文件,直接從webpack生成結(jié)果抄過(guò)來(lái),改改就行
          /******/?(()?=>?{?//?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__來(lái)生成最終的__webpack_modules__
          2. 模板里面用__TO_REPLACE_WEBPACK_ENTRY__來(lái)替代動(dòng)態(tài)的入口文件
          3. webpack代碼里面使用前面生成好的AST數(shù)組來(lái)替換模板的__TO_REPLACE_WEBPACK_MODULES__
          4. webpack代碼里面使用前面拿到的入口文件來(lái)替代模板的__TO_REPLACE_WEBPACK_ENTRY__
          5. 使用ejs來(lái)生成最終的代碼

          所以代碼就是:

          //?使用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生成好的代碼寫(xiě)入配置的輸出路徑就行了:

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

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

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

          image-20210218160539306

          總結(jié)

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

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

          下面再就本文的要點(diǎn)進(jìn)行下總結(jié):

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

          參考資料

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


          覺(jué)得博主寫(xiě)得還可以的話(huà),不要忘了分享、點(diǎn)贊、在看三連哦~

          長(zhǎng)按下方圖片,關(guān)注進(jìn)擊的大前端,獲取更多的優(yōu)質(zhì)原創(chuàng)文章~?

          參考資料

          [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的模塊加載機(jī)制,手寫(xiě)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/

          瀏覽 49
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产成人黄 | 精品做爱视频在线观看 | 欧美一级操逼逼 | 一级a一级a爰片免费啪啪女女 | 国产毛片AV一区二区三区牛牛影视 |