時(shí)間:2023-06-11 15:06:02 | 來源:網(wǎng)站運(yùn)營
時(shí)間:2023-06-11 15:06:02 來源:網(wǎng)站運(yùn)營
手撕webpack:hello world
作為一個(gè)簡單的例子,但是我將這句話拆成了幾部分,放到了不同的文件里面。hello.js
,只導(dǎo)出一個(gè)簡單的字符串:const hello = 'hello';export default hello;
然后再來一個(gè)helloWorld.js
,將hello
和world
拼成一句話,并導(dǎo)出拼接的這個(gè)方法:import hello from './hello';const world = 'world';const helloWorld = () => `${hello} ${world}`;export default helloWorld;
最后再來個(gè)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
是不能運(yùn)行成功的,因?yàn)榇蟛糠譃g覽器都不支持import
這種模塊導(dǎo)入。而webpack
就是來解決這個(gè)問題的,它會將我們模塊化的代碼轉(zhuǎn)換成瀏覽器認(rèn)識的普通JS來執(zhí)行。webpack
這么龐大的一個(gè)體系,我們也不能一口吃個(gè)胖子,得一點(diǎn)一點(diǎn)來。webpack
自己的邏輯,編譯后的文件還是有一百多行代碼,所以即使我把具體邏輯折疊起來了,這個(gè)截圖還是有點(diǎn)長,為了能夠看清楚他的結(jié)構(gòu),我將它分成了4個(gè)部分,標(biāo)記在了截圖上,下面我們分別來看看這幾個(gè)部分吧。__webpack_modules__
,這個(gè)對象里面有三個(gè)屬性,屬性名字是我們?nèi)齻€(gè)模塊的文件路徑,屬性的值是一個(gè)函數(shù),我們隨便展開一個(gè)./src/helloWorld.js
看下:helloWorld.js
非常像:__webpack_require__.r
和__webpack_require__.d
,這兩個(gè)輔助函數(shù)我們在后面會看到。import
關(guān)鍵字改成了__webpack_require__
函數(shù),并用一個(gè)變量_hello__WEBPACK_IMPORTED_MODULE_0__
來接收了import
進(jìn)來的內(nèi)容,后面引用的地方也改成了這個(gè),其他跟這個(gè)無關(guān)的代碼,比如const world = 'world';
還是保持原樣的。__webpack_modules__
對象存了所有的模塊代碼,其實(shí)對于模塊代碼的保存,在不同版本的webpack
里面實(shí)現(xiàn)的方式并不一樣,我這個(gè)版本是5.4.0
,在4.x
的版本里面好像是作為數(shù)組存下來,然后在最外層的立即執(zhí)行函數(shù)里面以參數(shù)的形式傳進(jìn)來的。但是不管是哪種方式,都只是轉(zhuǎn)換然后保存一下模塊代碼而已。__webpack_require__
,:__webpack_module_cache__
作為加載了的模塊的緩存__webpack_require__
其實(shí)就是用來加載模塊的__webpack_modules__
將對應(yīng)的模塊取出來執(zhí)行__webpack_modules__
就是上面第一塊代碼里的那個(gè)對象,取出的模塊其實(shí)就是我們自己寫的代碼,取出執(zhí)行的也是我們每個(gè)模塊的代碼export
的內(nèi)容添加到module.exports
上,這就是前面說的__webpack_require__.d
輔助方法的作用。添加到module.exports
上其實(shí)就是添加到了__webpack_module_cache__
緩存上,后面再引用這個(gè)模塊就直接從緩存拿了。__webpack_require__.d
:核心其實(shí)是Object.defineProperty
,主要是用來將我們模塊導(dǎo)出的內(nèi)容添加到全局的__webpack_module_cache__
緩存上。__webpack_require__.o
:其實(shí)就是Object.prototype.hasOwnProperty
的一個(gè)簡寫而已。__webpack_require__.r
:這個(gè)方法就是給每個(gè)模塊添加一個(gè)屬性__esModule
,來表明他是一個(gè)ES6
的模塊。__webpack_require__
加載入口模塊,啟動(dòng)執(zhí)行。import
這種瀏覽器不認(rèn)識的關(guān)鍵字替換成了__webpack_require__
函數(shù)調(diào)用。__webpack_require__
在實(shí)現(xiàn)時(shí)采用了類似CommonJS
的模塊思想。export
的內(nèi)容添加到這個(gè)模塊對象上。import
和export
關(guān)鍵字,放到__webpack_modules__
對象上。__webpack_modules__
和最后啟動(dòng)的入口是變化的,其他代碼,像__webpack_require__
,__webpack_require__.r
這些方法其實(shí)都是固定的,整個(gè)代碼結(jié)構(gòu)也是固定的,所以完全可以先定義好一個(gè)模板。import
這種代碼轉(zhuǎn)換成瀏覽器能識別的普通JS代碼,所以我們首先要能夠?qū)⒋a解析出來。在解析代碼的時(shí)候,可以將它讀出來當(dāng)成字符串替換,也可以使用更專業(yè)的AST
來解析。AST
全稱叫Abstract Syntax Trees
,也就是抽象語法樹
,是一個(gè)將代碼用樹來表示的數(shù)據(jù)結(jié)構(gòu),一個(gè)代碼可以轉(zhuǎn)換成AST
,AST
又可以轉(zhuǎn)換成代碼,而我們熟知的babel
其實(shí)就可以做這個(gè)工作。要生成AST
很復(fù)雜,涉及到編譯原理,但是如果僅僅拿來用就比較簡單了,本文就先不涉及復(fù)雜的編譯原理,而是直接將babel
生成好的AST
拿來使用。babel
,而是使用的acorn。webpack
自己實(shí)現(xiàn)了一個(gè)JavascriptParser類,這個(gè)類里面用到了acorn
。本文寫作時(shí)采用了babel
,這也是一個(gè)大家更熟悉的工具。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解析ASTconst ast = parser.parse(fileContent, { sourceType: "module" });console.log(ast); // 把a(bǔ)st打印出來看看
上面代碼可以將生成好的ast
打印在控制臺:AST
,但是看起來并不清晰,關(guān)鍵數(shù)據(jù)其實(shí)是body
字段,這里的body
也只是展示了類型名字。所以照著這個(gè)寫代碼其實(shí)不好寫,這里推薦一個(gè)在線工具https://astexplorer.net/,可以很清楚的看到每個(gè)節(jié)點(diǎn)的內(nèi)容:AST
我們可以看到,body
主要有4塊代碼:ImportDeclaration
:就是第一行的import
定義VariableDeclaration
:第三行的一個(gè)變量申明FunctionDeclaration
:第五行的一個(gè)函數(shù)定義ExpressionStatement
:第十三行的一個(gè)普通語句VariableDeclaration
展開后,其實(shí)還有個(gè)函數(shù)調(diào)用helloWorld()
:traverse
遍歷AST
AST
,我們可以使用@babel/traverse
來對他進(jìn)行遍歷和操作,比如我想拿到ImportDeclaration
進(jìn)行操作,就直接這樣寫:// 使用babel traverse來遍歷ast上的節(jié)點(diǎn)traverse(ast, { ImportDeclaration(path) { console.log(path.node); },});
上面代碼可以拿到所有的import
語句:import
轉(zhuǎn)換為函數(shù)調(diào)用import
:import helloWorld from "./helloWorld";
轉(zhuǎn)換成普通瀏覽器能識別的函數(shù)調(diào)用:var _helloWorld__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/helloWorld.js");
為了實(shí)現(xiàn)這個(gè)功能,我們還需要引入@babel/types
,這個(gè)庫可以幫我們創(chuàng)建新的AST
節(jié)點(diǎn),所以這個(gè)轉(zhuǎn)換代碼寫出來就是這樣:const t = require("@babel/types");// 使用babel traverse來遍歷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
,這些都是用來創(chuàng)建對應(yīng)的節(jié)點(diǎn)的,具體的API可以看這里。注意這個(gè)代碼里面我有很多寫死的地方,比如importFilePath
生成邏輯,還應(yīng)該處理多種后綴名的,還有最終生成的變量名_${path.basename(importFile)}__WEBPACK_IMPORTED_MODULE_0__
,最后的數(shù)字我也是直接寫了0
,按理來說應(yīng)該是根據(jù)不同的import
順序來生成的,但是本文主要講webpack
的原理,這些細(xì)節(jié)上我就沒花過多時(shí)間了。AST
,修改后的AST
可以用@babel/generator
又轉(zhuǎn)換為代碼:const generate = require('@babel/generator').default;const newCode = generate(ast).code;console.log(newCode);
這個(gè)打印結(jié)果是:import helloWorld from "./helloWorld";
已經(jīng)被轉(zhuǎn)換為var __helloWorld__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/helloWorld.js");
。import
進(jìn)來的變量import
語句替換成了一個(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來遍歷ast上的節(jié)點(diǎn) traverse(ast, { ImportDeclaration(p) { // 跟之前一樣的 }, }); const newCode = generate(ast).code; // 返回一個(gè)包含必要信息的新對象 return { file, dependcies: [importFilePath], code: newCode, };}
然后啟動(dòng)執(zhí)行的時(shí)候就可以調(diào)這個(gè)函數(shù)了parseFile(config.entry);
拿到的結(jié)果跟之前的差不多: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, )
,我也不知道,有知道的大佬告訴下我唄。traverse
里面加一個(gè)CallExpression
:traverse(ast, { ImportDeclaration(p) { // 跟前面的差不多,省略了 }, CallExpression(p) { // 如果調(diào)用的是import進(jìn)來的函數(shù) if (p.node.callee.name === importVarName) { // 就將它替換為轉(zhuǎn)換后的函數(shù)名字 p.node.callee.name = `${importCovertVarName}.default`; } }, });
這樣轉(zhuǎn)換后,我們再重新生成一下代碼,已經(jīng)像那么個(gè)樣子了:parseFile
方法來解析處理入口文件,但是我們的文件其實(shí)不止一個(gè),我們應(yīng)該依據(jù)模塊的依賴關(guān)系,遞歸的將所有的模塊都解析了。要實(shí)現(xiàn)遞歸解析也不復(fù)雜,因?yàn)榍懊娴?code>parseFile的依賴dependcies
已經(jīng)返回了:function parseFiles(entryFile) { const entryRes = parseFile(entryFile); // 解析入口文件 const results = [entryRes]; // 將解析結(jié)果放入一個(gè)數(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)用這個(gè)方法解析所有文件了:const allAst = parseFiles(config.entry);console.log(allAst);
看看解析結(jié)果吧:__webpack_modules__
已經(jīng)很像了,但是還有兩塊沒有處理:import
進(jìn)來的內(nèi)容作為變量使用,比如export
語句還沒處理import
進(jìn)來的變量(作為變量調(diào)用)CallExpression
處理過作為函數(shù)使用的import
變量了,現(xiàn)在要處理作為變量使用的其實(shí)用Identifier
處理下就行了,處理邏輯跟之前的CallExpression
差不多:traverse(ast, { ImportDeclaration(p) { // 跟以前一樣的 }, CallExpression(p) { // 跟以前一樣的 }, Identifier(p) { // 如果調(diào)用的是import進(jìn)來的變量 if (p.node.name === importVarName) { // 就將它替換為轉(zhuǎn)換后的變量名字 p.node.name = `${importCovertVarName}.default`; } }, });
現(xiàn)在再運(yùn)行下,import
進(jìn)來的變量名字已經(jīng)變掉了:export
語句export
需要進(jìn)行兩個(gè)處理:export default
,需要添加一個(gè)__webpack_require__.d
的輔助方法調(diào)用,內(nèi)容都是固定的,加上就行。export
語句轉(zhuǎn)換為普通的變量定義。export
語句,在遍歷ast
的時(shí)候添加ExportDefaultDeclaration
就行了:traverse(ast, { ImportDeclaration(p) { // 跟以前一樣的 }, CallExpression(p) { // 跟以前一樣的 }, Identifier(p) { // 跟以前一樣的 }, ExportDefaultDeclaration(p) { hasExport = true; // 先標(biāo)記是否有export // 跟前面import類似的,創(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
語句被替換了: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)用添上吧__webpack_require__.r
的調(diào)用__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è)代碼也加上了: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
__TO_REPLACE_WEBPACK_MODULES__
來生成最終的__webpack_modules__
__TO_REPLACE_WEBPACK_ENTRY__
來替代動(dòng)態(tài)的入口文件webpack
代碼里面使用前面生成好的AST
數(shù)組來替換模板的__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
來編譯代碼,就可以看到效果了。webpack
的基本原理,并自己手寫實(shí)現(xiàn)了一個(gè)基本的支持import
和export
的default
的webpack
。完整代碼在這里。webpack
最基本的功能其實(shí)是將JS
的高級模塊化語句,import
和require
之類的轉(zhuǎn)換為瀏覽器能認(rèn)識的普通函數(shù)調(diào)用語句。AST
,也就是將代碼轉(zhuǎn)換為抽象語法樹
。AST
是一個(gè)描述代碼結(jié)構(gòu)的樹形數(shù)據(jù)結(jié)構(gòu),代碼可以轉(zhuǎn)換為AST
,AST
也可以轉(zhuǎn)換為代碼。babel
可以將代碼轉(zhuǎn)換為AST
,但是webpack
官方并沒有使用babel
,而是基于acorn自己實(shí)現(xiàn)了一個(gè)JavascriptParser。webpack
構(gòu)建的結(jié)果入手,也使用AST
自己生成了一個(gè)類似的代碼。webpack
最終生成的代碼其實(shí)分為動(dòng)態(tài)和固定的兩部分,我們將固定的部分寫入一個(gè)模板,動(dòng)態(tài)的部分在模板里面使用ejs
占位。babel
來生成AST
,并對其進(jìn)行修改,最后再使用babel
將其生成新的代碼。AST
時(shí),我們從配置的入口文件開始,遞歸的解析所有文件。即解析入口文件的時(shí)候,將它的依賴記錄下來,入口文件解析完后就去解析他的依賴文件,在解析他的依賴文件時(shí),將依賴的依賴也記錄下來,后面繼續(xù)解析。重復(fù)這種步驟,直到所有依賴解析完。ejs
將其寫入模板,以生成最終的代碼。require
或者AMD
,其實(shí)思路是類似的,最終生成的代碼也是差不多的,主要的差別在AST
解析那一塊。關(guān)鍵詞:
客戶&案例
營銷資訊
關(guān)于我們
微信公眾號
版權(quán)所有? 億企邦 1997-2025 保留一切法律許可權(quán)利。