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

const hello = 'hello';export defau" />

国产成人精品无码青草_亚洲国产美女精品久久久久∴_欧美人与鲁交大毛片免费_国产果冻豆传媒麻婆精东

15158846557 在線咨詢 在線咨詢
15158846557 在線咨詢
所在位置: 首頁 > 營銷資訊 > 網(wǎng)站運(yùn)營 > 手撕webpack

手撕webpack

時(shí)間:2023-06-11 15:06:02 | 來源:網(wǎng)站運(yùn)營

時(shí)間:2023-06-11 15:06:02 來源:網(wǎng)站運(yùn)營

手撕webpack:

簡單例子

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

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

const hello = 'hello';export default hello;然后再來一個(gè)helloWorld.js,將helloworld拼成一句話,并導(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í)行。

深入原理

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

webpack把代碼編譯成了啥?

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

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







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

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

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



他只是在我們的代碼前先調(diào)用了__webpack_require__.r__webpack_require__.d,這兩個(gè)輔助函數(shù)我們在后面會看到。
然后對我們的代碼進(jìn)行了一點(diǎn)修改,將我們的import關(guān)鍵字改成了__webpack_require__函數(shù),并用一個(gè)變量_hello__WEBPACK_IMPORTED_MODULE_0__來接收了import進(jìn)來的內(nèi)容,后面引用的地方也改成了這個(gè),其他跟這個(gè)無關(guān)的代碼,比如const world = 'world';還是保持原樣的。
這個(gè)__webpack_modules__對象存了所有的模塊代碼,其實(shí)對于模塊代碼的保存,在不同版本的webpack里面實(shí)現(xiàn)的方式并不一樣,我這個(gè)版本是5.4.0,在4.x的版本里面好像是作為數(shù)組存下來,然后在最外層的立即執(zhí)行函數(shù)里面以參數(shù)的形式傳進(jìn)來的。但是不管是哪種方式,都只是轉(zhuǎn)換然后保存一下模塊代碼而已。

  1. 第二塊代碼的核心是__webpack_require__,:

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


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



    1. __webpack_require__.o:其實(shí)就是Object.prototype.hasOwnProperty的一個(gè)簡寫而已。



    1. __webpack_require__.r:這個(gè)方法就是給每個(gè)模塊添加一個(gè)屬性__esModule,來表明他是一個(gè)ES6的模塊。
    1. 第四塊就一行代碼,調(diào)用__webpack_require__加載入口模塊,啟動(dòng)執(zhí)行。
這樣我們將代碼分成了4塊,每塊的作用都搞清楚,其實(shí)webpack干的事情就清晰了:

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

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

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

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

使用AST解析代碼

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

注意:webpack源碼解析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打印在控制臺:







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







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

  1. ImportDeclaration:就是第一行的import定義
  2. VariableDeclaration:第三行的一個(gè)變量申明
  3. FunctionDeclaration:第五行的一個(gè)函數(shù)定義
  4. ExpressionStatement:第十三行的一個(gè)普通語句
你如果把每個(gè)節(jié)點(diǎn)展開,會發(fā)現(xiàn)他們下面又嵌套了很多其他節(jié)點(diǎn),比如第三行的VariableDeclaration展開后,其實(shí)還有個(gè)函數(shù)調(diào)用helloWorld()







使用traverse遍歷AST

對于這樣一個(gè)生成好的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)用

前面我們說了,我們的目標(biāo)是將ES6的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.variableDeclarationt.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í)間了。

上面的代碼其實(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)來的變量

前面我們將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é)果跟之前的差不多:







好了,現(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, ),我也不知道,有知道的大佬告訴下我唄。

所以我們在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è)樣子了:







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

現(xiàn)在我們有了一個(gè)parseFile方法來解析處理入口文件,但是我們的文件其實(shí)不止一個(gè),我們應(yīng)該依據(jù)模塊的依賴關(guān)系,遞歸的將所有的模塊都解析了。要實(shí)現(xiàn)遞歸解析也不復(fù)雜,因?yàn)榍懊娴?code>parseFile的依賴dependcies已經(jīng)返回了:

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

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







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

  1. 一個(gè)是import進(jìn)來的內(nèi)容作為變量使用,比如
    import hello from './hello'; const world = 'world'; const helloWorld = () => `${hello} ${world}`;
  2. 另一個(gè)就是export語句還沒處理

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

前面我們已經(jīng)用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語句

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

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

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

然后就是根據(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)用添上吧

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

這個(gè)只是拿來給模塊添加一個(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)完成了。下面要做的工作就比較簡單了,因?yàn)樽罱K生成的代碼里面,各種輔助方法都是固定的,動(dòng)態(tài)的部分就是前面解析的模塊和入口文件。所以我們可以創(chuàng)建一個(gè)這樣的模板,將動(dòng)態(tài)的部分標(biāo)記出來就行,其他不變的部分寫死。這個(gè)模板文件的處理,你可以將它讀進(jìn)來作為字符串處理,也可以用模板引擎,我這里采用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

生成最終的代碼

生成最終代碼的思路就是:

  1. 模板里面用__TO_REPLACE_WEBPACK_MODULES__來生成最終的__webpack_modules__
  2. 模板里面用__TO_REPLACE_WEBPACK_ENTRY__來替代動(dòng)態(tài)的入口文件
  3. webpack代碼里面使用前面生成好的AST數(shù)組來替換模板的__TO_REPLACE_WEBPACK_MODULES__
  4. webpack代碼里面使用前面拿到的入口文件來替代模板的__TO_REPLACE_WEBPACK_ENTRY__
  5. 使用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來編譯代碼,就可以看到效果了。







總結(jié)

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

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

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

關(guān)鍵詞:

74
73
25
news

版權(quán)所有? 億企邦 1997-2025 保留一切法律許可權(quán)利。

為了最佳展示效果,本站不支持IE9及以下版本的瀏覽器,建議您使用谷歌Chrome瀏覽器。 點(diǎn)擊下載Chrome瀏覽器
關(guān)閉