手寫(xiě)一個(gè)webpack,看看AST怎么用
本文開(kāi)始我會(huì)圍繞webpack和babel寫(xiě)一系列的工程化文章,這兩個(gè)工具我雖然天天用,但是對(duì)他們的原理理解的其實(shí)不是很深入,寫(xiě)這些文章的過(guò)程其實(shí)也是我深入學(xué)習(xí)的過(guò)程。由于webpack和babel的體系太大,知識(shí)點(diǎn)眾多,不可能一篇文章囊括所有知識(shí)點(diǎn),目前我的計(jì)劃是從簡(jiǎn)單入手,先實(shí)現(xiàn)一個(gè)最簡(jiǎn)單的可以運(yùn)行的webpack,然后再看看plugin, loader和tree shaking等功能。目前我計(jì)劃會(huì)有這些文章:
手寫(xiě)最簡(jiǎn) webpack,也就是本文webpack的plugin實(shí)現(xiàn)原理webpack的loader實(shí)現(xiàn)原理webpack的tree shaking實(shí)現(xiàn)原理webpack的HMR實(shí)現(xiàn)原理babel和ast原理
所有文章都是原理或者源碼解析,歡迎關(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),而且只處理了import和export的default情況,如果你想在生產(chǎn)環(huán)境使用,請(qǐng)自己添加其他情況的處理和邊界判斷。
為什么要用webpack
筆者剛開(kāi)始做前端時(shí),其實(shí)不知道什么webpack,也不懂模塊化,都是html里面直接寫(xiě)script,引入jquery直接干。所以如果一個(gè)頁(yè)面的JS需要依賴(lài)jquery和lodash,那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)題:
單獨(dú)看 index.js不能清晰的找到他到底依賴(lài)哪些外部庫(kù)script的順序必須寫(xiě)正確,如果錯(cuò)了就會(huì)導(dǎo)致找不到依賴(lài),直接報(bào)錯(cuò)模塊間通信困難,基本都靠往 window上注入變量來(lái)暴露給外部瀏覽器嚴(yán)格按照 script標(biāo)簽來(lái)下載代碼,有些沒(méi)用到的代碼也會(huì)下載下來(lái)當(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ū)提出了很多模塊管理方案,比如ES2015的import,CommonJS的require,另外還有AMD,CMD等等。就目前我見(jiàn)到的情況來(lái)說(shuō),import因?yàn)橐呀?jīng)成為ES2015標(biāo)準(zhǔn),所以在客戶(hù)端廣泛使用,而require是Node.js的自帶模塊管理機(jī)制,也有很廣泛的用途,而AMD和CMD的使用已經(jīng)很少見(jiàn)了。
但是webpack作為一個(gè)開(kāi)放的模塊化工具,他是支持ES6,CommonJS和AMD等多種標(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,將hello和world拼成一句話(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)單。
先把依賴(lài)裝上吧,這沒(méi)什么好說(shuō)的:
//?package.json
{
??"devDependencies":?{
????"webpack":?"^5.4.0",
????"webpack-cli":?"^4.2.0"
??},
}為了使用方便,再加個(gè)
build腳本吧://?package.json
{
??"scripts":?{
????"build":?"webpack"
??},
}最后再簡(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è)配置
mode和devtool只是我用來(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]。然后就可以在
dist下面建個(gè)index.html來(lái)引用編譯后的代碼了://?index.html
html>
<html>
??<head>
????<meta?charset="utf-8"?/>
??head>
??<body>
????<script?src="main.js">script>
??body>
html>運(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ì)藏著這么黑科技。
下面是編譯完的代碼截圖:

雖然我們只有三個(gè)簡(jiǎn)單的JS文件,但是加上webpack自己的邏輯,編譯后的文件還是有一百多行代碼,所以即使我把具體邏輯折疊起來(lái)了,這個(gè)截圖還是有點(diǎn)長(zhǎng),為了能夠看清楚他的結(jié)構(gòu),我將它分成了4個(gè)部分,標(biāo)記在了截圖上,下面我們分別來(lái)看看這幾個(gè)部分吧。
第一部分其實(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)換然后保存一下模塊代碼而已。第二塊代碼的核心是
__webpack_require__,這個(gè)代碼展開(kāi),瞬間給了我一種熟悉感:
image-20210203162542359 來(lái)看一下這個(gè)流程吧:
這個(gè)流程我太熟悉了,因?yàn)樗?jiǎn)直跟
Node.js的CommonJS實(shí)現(xiàn)思路一模一樣,具體的可以看我之前寫(xiě)的這篇文章:深入Node.js的模塊加載機(jī)制,手寫(xiě)require函數(shù)[3]。先定義一個(gè)變量 __webpack_module_cache__作為加載了的模塊的緩存__webpack_require__其實(shí)就是用來(lái)加載模塊的加載模塊時(shí),先檢查緩存中有沒(méi)有,如果有,就直接返回緩存 如果緩存沒(méi)有,就從 __webpack_modules__將對(duì)應(yīng)的模塊取出來(lái)執(zhí)行__webpack_modules__就是上面第一塊代碼里的那個(gè)對(duì)象,取出的模塊其實(shí)就是我們自己寫(xiě)的代碼,取出執(zhí)行的也是我們每個(gè)模塊的代碼每個(gè)模塊執(zhí)行除了執(zhí)行我們的邏輯外,還會(huì)將 export的內(nèi)容添加到module.exports上,這就是前面說(shuō)的__webpack_require__.d輔助方法的作用。添加到module.exports上其實(shí)就是添加到了__webpack_module_cache__緩存上,后面再引用這個(gè)模塊就直接從緩存拿了。第三塊代碼其實(shí)就是我們前面看到過(guò)的幾個(gè)輔助函數(shù)的定義,具體干啥的,其實(shí)他的注釋已經(jīng)寫(xiě)了:
__webpack_require__.d:核心其實(shí)是Object.defineProperty,主要是用來(lái)將我們模塊導(dǎo)出的內(nèi)容添加到全局的__webpack_module_cache__緩存上。
image-20210203164427116 __webpack_require__.o:其實(shí)就是Object.prototype.hasOwnProperty的一個(gè)簡(jiǎn)寫(xiě)而已。
image-20210203164450385 __webpack_require__.r:這個(gè)方法就是給每個(gè)模塊添加一個(gè)屬性__esModule,來(lái)表明他是一個(gè)ES6的模塊。
image-20210203164658054 第四塊就一行代碼,調(diào)用
__webpack_require__加載入口模塊,啟動(dòng)執(zhí)行。
這樣我們將代碼分成了4塊,每塊的作用都搞清楚,其實(shí)webpack干的事情就清晰了:
將 import這種瀏覽器不認(rèn)識(shí)的關(guān)鍵字替換成了__webpack_require__函數(shù)調(diào)用。__webpack_require__在實(shí)現(xiàn)時(shí)采用了類(lèi)似CommonJS的模塊思想。一個(gè)文件就是一個(gè)模塊,對(duì)應(yīng)模塊緩存上的一個(gè)對(duì)象。 當(dāng)模塊代碼執(zhí)行時(shí),會(huì)將 export的內(nèi)容添加到這個(gè)模塊對(duì)象上。當(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í)主要分兩塊:
遍歷所有模塊,將每個(gè)模塊代碼讀取出來(lái),替換掉 import和export關(guān)鍵字,放到__webpack_modules__對(duì)象上。整個(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繼承acorn的Parser,自己實(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):

這雖然是一個(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)容:

從這個(gè)解析出來(lái)的AST我們可以看到,body主要有4塊代碼:
ImportDeclaration:就是第一行的import定義VariableDeclaration:第三行的一個(gè)變量申明FunctionDeclaration:第五行的一個(gè)函數(shù)定義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():

使用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ǔ)句:

將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é)果是:

可以看到這個(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é)果跟之前的差不多:

好了,現(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è)樣子了:

遞歸解析多個(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)返回了:
我們創(chuàng)建一個(gè)數(shù)組存放文件的解析結(jié)果,初始狀態(tài)下他只有入口文件的解析結(jié)果 根據(jù)入口文件的解析結(jié)果,可以拿到入口文件的依賴(lài) 解析所有的依賴(lài),將結(jié)果繼續(xù)加到解析結(jié)果數(shù)組里面 一直循環(huán)這個(gè)解析結(jié)果數(shù)組,將里面的依賴(lài)文件解析完 最后將解析結(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é)果吧:

這個(gè)結(jié)果其實(shí)跟我們最終需要生成的__webpack_modules__已經(jīng)很像了,但是還有兩塊沒(méi)有處理:
一個(gè)是
import進(jìn)來(lái)的內(nèi)容作為變量使用,比如import?hello?from?'./hello';
const?world?=?'world';
const?helloWorld?=?()?=>?`${hello}?${world}`;另一個(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)變掉了:

替換export語(yǔ)句
從我們需要生成的結(jié)果來(lái)看,export需要進(jìn)行兩個(gè)處理:
如果一個(gè)文件有 export default,需要添加一個(gè)__webpack_require__.d的輔助方法調(diào)用,內(nèi)容都是固定的,加上就行。將 export語(yǔ)句轉(zhuǎn)換為普通的變量定義。
對(duì)應(yīng)生成結(jié)果上的這兩個(gè):

要處理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ǔ)句被替換了:

然后就是根據(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也就處理好了:

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

這個(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è)代碼也加上了:

創(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
生成最終的代碼
生成最終代碼的思路就是:
模板里面用 __TO_REPLACE_WEBPACK_MODULES__來(lái)生成最終的__webpack_modules__模板里面用 __TO_REPLACE_WEBPACK_ENTRY__來(lái)替代動(dòng)態(tài)的入口文件webpack代碼里面使用前面生成好的AST數(shù)組來(lái)替換模板的__TO_REPLACE_WEBPACK_MODULES__webpack代碼里面使用前面拿到的入口文件來(lái)替代模板的__TO_REPLACE_WEBPACK_ENTRY__使用 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看看效果了:

總結(jié)
本文使用簡(jiǎn)單質(zhì)樸的方式講述了webpack的基本原理,并自己手寫(xiě)實(shí)現(xiàn)了一個(gè)基本的支持import和export的default的webpack。
本文可運(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é):
webpack最基本的功能其實(shí)是將JS的高級(jí)模塊化語(yǔ)句,import和require之類(lèi)的轉(zhuǎn)換為瀏覽器能認(rèn)識(shí)的普通函數(shù)調(diào)用語(yǔ)句。要進(jìn)行語(yǔ)言代碼的轉(zhuǎn)換,我們需要對(duì)代碼進(jìn)行解析。 常用的解析手段是 AST,也就是將代碼轉(zhuǎn)換為抽象語(yǔ)法樹(shù)。AST是一個(gè)描述代碼結(jié)構(gòu)的樹(shù)形數(shù)據(jù)結(jié)構(gòu),代碼可以轉(zhuǎn)換為AST,AST也可以轉(zhuǎn)換為代碼。babel可以將代碼轉(zhuǎn)換為AST,但是webpack官方并沒(méi)有使用babel,而是基于acorn[9]自己實(shí)現(xiàn)了一個(gè)JavascriptParser[10]。本文從 webpack構(gòu)建的結(jié)果入手,也使用AST自己生成了一個(gè)類(lèi)似的代碼。webpack最終生成的代碼其實(shí)分為動(dòng)態(tài)和固定的兩部分,我們將固定的部分寫(xiě)入一個(gè)模板,動(dòng)態(tài)的部分在模板里面使用ejs占位。生成代碼動(dòng)態(tài)部分需要借助 babel來(lái)生成AST,并對(duì)其進(jìn)行修改,最后再使用babel將其生成新的代碼。在生成 AST時(shí),我們從配置的入口文件開(kāi)始,遞歸的解析所有文件。即解析入口文件的時(shí)候,將它的依賴(lài)記錄下來(lái),入口文件解析完后就去解析他的依賴(lài)文件,在解析他的依賴(lài)文件時(shí),將依賴(lài)的依賴(lài)也記錄下來(lái),后面繼續(xù)解析。重復(fù)這種步驟,直到所有依賴(lài)解析完。動(dòng)態(tài)代碼生成好后,使用 ejs將其寫(xiě)入模板,以生成最終的代碼。如果要支持 require或者AMD,其實(shí)思路是類(lèi)似的,最終生成的代碼也是差不多的,主要的差別在AST解析那一塊。
參考資料
babel操作AST文檔[11] webpack源碼[12] webpack官方文檔[13]
覺(jué)得博主寫(xiě)得還可以的話(huà),不要忘了分享、點(diǎn)贊、在看三連哦~
長(zhǎng)按下方圖片,關(guān)注進(jìn)擊的大前端,獲取更多的優(yōu)質(zhì)原創(chuàng)文章~?
參考資料
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/
