手寫一個webpack,看看AST怎麼用
本文開始我會圍繞webpack
和babel
寫一系列的工程化文章,這兩個工具我雖然天天用,但是對他們的原理理解的其實不是很深入,寫這些文章的過程其實也是我深入學習的過程。由於webpack
和babel
的體系太大,知識點眾多,不可能一篇文章囊括所有知識點,目前我的計劃是從簡單入手,先實現一個最簡單的可以運行的webpack
,然後再看看plugin
, loader
和tree shaking
等功能。目前我計劃會有這些文章:
- 手寫最簡
webpack
,也就是本文 webpack
的plugin
實現原理webpack
的loader
實現原理webpack
的tree shaking
實現原理webpack
的HMR
實現原理babel
和ast
原理
所有文章都是原理或者源碼解析,歡迎關注~
本文可運行程式碼已經上傳GitHub,大家可以拿下來玩玩://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Engineering/mini-webpack
注意:本文主要講webpack
原理,在實現時並不嚴謹,而且只處理了import
和export
的default
情況,如果你想在生產環境使用,請自己添加其他情況的處理和邊界判斷。
為什麼要用webpack
筆者剛開始做前端時,其實不知道什麼webpack
,也不懂模組化,都是html
裡面直接寫script
,引入jquery
直接干。所以如果一個頁面的JS需要依賴jquery
和lodash
,那html
可能就長這樣:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<script src="//unpkg.com/[email protected]"></script>
<script src="//unpkg.com/[email protected]"></script>
<script src="./src/index.js"></script>
</head>
<body>
</body>
</html>
這樣寫會導致幾個問題:
- 單獨看
index.js
不能清晰的找到他到底依賴哪些外部庫 script
的順序必須寫正確,如果錯了就會導致找不到依賴,直接報錯- 模組間通訊困難,基本都靠往
window
上注入變數來暴露給外部 - 瀏覽器嚴格按照
script
標籤來下載程式碼,有些沒用到的程式碼也會下載下來 - 當前端規模變大,JS腳本會顯得很雜亂,項目管理混亂
webpack
的一個最基本的功能就是來解決上述的情況,允許在JS裡面通過import
或者require
等關鍵字來顯式申明依賴,可以引用第三方庫,自己的JS程式碼間也可以相互引用,這樣在實質上就實現了前端程式碼的模組化。由於歷史問題,老版的JS並沒有自己模組管理方案,所以社區提出了很多模組管理方案,比如ES2015
的import
,CommonJS
的require
,另外還有AMD
,CMD
等等。就目前我見到的情況來說,import
因為已經成為ES2015
標準,所以在客戶端廣泛使用,而require
是Node.js
的自帶模組管理機制,也有很廣泛的用途,而AMD
和CMD
的使用已經很少見了。
但是webpack
作為一個開放的模組化工具,他是支援ES6
,CommonJS
和AMD
等多種標準的,不同的模組化標準有不同的解析方法,本文只會講ES6
標準的import
方案,這也是客戶端JS使用最多的方案。
簡單例子
按照業界慣例,我也用hello world
作為一個簡單的例子,但是我將這句話拆成了幾部分,放到了不同的文件裡面。
先來建一個hello.js
,只導出一個簡單的字元串:
const hello = 'hello';
export default hello;
然後再來一個helloWorld.js
,將hello
和world
拼成一句話,並導出拼接的這個方法:
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());
現在如果你直接在html
裡面引用index.js
是不能運行成功的,因為大部分瀏覽器都不支援import
這種模組導入。而webpack
就是來解決這個問題的,它會將我們模組化的程式碼轉換成瀏覽器認識的普通JS來執行。
引入webpack
我們印象中webpack
的配置很多,很麻煩,但那是因為我們需要開啟的功能很多,如果只是解析轉換import
,配置起來非常簡單。
-
先把依賴裝上吧,這沒什麼好說的:
// package.json { "devDependencies": { "webpack": "^5.4.0", "webpack-cli": "^4.2.0" }, }
-
為了使用方便,再加個
build
腳本吧:// package.json { "scripts": { "build": "webpack" }, }
-
最後再簡單寫下
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
這個文件裡面。這個配置文件上還有兩個配置
mode
和devtool
只是我用來方便調試編譯後的程式碼的,mode
指定用哪種模式編譯,默認是production
,會對程式碼進行壓縮和混淆,不好讀,所以我設置為development
;而devtool
是用來控制生成哪種粒度的source map
,簡單來說,想要更好調試,就要更好的,更清晰的source map
,但是編譯速度變慢;反之,想要編譯速度快,就要選擇粒度更粗,更不好讀的source map
,webpack
提供了很多可供選擇的source map
,具體的可以看他的文檔。 -
然後就可以在
dist
下面建個index.html
來引用編譯後的程式碼了:// index.html <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> </head> <body> <script src="main.js"></script> </body> </html>
-
運行下
yarn build
就會編譯我們的程式碼,然後打開index.html
就可以看到效果了。
深入原理
前面講的這個例子很簡單,一般也滿足不了我們實際工程中的需求,但是對於我們理解原理卻是一個很好的突破口,畢竟webpack
這麼龐大的一個體系,我們也不能一口吃個胖子,得一點一點來。
webpack把程式碼編譯成了啥?
為了弄懂他的原理,我們可以直接從編譯後的程式碼入手,先看看他長啥樣子,有的朋友可能一提到去看源碼,心理就沒底,其實我以前也是這樣的。但是完全沒有必要懼怕,他編譯後的程式碼瀏覽器能夠執行,那肯定就是普通的JS程式碼,不會藏著這麼黑科技。
下面是編譯完的程式碼截圖:
雖然我們只有三個簡單的JS文件,但是加上webpack
自己的邏輯,編譯後的文件還是有一百多行程式碼,所以即使我把具體邏輯摺疊起來了,這個截圖還是有點長,為了能夠看清楚他的結構,我將它分成了4個部分,標記在了截圖上,下面我們分別來看看這幾個部分吧。
-
第一部分其實就是一個對象
__webpack_modules__
,這個對象裡面有三個屬性,屬性名字是我們三個模組的文件路徑,屬性的值是一個函數,我們隨便展開一個./src/helloWorld.js
看下:我們發現這個程式碼內容跟我們自己寫的
helloWorld.js
非常像:他只是在我們的程式碼前先調用了
__webpack_require__.r
和__webpack_require__.d
,這兩個輔助函數我們在後面會看到。然後對我們的程式碼進行了一點修改,將我們的
import
關鍵字改成了__webpack_require__
函數,並用一個變數_hello__WEBPACK_IMPORTED_MODULE_0__
來接收了import
進來的內容,後面引用的地方也改成了這個,其他跟這個無關的程式碼,比如const world = 'world';
還是保持原樣的。這個
__webpack_modules__
對象存了所有的模組程式碼,其實對於模組程式碼的保存,在不同版本的webpack
裡面實現的方式並不一樣,我這個版本是5.4.0
,在4.x
的版本裡面好像是作為數組存下來,然後在最外層的立即執行函數裡面以參數的形式傳進來的。但是不管是哪種方式,都只是轉換然後保存一下模組程式碼而已。 -
第二塊程式碼的核心是
__webpack_require__
,這個程式碼展開,瞬間給了我一種熟悉感:來看一下這個流程吧:
- 先定義一個變數
__webpack_module_cache__
作為載入了的模組的快取 __webpack_require__
其實就是用來載入模組的- 載入模組時,先檢查快取中有沒有,如果有,就直接返回快取
- 如果快取沒有,就從
__webpack_modules__
將對應的模組取出來執行 __webpack_modules__
就是上面第一塊程式碼里的那個對象,取出的模組其實就是我們自己寫的程式碼,取出執行的也是我們每個模組的程式碼- 每個模組執行除了執行我們的邏輯外,還會將
export
的內容添加到module.exports
上,這就是前面說的__webpack_require__.d
輔助方法的作用。添加到module.exports
上其實就是添加到了__webpack_module_cache__
快取上,後面再引用這個模組就直接從快取拿了。
這個流程我太熟悉了,因為他簡直跟
Node.js
的CommonJS
實現思路一模一樣,具體的可以看我之前寫的這篇文章:深入Node.js的模組載入機制,手寫require函數。 - 先定義一個變數
-
第三塊程式碼其實就是我們前面看到過的幾個輔助函數的定義,具體幹啥的,其實他的注釋已經寫了:
-
__webpack_require__.d
:核心其實是Object.defineProperty
,主要是用來將我們模組導出的內容添加到全局的__webpack_module_cache__
快取上。 -
__webpack_require__.o
:其實就是Object.prototype.hasOwnProperty
的一個簡寫而已。 -
__webpack_require__.r
:這個方法就是給每個模組添加一個屬性__esModule
,來表明他是一個ES6
的模組。 -
第四塊就一行程式碼,調用
__webpack_require__
載入入口模組,啟動執行。
-
這樣我們將程式碼分成了4塊,每塊的作用都搞清楚,其實webpack乾的事情就清晰了:
- 將
import
這種瀏覽器不認識的關鍵字替換成了__webpack_require__
函數調用。 __webpack_require__
在實現時採用了類似CommonJS
的模組思想。- 一個文件就是一個模組,對應模組快取上的一個對象。
- 當模組程式碼執行時,會將
export
的內容添加到這個模組對象上。 - 當再次引用一個以前引用過的模組時,會直接從快取上讀取模組。
自己實現一個webpack
現在webpack到底幹了什麼事情我們已經清楚了,接下來我們就可以自己動手實現一個了。根據前面最終生成的程式碼結果,我們要實現的程式碼其實主要分兩塊:
- 遍歷所有模組,將每個模組程式碼讀取出來,替換掉
import
和export
關鍵字,放到__webpack_modules__
對象上。 - 整個程式碼裡面除了
__webpack_modules__
和最後啟動的入口是變化的,其他程式碼,像__webpack_require__
,__webpack_require__.r
這些方法其實都是固定的,整個程式碼結構也是固定的,所以完全可以先定義好一個模板。
使用AST解析程式碼
由於我們需要將import
這種程式碼轉換成瀏覽器能識別的普通JS程式碼,所以我們首先要能夠將程式碼解析出來。在解析程式碼的時候,可以將它讀出來當成字元串替換,也可以使用更專業的AST
來解析。AST
全稱叫Abstract Syntax Trees
,也就是抽象語法樹
,是一個將程式碼用樹來表示的數據結構,一個程式碼可以轉換成AST
,AST
又可以轉換成程式碼,而我們熟知的babel
其實就可以做這個工作。要生成AST
很複雜,涉及到編譯原理,但是如果僅僅拿來用就比較簡單了,本文就先不涉及複雜的編譯原理,而是直接將babel
生成好的AST
拿來使用。
注意: webpack源碼解析AST並不是使用的babel
,而是使用的acorn,webpack繼承acorn
的Parser
,自己實現了一個JavascriptParser,本文寫作時採用了babel
,這也是一個大家更熟悉的工具。
比如我先將入口文件讀出來,然後用babel
轉換成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
列印在控制台:
這雖然是一個完整的AST
,但是看起來並不清晰,關鍵數據其實是body
欄位,這裡的body
也只是展示了類型名字。所以照著這個寫程式碼其實不好寫,這裡推薦一個在線工具//astexplorer.net/,可以很清楚的看到每個節點的內容:
從這個解析出來的AST
我們可以看到,body
主要有4塊程式碼:
ImportDeclaration
:就是第一行的import
定義VariableDeclaration
:第三行的一個變數申明FunctionDeclaration
:第五行的一個函數定義ExpressionStatement
:第十三行的一個普通語句
你如果把每個節點展開,會發現他們下面又嵌套了很多其他節點,比如第三行的VariableDeclaration
展開後,其實還有個函數調用helloWorld()
:
使用traverse
遍歷AST
對於這樣一個生成好的AST
,我們可以使用@babel/traverse
來對他進行遍歷和操作,比如我想拿到ImportDeclaration
進行操作,就直接這樣寫:
// 使用babel traverse來遍歷ast上的節點
traverse(ast, {
ImportDeclaration(path) {
console.log(path.node);
},
});
上面程式碼可以拿到所有的import
語句:
將import
轉換為函數調用
前面我們說了,我們的目標是將ES6的import
:
import helloWorld from "./helloWorld";
轉換成普通瀏覽器能識別的函數調用:
var _helloWorld__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/helloWorld.js");
為了實現這個功能,我們還需要引入@babel/types
,這個庫可以幫我們創建新的AST
節點,所以這個轉換程式碼寫出來就是這樣:
const t = require("@babel/types");
// 使用babel traverse來遍歷ast上的節點
traverse(ast, {
ImportDeclaration(p) {
// 獲取被import的文件
const importFile = p.node.source.value;
// 獲取文件路徑
let importFilePath = path.join(path.dirname(config.entry), importFile);
importFilePath = `./${importFilePath}.js`;
// 構建一個變數定義的AST節點
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),
])
),
]);
// 將當前節點替換為變數定義節點
p.replaceWith(variableDeclaration);
},
});
上面這段程式碼我們用了很多@babel/types
下面的API,比如t.variableDeclaration
,t.variableDeclarator
,這些都是用來創建對應的節點的,具體的API可以看這裡。注意這個程式碼裡面我有很多寫死的地方,比如importFilePath
生成邏輯,還應該處理多種後綴名的,還有最終生成的變數名_${path.basename(importFile)}__WEBPACK_IMPORTED_MODULE_0__
,最後的數字我也是直接寫了0
,按理來說應該是根據不同的import
順序來生成的,但是本文主要講webpack
的原理,這些細節上我就沒花過多時間了。
上面的程式碼其實是修改了我們的AST
,修改後的AST
可以用@babel/generator
又轉換為程式碼:
const generate = require('@babel/generator').default;
const newCode = generate(ast).code;
console.log(newCode);
這個列印結果是:
可以看到這個結果裡面import helloWorld from "./helloWorld";
已經被轉換為var __helloWorld__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/helloWorld.js");
。
替換import
進來的變數
前面我們將import
語句替換成了一個變數定義,變數名字也改為了__helloWorld__WEBPACK_IMPORTED_MODULE_0__
,自然要將調用的地方也改了。為了更好的管理,我們將AST
遍歷,操作以及最後的生成新程式碼都封裝成一個函數吧。
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上的節點
traverse(ast, {
ImportDeclaration(p) {
// 跟之前一樣的
},
});
const newCode = generate(ast).code;
// 返回一個包含必要資訊的新對象
return {
file,
dependcies: [importFilePath],
code: newCode,
};
}
然後啟動執行的時候就可以調這個函數了
parseFile(config.entry);
拿到的結果跟之前的差不多:
好了,現在需要將使用import
的地方也替換了,因為我們已經知道了這個地方是將它作為函數調用的,也就是要將
const helloWorldStr = helloWorld();
轉為這個樣子:
const helloWorldStr = (0,_helloWorld__WEBPACK_IMPORTED_MODULE_0__.default)();
這行程式碼的效果其實跟_helloWorld__WEBPACK_IMPORTED_MODULE_0__.default()
是一樣的,為啥在前麵包個(0, )
,我也不知道,有知道的大佬告訴下我唄。
所以我們在traverse
裡面加一個CallExpression
:
traverse(ast, {
ImportDeclaration(p) {
// 跟前面的差不多,省略了
},
CallExpression(p) {
// 如果調用的是import進來的函數
if (p.node.callee.name === importVarName) {
// 就將它替換為轉換後的函數名字
p.node.callee.name = `${importCovertVarName}.default`;
}
},
});
這樣轉換後,我們再重新生成一下程式碼,已經像那麼個樣子了:
遞歸解析多個文件
現在我們有了一個parseFile
方法來解析處理入口文件,但是我們的文件其實不止一個,我們應該依據模組的依賴關係,遞歸的將所有的模組都解析了。要實現遞歸解析也不複雜,因為前面的parseFile
的依賴dependcies
已經返回了:
- 我們創建一個數組存放文件的解析結果,初始狀態下他只有入口文件的解析結果
- 根據入口文件的解析結果,可以拿到入口文件的依賴
- 解析所有的依賴,將結果繼續加到解析結果數組裡面
- 一直循環這個解析結果數組,將裡面的依賴文件解析完
- 最後將解析結果數組返回就行
寫成程式碼就是這樣:
function parseFiles(entryFile) {
const entryRes = parseFile(entryFile); // 解析入口文件
const results = [entryRes]; // 將解析結果放入一個數組
// 循環結果數組,將它的依賴全部拿出來解析
for (const res of results) {
const dependencies = res.dependencies;
dependencies.map((dependency) => {
if (dependency) {
const ast = parseFile(dependency);
results.push(ast);
}
});
}
return results;
}
然後就可以調用這個方法解析所有文件了:
const allAst = parseFiles(config.entry);
console.log(allAst);
看看解析結果吧:
這個結果其實跟我們最終需要生成的__webpack_modules__
已經很像了,但是還有兩塊沒有處理:
-
一個是
import
進來的內容作為變數使用,比如import hello from './hello'; const world = 'world'; const helloWorld = () => `${hello} ${world}`;
-
另一個就是
export
語句還沒處理
替換import
進來的變數(作為變數調用)
前面我們已經用CallExpression
處理過作為函數使用的import
變數了,現在要處理作為變數使用的其實用Identifier
處理下就行了,處理邏輯跟之前的CallExpression
差不多:
traverse(ast, {
ImportDeclaration(p) {
// 跟以前一樣的
},
CallExpression(p) {
// 跟以前一樣的
},
Identifier(p) {
// 如果調用的是import進來的變數
if (p.node.name === importVarName) {
// 就將它替換為轉換後的變數名字
p.node.name = `${importCovertVarName}.default`;
}
},
});
現在再運行下,import
進來的變數名字已經變掉了:
替換export
語句
從我們需要生成的結果來看,export
需要進行兩個處理:
- 如果一個文件有
export default
,需要添加一個__webpack_require__.d
的輔助方法調用,內容都是固定的,加上就行。 - 將
export
語句轉換為普通的變數定義。
對應生成結果上的這兩個:
要處理export
語句,在遍歷ast
的時候添加ExportDefaultDeclaration
就行了:
traverse(ast, {
ImportDeclaration(p) {
// 跟以前一樣的
},
CallExpression(p) {
// 跟以前一樣的
},
Identifier(p) {
// 跟以前一樣的
},
ExportDefaultDeclaration(p) {
hasExport = true; // 先標記是否有export
// 跟前面import類似的,創建一個變數定義節點
const variableDeclaration = t.variableDeclaration("const", [
t.variableDeclarator(
t.identifier("__WEBPACK_DEFAULT_EXPORT__"),
t.identifier(p.node.declaration.name)
),
]);
// 將當前節點替換為變數定義節點
p.replaceWith(variableDeclaration);
},
});
然後再運行下就可以看到export
語句被替換了:
然後就是根據hasExport
變數判斷在AST
轉換為程式碼的時候要不要加__webpack_require__.d
輔助函數:
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
的調用添上吧
前面說了,最終生成的程式碼,每個模組前面都有個__webpack_require__.r
的調用
這個只是拿來給模組添加一個__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}`;
}
再運行下看看,這個程式碼也加上了:
創建程式碼模板
到現在,最難的一塊,模組程式碼的解析和轉換我們其實已經完成了。下面要做的工作就比較簡單了,因為最終生成的程式碼裡面,各種輔助方法都是固定的,動態的部分就是前面解析的模組和入口文件。所以我們可以創建一個這樣的模板,將動態的部分標記出來就行,其他不變的部分寫死。這個模板文件的處理,你可以將它讀進來作為字元串處理,也可以用模板引擎,我這裡採用ejs
模板引擎:
// 模板文件,直接從webpack生成結果抄過來,改改就行
/******/ (() => { // 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__
來生成最終的__webpack_modules__
- 模板裡面用
__TO_REPLACE_WEBPACK_ENTRY__
來替代動態的入口文件 webpack
程式碼裡面使用前面生成好的AST
數組來替換模板的__TO_REPLACE_WEBPACK_MODULES__
webpack
程式碼裡面使用前面拿到的入口文件來替代模板的__TO_REPLACE_WEBPACK_ENTRY__
- 使用
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
看看效果了:
總結
本文使用簡單質樸的方式講述了webpack
的基本原理,並自己手寫實現了一個基本的支援import
和export
的default
的webpack
。
本文可運行程式碼已經上傳GitHub,大家可以拿下來玩玩://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Engineering/mini-webpack
下面再就本文的要點進行下總結:
webpack
最基本的功能其實是將JS
的高級模組化語句,import
和require
之類的轉換為瀏覽器能認識的普通函數調用語句。- 要進行語言程式碼的轉換,我們需要對程式碼進行解析。
- 常用的解析手段是
AST
,也就是將程式碼轉換為抽象語法樹
。 AST
是一個描述程式碼結構的樹形數據結構,程式碼可以轉換為AST
,AST
也可以轉換為程式碼。babel
可以將程式碼轉換為AST
,但是webpack
官方並沒有使用babel
,而是基於acorn自己實現了一個JavascriptParser。- 本文從
webpack
構建的結果入手,也使用AST
自己生成了一個類似的程式碼。 webpack
最終生成的程式碼其實分為動態和固定的兩部分,我們將固定的部分寫入一個模板,動態的部分在模板裡面使用ejs
佔位。- 生成程式碼動態部分需要藉助
babel
來生成AST
,並對其進行修改,最後再使用babel
將其生成新的程式碼。 - 在生成
AST
時,我們從配置的入口文件開始,遞歸的解析所有文件。即解析入口文件的時候,將它的依賴記錄下來,入口文件解析完後就去解析他的依賴文件,在解析他的依賴文件時,將依賴的依賴也記錄下來,後面繼續解析。重複這種步驟,直到所有依賴解析完。 - 動態程式碼生成好後,使用
ejs
將其寫入模板,以生成最終的程式碼。 - 如果要支援
require
或者AMD
,其實思路是類似的,最終生成的程式碼也是差不多的,主要的差別在AST
解析那一塊。
參考資料
文章的最後,感謝你花費寶貴的時間閱讀本文,如果本文給了你一點點幫助或者啟發,請不要吝嗇你的贊和GitHub小星星,你的支援是作者持續創作的動力。
歡迎關注我的公眾號進擊的大前端第一時間獲取高品質原創~
「前端進階知識」系列文章://juejin.im/post/5e3ffc85518825494e2772fd
「前端進階知識」系列文章源碼GitHub地址: //github.com/dennis-jiang/Front-End-Knowledges