本文開始我會圍繞webpack
和babel
寫一系列的工程化文章,這兩個工具我雖然每天用,可是對他們的原理理解的其實不是很深刻,寫這些文章的過程其實也是我深刻學習的過程。因爲webpack
和babel
的體系太大,知識點衆多,不可能一篇文章囊括全部知識點,目前個人計劃是從簡單入手,先實現一個最簡單的能夠運行的webpack
,而後再看看plugin
, loader
和tree shaking
等功能。目前我計劃會有這些文章:javascript
webpack
,也就是本文webpack
的plugin
實現原理webpack
的loader
實現原理webpack
的tree shaking
實現原理webpack
的HMR
實現原理babel
和ast
原理全部文章都是原理或者源碼解析,歡迎關注~html
本文可運行代碼已經上傳GitHub,你們能夠拿下來玩玩:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Engineering/mini-webpack前端
注意:本文主要講webpack
原理,在實現時並不嚴謹,並且只處理了import
和export
的default
狀況,若是你想在生產環境使用,請本身添加其餘狀況的處理和邊界判斷。java
筆者剛開始作前端時,其實不知道什麼webpack
,也不懂模塊化,都是html
裏面直接寫script
,引入jquery
直接幹。因此若是一個頁面的JS須要依賴jquery
和lodash
,那html
可能就長這樣:node
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <script src="https://unpkg.com/jquery@3.5.1"></script> <script src="https://unpkg.com/lodash@4.17.20"></script> <script src="./src/index.js"></script> </head> <body> </body> </html>
這樣寫會致使幾個問題:jquery
index.js
不能清晰的找到他到底依賴哪些外部庫script
的順序必須寫正確,若是錯了就會致使找不到依賴,直接報錯window
上注入變量來暴露給外部script
標籤來下載代碼,有些沒用到的代碼也會下載下來webpack
的一個最基本的功能就是來解決上述的狀況,容許在JS裏面經過import
或者require
等關鍵字來顯式申明依賴,能夠引用第三方庫,本身的JS代碼間也能夠相互引用,這樣在實質上就實現了前端代碼的模塊化。因爲歷史問題,老版的JS並無本身模塊管理方案,因此社區提出了不少模塊管理方案,好比ES2015
的import
,CommonJS
的require
,另外還有AMD
,CMD
等等。就目前我見到的狀況來講,import
由於已經成爲ES2015
標準,因此在客戶端普遍使用,而require
是Node.js
的自帶模塊管理機制,也有很普遍的用途,而AMD
和CMD
的使用已經不多見了。webpack
可是webpack
做爲一個開放的模塊化工具,他是支持ES6
,CommonJS
和AMD
等多種標準的,不一樣的模塊化標準有不一樣的解析方法,本文只會講ES6
標準的import
方案,這也是客戶端JS使用最多的方案。git
按照業界慣例,我也用hello world
做爲一個簡單的例子,可是我將這句話拆成了幾部分,放到了不一樣的文件裏面。github
先來建一個hello.js
,只導出一個簡單的字符串:web
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
的配置不少,很麻煩,但那是由於咱們須要開啓的功能不少,若是隻是解析轉換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
這麼龐大的一個體系,咱們也不能一口吃個胖子,得一點一點來。
爲了弄懂他的原理,咱們能夠直接從編譯後的代碼入手,先看看他長啥樣子,有的朋友可能一提到去看源碼,心理就沒底,其實我之前也是這樣的。可是徹底沒有必要害怕,他編譯後的代碼瀏覽器可以執行,那確定就是普通的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到底幹了什麼事情咱們已經清楚了,接下來咱們就能夠本身動手實現一個了。根據前面最終生成的代碼結果,咱們要實現的代碼其實主要分兩塊:
import
和export
關鍵字,放到__webpack_modules__
對象上。__webpack_modules__
和最後啓動的入口是變化的,其餘代碼,像__webpack_require__
,__webpack_require__.r
這些方法其實都是固定的,整個代碼結構也是固定的,因此徹底能夠先定義好一個模板。因爲咱們須要將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
也只是展現了類型名字。因此照着這個寫代碼其實很差寫,這裏推薦一個在線工具https://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,你們能夠拿下來玩玩:https://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小星星,你的支持是做者持續創做的動力。
歡迎關注個人公衆號進擊的大前端第一時間獲取高質量原創~
「前端進階知識」系列文章源碼地址: https://github.com/dennis-jiang/Front-End-Knowledges