時(shí)間:2023-06-04 05:00:01 | 來(lái)源:網(wǎng)站運(yùn)營(yíng)
時(shí)間:2023-06-04 05:00:01 來(lái)源:網(wǎng)站運(yùn)營(yíng)
二十張圖片徹底講明白 Webpack 設(shè)計(jì)理念:Plugin 系統(tǒng)
還是 Loader 系統(tǒng)
,都是建立于這套核心思想之上。所謂萬(wàn)變不離其宗,一通百通。注重實(shí)現(xiàn)思路
,注重設(shè)計(jì)思想
。初始化項(xiàng)目:
npm init //初始化一個(gè)項(xiàng)目yarn add webpack //安裝項(xiàng)目依賴(lài)
安裝完依賴(lài)后,根據(jù)以下目錄結(jié)構(gòu)來(lái)添加對(duì)應(yīng)的目錄和文件:├── node_modules├── package-lock.json├── package.json├── webpack.config.js #配置文件├── debugger.js #測(cè)試文件└── src # 源碼目錄 |── index.js |── name.js └── age.js
webpack.config.jsconst path = require("path");module.exports = { mode: "development", //防止代碼被壓縮 entry: "./src/index.js", //入口文件 output: { path: path.resolve(__dirname, "dist"), filename: "[name].js", }, devtool: "source-map", //防止干擾源文件};
src/index.js(本文不討論CommonJS 和 ES Module之間的引用關(guān)系,以CommonJS為準(zhǔn)
)const name = require("./name");const age = require("./age");console.log("entry文件打印作者信息", name, age);
src/name.jsmodule.exports = "不要禿頭啊";
src/age.jsmodule.exports = "99";
文件依賴(lài)關(guān)系:compiler
對(duì)象中的 run 方法就會(huì)啟動(dòng)編譯。run
方法接受一個(gè)回調(diào),可以用來(lái)查看編譯過(guò)程中的錯(cuò)誤信息或編譯信息。// const { webpack } = require("./webpack.js"); //后面自己手寫(xiě)const { webpack } = require("webpack");const webpackOptions = require("./webpack.config.js");const compiler = webpack(webpackOptions);//開(kāi)始編譯compiler.run((err, stats) => { console.log(err); console.log( stats.toJson({ assets: true, //打印本次編譯產(chǎn)出的資源 chunks: true, //打印本次編譯產(chǎn)出的代碼塊 modules: true, //打印本次編譯產(chǎn)出的模塊 }) );});
執(zhí)行打包命令:node ./debugger.js
得到產(chǎn)出文件 dist/main.js(先暫停三十秒讀一讀下面代碼,命名經(jīng)優(yōu)化):entry文件打印作者信息 不要禿頭啊 99
src/index.js
)被包裹在最后的立即執(zhí)行函數(shù)中,而它所依賴(lài)的模塊(src/name.js
、src/age.js
)則被放進(jìn)了 modules
對(duì)象中(modules
用于存放入口文件的依賴(lài)模塊,key 值為依賴(lài)模塊路徑,value 值為依賴(lài)模塊源代碼
)。require
函數(shù)是 web 環(huán)境下 加載模塊的方法( require
原本是 node環(huán)境 中內(nèi)置的方法,瀏覽器并不認(rèn)識(shí) require
,所以這里需要手動(dòng)實(shí)現(xiàn)一下),它接受模塊的路徑為參數(shù),返回模塊導(dǎo)出的內(nèi)容。核心思想:
webpack.config.js
)找到入口文件(src/index.js
)路徑、源代碼、它所依賴(lài)的模塊
等:var modules = [{ id: "./src/name.js",//路徑 dependencies: [], //所依賴(lài)的模塊 source: 'module.exports = "不要禿頭啊";', //源代碼},{ id: "./src/age.js", dependencies: [], source: 'module.exports = "99";',},{ id: "./src/index.js", dependencies: ["./src/name.js", "./src/age.js"], source: 'const name = require("./src/name.js");/n' + 'const age = require("./src/age.js");/n' + 'console.log("entry文件打印作者信息", name, age);',},];
html、js、css
以外的文件格式,所以我們還需要對(duì)源文件進(jìn)行轉(zhuǎn)換 —— **Loader 系統(tǒng)
**。Plugin 系統(tǒng)
**。compilation
)單獨(dú)解耦出來(lái)。compiler
就像是一個(gè)大管家,它就代表上面說(shuō)的三個(gè)階段,在它上面掛載著各種生命周期函數(shù),而 compilation
就像專(zhuān)管伙食的廚師,專(zhuān)門(mén)負(fù)責(zé)編譯相關(guān)的工作,也就是打包過(guò)程中
這個(gè)階段。畫(huà)個(gè)圖幫助大家理解:Vue
和 React
框架中的生命周期函數(shù),它們就是到了固定的時(shí)間節(jié)點(diǎn)就執(zhí)行對(duì)應(yīng)的生命周期,tapable
做的事情就和這個(gè)差不多,我們可以通過(guò)它先注冊(cè)一系列的生命周期函數(shù),然后在合適的時(shí)間點(diǎn)執(zhí)行。const { SyncHook } = require("tapable"); //這是一個(gè)同步鉤子//第一步:實(shí)例化鉤子函數(shù),可以在這里定義形參const syncHook = new SyncHook(["author", "age"]);//第二步:注冊(cè)事件1syncHook.tap("監(jiān)聽(tīng)器1", (name, age) => { console.log("監(jiān)聽(tīng)器1:", name, age);});//第二步:注冊(cè)事件2syncHook.tap("監(jiān)聽(tīng)器2", (name) => { console.log("監(jiān)聽(tīng)器2", name);});//第三步:注冊(cè)事件3syncHook.tap("監(jiān)聽(tīng)器3", (name) => { console.log("監(jiān)聽(tīng)器3", name);});//第三步:觸發(fā)事件,這里傳的是實(shí)參,會(huì)被每一個(gè)注冊(cè)函數(shù)接收到syncHook.call("不要禿頭啊", "99");
運(yùn)行上面這段代碼,得到結(jié)果:監(jiān)聽(tīng)器1 不要禿頭啊 99監(jiān)聽(tīng)器2 不要禿頭啊監(jiān)聽(tīng)器3 不要禿頭啊
在 Webpack 中,就是通過(guò) tapable
在 comiler
和 compilation
上像這樣掛載著一系列生命周期 Hook
,它就像是一座橋梁,貫穿著整個(gè)構(gòu)建過(guò)程:class Compiler { constructor() { //它內(nèi)部提供了很多鉤子 this.hooks = { run: new SyncHook(), //會(huì)在編譯剛開(kāi)始的時(shí)候觸發(fā)此鉤子 done: new SyncHook(), //會(huì)在編譯結(jié)束的時(shí)候觸發(fā)此鉤子 }; }}
Compiler
對(duì)象Compiler
對(duì)象的 run
方法開(kāi)始執(zhí)行編譯entry
配置項(xiàng)找到所有的入口loader
規(guī)則,對(duì)各模塊進(jìn)行編譯chunk
chunk
轉(zhuǎn)換成一個(gè)一個(gè)文件加入到輸出列表compiler
對(duì)象中的 run 方法就會(huì)啟動(dòng)編譯。run
方法接受一個(gè)回調(diào),可以用來(lái)查看編譯過(guò)程中的錯(cuò)誤信息或編譯信息。+ const webpack = require("./webpack"); //手寫(xiě)webpackconst webpackOptions = require("./webpack.config.js"); //這里一般會(huì)放配置信息const compiler = webpack(webpackOptions);compiler.run((err, stats) => { console.log(err); console.log( stats.toJson({ assets: true, //打印本次編譯產(chǎn)出的資源 chunks: true, //打印本次編譯產(chǎn)出的代碼塊 modules: true, //打印本次編譯產(chǎn)出的模塊 }) );});
搭建結(jié)構(gòu):class Compiler { constructor() {} run(callback) {}}//第一步:搭建結(jié)構(gòu),讀取配置參數(shù),這里接受的是webpack.config.js中的參數(shù)function webpack(webpackOptions) { const compiler = new Compiler() return compiler;}
運(yùn)行流程圖:Compiler
對(duì)象Compiler
它就是整個(gè)打包過(guò)程的大管家,它里面放著各種你可能需要的編譯信息
和生命周期 Hook
,而且是單例模式。//Compiler其實(shí)是一個(gè)類(lèi),它是整個(gè)編譯過(guò)程的大管家,而且是單例模式class Compiler {+ constructor(webpackOptions) {+ this.options = webpackOptions; //存儲(chǔ)配置信息+ //它內(nèi)部提供了很多鉤子+ this.hooks = {+ run: new SyncHook(), //會(huì)在編譯剛開(kāi)始的時(shí)候觸發(fā)此run鉤子+ done: new SyncHook(), //會(huì)在編譯結(jié)束的時(shí)候觸發(fā)此done鉤子+ };+ }}//第一步:搭建結(jié)構(gòu),讀取配置參數(shù),這里接受的是webpack.config.js中的參數(shù)function webpack(webpackOptions) { //第二步:用配置參數(shù)對(duì)象初始化 `Compiler` 對(duì)象+ const compiler = new Compiler(webpackOptions) return compiler;}
運(yùn)行流程圖:Webpack Plugin 其實(shí)就是一個(gè)普通的函數(shù),在該函數(shù)中需要我們定制一個(gè) apply 方法。
當(dāng) Webpack 內(nèi)部進(jìn)行插件掛載時(shí)會(huì)執(zhí)行 apply
函數(shù)。我們可以在 apply
方法中訂閱各種生命周期鉤子,當(dāng)?shù)竭_(dá)對(duì)應(yīng)的時(shí)間點(diǎn)時(shí)就會(huì)執(zhí)行。//自定義插件WebpackRunPluginclass WebpackRunPlugin { apply(compiler) { compiler.hooks.run.tap("WebpackRunPlugin", () => { console.log("開(kāi)始編譯"); }); }}//自定義插件WebpackDonePluginclass WebpackDonePlugin { apply(compiler) { compiler.hooks.done.tap("WebpackDonePlugin", () => { console.log("結(jié)束編譯"); }); }}
webpack.config.js:+ const { WebpackRunPlugin, WebpackDonePlugin } = require("./webpack");module.exports = { //其他省略+ plugins: [new WebpackRunPlugin(), new WebpackDonePlugin()],};
插件定義時(shí)必須要有一個(gè) apply
方法,加載插件其實(shí)執(zhí)行 apply
方法。//第一步:搭建結(jié)構(gòu),讀取配置參數(shù),這里接受的是webpack.config.js中的參數(shù)function webpack(webpackOptions) { //第二步:用配置參數(shù)對(duì)象初始化 `Compiler` 對(duì)象 const compiler = new Compiler(webpackOptions); //第三步:掛載配置文件中的插件+ const { plugins } = webpackOptions;+ for (let plugin of plugins) {+ plugin.apply(compiler);+ } return compiler;}
運(yùn)行流程圖:Compiler
對(duì)象的run
方法開(kāi)始執(zhí)行編譯Compiler
中的 run
鉤子,表示開(kāi)始啟動(dòng)編譯了;在編譯結(jié)束后,需要調(diào)用 done
鉤子,表示編譯完成。//Compiler其實(shí)是一個(gè)類(lèi),它是整個(gè)編譯過(guò)程的大管家,而且是單例模式class Compiler { constructor(webpackOptions) { //省略 } + compile(callback){+ //+ }+ //第四步:執(zhí)行`Compiler`對(duì)象的`run`方法開(kāi)始執(zhí)行編譯+ run(callback) {+ this.hooks.run.call(); //在編譯前觸發(fā)run鉤子執(zhí)行,表示開(kāi)始啟動(dòng)編譯了+ const onCompiled = () => {+ this.hooks.done.call(); //當(dāng)編譯成功后會(huì)觸發(fā)done這個(gè)鉤子執(zhí)行+ };+ this.compile(onCompiled); //開(kāi)始編譯,成功之后調(diào)用onCompiled }}
上面架構(gòu)設(shè)計(jì)中提到過(guò),編譯這個(gè)階段需要單獨(dú)解耦出來(lái),通過(guò) Compilation
來(lái)完成,定義Compilation
大致結(jié)構(gòu):class Compiler { //省略其他 run(callback) { //省略 } compile(callback) { //雖然webpack只有一個(gè)Compiler,但是每次編譯都會(huì)產(chǎn)出一個(gè)新的Compilation, //這里主要是為了考慮到watch模式,它會(huì)在啟動(dòng)時(shí)先編譯一次,然后監(jiān)聽(tīng)文件變化,如果發(fā)生變化會(huì)重新開(kāi)始編譯 //每次編譯都會(huì)產(chǎn)出一個(gè)新的Compilation,代表每次的編譯結(jié)果+ let compilation = new Compilation(this.options);+ compilation.build(callback); //執(zhí)行compilation的build方法進(jìn)行編譯,編譯成功之后執(zhí)行回調(diào) }}+ class Compilation {+ constructor(webpackOptions) {+ this.options = webpackOptions;+ this.modules = []; //本次編譯所有生成出來(lái)的模塊+ this.chunks = []; //本次編譯產(chǎn)出的所有代碼塊,入口模塊和依賴(lài)的模塊打包在一起為代碼塊+ this.assets = {}; //本次編譯產(chǎn)出的資源文件+ this.fileDependencies = []; //本次打包涉及到的文件,這里主要是為了實(shí)現(xiàn)watch模式下監(jiān)聽(tīng)文件的變化,文件發(fā)生變化后會(huì)重新編譯+ }+ build(callback) {+ //這里開(kāi)始做編譯工作,編譯成功執(zhí)行callback+ callback()+ }+ }
運(yùn)行流程圖(點(diǎn)擊可放大):entry
配置項(xiàng)找到所有的入口Compilation
中。class Compilation { constructor(webpackOptions) { this.options = webpackOptions; this.modules = []; //本次編譯所有生成出來(lái)的模塊 this.chunks = []; //本次編譯產(chǎn)出的所有代碼塊,入口模塊和依賴(lài)的模塊打包在一起為代碼塊 this.assets = {}; //本次編譯產(chǎn)出的資源文件 this.fileDependencies = []; //本次打包涉及到的文件,這里主要是為了實(shí)現(xiàn)watch模式下監(jiān)聽(tīng)文件的變化,文件發(fā)生變化后會(huì)重新編譯 } build(callback) { //第五步:根據(jù)配置文件中的`entry`配置項(xiàng)找到所有的入口+ let entry = {};+ if (typeof this.options.entry === "string") {+ entry.main = this.options.entry; //如果是單入口,將entry:"xx"變成{main:"xx"},這里需要做兼容+ } else {+ entry = this.options.entry;+ } //編譯成功執(zhí)行callback callback() }}
運(yùn)行流程圖(點(diǎn)擊可放大):loader
規(guī)則,對(duì)各模塊進(jìn)行編譯const loader1 = (source) => { return source + "//給你的代碼加點(diǎn)注釋?zhuān)簂oader1";};const loader2 = (source) => { return source + "//給你的代碼加點(diǎn)注釋?zhuān)簂oader2";};
webpack.config.jsconst { loader1, loader2 } = require("./webpack");module.exports = { //省略其他 module: { rules: [ { test: //.js$/, use: [loader1, loader2], }, ], },};
這一步驟將從入口文件出發(fā),然后查找出對(duì)應(yīng)的 Loader 對(duì)源代碼進(jìn)行翻譯和替換。this.fileDependencies
)中,記錄此次編譯依賴(lài)的模塊module
對(duì)象 (里面放著該模塊的路徑、依賴(lài)模塊、源代碼等)Loader
對(duì)源代碼進(jìn)行翻譯和替換module
對(duì)象 push 進(jìn) this.modules
中6.1:把入口文件的絕對(duì)路徑添加到依賴(lài)數(shù)組中,記錄此次編譯依賴(lài)的模塊這里因?yàn)橐@取入口文件的絕對(duì)路徑,考慮到操作系統(tǒng)的兼容性問(wèn)題,需要將路徑的
/
都替換成 /
://將/替換成/function toUnixPath(filePath) { return filePath.replace(////g, "/");}const baseDir = toUnixPath(process.cwd()); //獲取工作目錄,在哪里執(zhí)行命令就獲取哪里的目錄,這里獲取的也是跟操作系統(tǒng)有關(guān)系,要替換成/class Compilation { constructor(webpackOptions) { this.options = webpackOptions; this.modules = []; //本次編譯所有生成出來(lái)的模塊 this.chunks = []; //本次編譯產(chǎn)出的所有代碼塊,入口模塊和依賴(lài)的模塊打包在一起為代碼塊 this.assets = {}; //本次編譯產(chǎn)出的資源文件 this.fileDependencies = []; //本次打包涉及到的文件,這里主要是為了實(shí)現(xiàn)watch模式下監(jiān)聽(tīng)文件的變化,文件發(fā)生變化后會(huì)重新編譯 } build(callback) { //第五步:根據(jù)配置文件中的`entry`配置項(xiàng)找到所有的入口 let entry = {}; if (typeof this.options.entry === "string") { entry.main = this.options.entry; //如果是單入口,將entry:"xx"變成{main:"xx"},這里需要做兼容 } else { entry = this.options.entry; }+ //第六步:從入口文件出發(fā),調(diào)用配置的 `loader` 規(guī)則,對(duì)各模塊進(jìn)行編譯+ for (let entryName in entry) {+ //entryName="main" entryName就是entry的屬性名,也將會(huì)成為代碼塊的名稱(chēng)+ let entryFilePath = path.posix.join(baseDir, entry[entryName]); //path.posix為了解決不同操作系統(tǒng)的路徑分隔符,這里拿到的就是入口文件的絕對(duì)路徑+ //6.1 把入口文件的絕對(duì)路徑添加到依賴(lài)數(shù)組(`this.fileDependencies`)中,記錄此次編譯依賴(lài)的模塊+ this.fileDependencies.push(entryFilePath);+ } //編譯成功執(zhí)行callback callback() }}
6.2.1:讀取模塊內(nèi)容,獲取源代碼
class Compilation { constructor(webpackOptions) { this.options = webpackOptions; this.modules = []; //本次編譯所有生成出來(lái)的模塊 this.chunks = []; //本次編譯產(chǎn)出的所有代碼塊,入口模塊和依賴(lài)的模塊打包在一起為代碼塊 this.assets = {}; //本次編譯產(chǎn)出的資源文件 this.fileDependencies = []; //本次打包涉及到的文件,這里主要是為了實(shí)現(xiàn)watch模式下監(jiān)聽(tīng)文件的變化,文件發(fā)生變化后會(huì)重新編譯 }+ //當(dāng)編譯模塊的時(shí)候,name:這個(gè)模塊是屬于哪個(gè)代碼塊chunk的,modulePath:模塊絕對(duì)路徑+ buildModule(name, modulePath) {+ //6.2.1 讀取模塊內(nèi)容,獲取源代碼+ let sourceCode = fs.readFileSync(modulePath, "utf8");++ return {};+ } build(callback) { //第五步:根據(jù)配置文件中的`entry`配置項(xiàng)找到所有的入口 //代碼省略... //第六步:從入口文件出發(fā),調(diào)用配置的 `loader` 規(guī)則,對(duì)各模塊進(jìn)行編譯 for (let entryName in entry) { //entryName="main" entryName就是entry的屬性名,也將會(huì)成為代碼塊的名稱(chēng) let entryFilePath = path.posix.join(baseDir, entry[entryName]); //path.posix為了解決不同操作系統(tǒng)的路徑分隔符,這里拿到的就是入口文件的絕對(duì)路徑 //6.1 把入口文件的絕對(duì)路徑添加到依賴(lài)數(shù)組(`this.fileDependencies`)中,記錄此次編譯依賴(lài)的模塊 this.fileDependencies.push(entryFilePath); //6.2 得到入口模塊的的 `module` 對(duì)象 (里面放著該模塊的路徑、依賴(lài)模塊、源代碼等)+ let entryModule = this.buildModule(entryName, entryFilePath); } //編譯成功執(zhí)行callback callback() }}
6.2.2:創(chuàng)建模塊對(duì)象
class Compilation { //省略其他 //當(dāng)編譯模塊的時(shí)候,name:這個(gè)模塊是屬于哪個(gè)代碼塊chunk的,modulePath:模塊絕對(duì)路徑 buildModule(name, modulePath) { //6.2.1 讀取模塊內(nèi)容,獲取源代碼 let sourceCode = fs.readFileSync(modulePath, "utf8"); //buildModule最終會(huì)返回一個(gè)modules模塊對(duì)象,每個(gè)模塊都會(huì)有一個(gè)id,id是相對(duì)于根目錄的相對(duì)路徑+ let moduleId = "./" + path.posix.relative(baseDir, modulePath); //模塊id:從根目錄出發(fā),找到與該模塊的相對(duì)路徑(./src/index.js)+ //6.2.2 創(chuàng)建模塊對(duì)象+ let module = {+ id: moduleId,+ names: [name], //names設(shè)計(jì)成數(shù)組是因?yàn)榇淼氖谴四K屬于哪個(gè)代碼塊,可能屬于多個(gè)代碼塊+ dependencies: [], //它依賴(lài)的模塊+ _source: "", //該模塊的代碼信息+ };+ return module; } build(callback) { //省略 }}
6.2.3:找到對(duì)應(yīng)的 Loader
對(duì)源代碼進(jìn)行翻譯和替換
class Compilation { //省略其他 //當(dāng)編譯模塊的時(shí)候,name:這個(gè)模塊是屬于哪個(gè)代碼塊chunk的,modulePath:模塊絕對(duì)路徑 buildModule(name, modulePath) { //6.2.1 讀取模塊內(nèi)容,獲取源代碼 let sourceCode = fs.readFileSync(modulePath, "utf8"); //buildModule最終會(huì)返回一個(gè)modules模塊對(duì)象,每個(gè)模塊都會(huì)有一個(gè)id,id是相對(duì)于根目錄的相對(duì)路徑 let moduleId = "./" + path.posix.relative(baseDir, modulePath); //模塊id:從根目錄出發(fā),找到與該模塊的相對(duì)路徑(./src/index.js) //6.2.2 創(chuàng)建模塊對(duì)象 let module = { id: moduleId, names: [name], //names設(shè)計(jì)成數(shù)組是因?yàn)榇淼氖谴四K屬于哪個(gè)代碼塊,可能屬于多個(gè)代碼塊 dependencies: [], //它依賴(lài)的模塊 _source: "", //該模塊的代碼信息 }; //6.2.3 找到對(duì)應(yīng)的 `Loader` 對(duì)源代碼進(jìn)行翻譯和替換+ let loaders = [];+ let { rules = [] } = this.options.module;+ rules.forEach((rule) => {+ let { test } = rule;+ //如果模塊的路徑和正則匹配,就把此規(guī)則對(duì)應(yīng)的loader添加到loader數(shù)組中+ if (modulePath.match(test)) {+ loaders.push(...rule.use);+ }+ });+ //自右向左對(duì)模塊進(jìn)行轉(zhuǎn)譯+ sourceCode = loaders.reduceRight((code, loader) => {+ return loader(code);+ }, sourceCode); return module; } build(callback) { //省略 }}
6.3:將生成的入口文件module
對(duì)象 push 進(jìn)this.modules
中
class Compilation { constructor(webpackOptions) { this.options = webpackOptions; this.modules = []; //本次編譯所有生成出來(lái)的模塊 this.chunks = []; //本次編譯產(chǎn)出的所有代碼塊,入口模塊和依賴(lài)的模塊打包在一起為代碼塊 this.assets = {}; //本次編譯產(chǎn)出的資源文件 this.fileDependencies = []; //本次打包涉及到的文件,這里主要是為了實(shí)現(xiàn)watch模式下監(jiān)聽(tīng)文件的變化,文件發(fā)生變化后會(huì)重新編譯 } buildModule(name, modulePath) { //省略其他 } build(callback) { //第五步:根據(jù)配置文件中的`entry`配置項(xiàng)找到所有的入口 //省略其他 //第六步:從入口文件出發(fā),調(diào)用配置的 `loader` 規(guī)則,對(duì)各模塊進(jìn)行編譯 for (let entryName in entry) { //entryName="main" entryName就是entry的屬性名,也將會(huì)成為代碼塊的名稱(chēng) let entryFilePath = path.posix.join(baseDir, entry[entryName]); //path.posix為了解決不同操作系統(tǒng)的路徑分隔符,這里拿到的就是入口文件的絕對(duì)路徑 //6.1 把入口文件的絕對(duì)路徑添加到依賴(lài)數(shù)組(`this.fileDependencies`)中,記錄此次編譯依賴(lài)的模塊 this.fileDependencies.push(entryFilePath); //6.2 得到入口模塊的的 `module` 對(duì)象 (里面放著該模塊的路徑、依賴(lài)模塊、源代碼等) let entryModule = this.buildModule(entryName, entryFilePath);+ //6.3 將生成的入口文件 `module` 對(duì)象 push 進(jìn) `this.modules` 中+ this.modules.push(entryModule); } //編譯成功執(zhí)行callback callback() }}
運(yùn)行流程圖(點(diǎn)擊可放大):AST
中查找 require
語(yǔ)句,找出依賴(lài)的模塊名稱(chēng)和絕對(duì)路徑this.fileDependencies
中模塊 id
模塊 id
dependencies
屬性中module._source
屬性上module 對(duì)象
中的 dependencies
進(jìn)行遞歸執(zhí)行 buildModule
)module 對(duì)象
,push 到 this.modules
中module
對(duì)象+ const parser = require("@babel/parser");+ let types = require("@babel/types"); //用來(lái)生成或者判斷節(jié)點(diǎn)的AST語(yǔ)法樹(shù)的節(jié)點(diǎn)+ const traverse = require("@babel/traverse").default;+ const generator = require("@babel/generator").default;//獲取文件路徑+ function tryExtensions(modulePath, extensions) {+ if (fs.existsSync(modulePath)) {+ return modulePath;+ }+ for (let i = 0; i < extensions?.length; i++) {+ let filePath = modulePath + extensions[i];+ if (fs.existsSync(filePath)) {+ return filePath;+ }+ }+ throw new Error(`無(wú)法找到${modulePath}`);+ }class Compilation { constructor(webpackOptions) { this.options = webpackOptions; this.modules = []; //本次編譯所有生成出來(lái)的模塊 this.chunks = []; //本次編譯產(chǎn)出的所有代碼塊,入口模塊和依賴(lài)的模塊打包在一起為代碼塊 this.assets = {}; //本次編譯產(chǎn)出的資源文件 this.fileDependencies = []; //本次打包涉及到的文件,這里主要是為了實(shí)現(xiàn)watch模式下監(jiān)聽(tīng)文件的變化,文件發(fā)生變化后會(huì)重新編譯 } //當(dāng)編譯模塊的時(shí)候,name:這個(gè)模塊是屬于哪個(gè)代碼塊chunk的,modulePath:模塊絕對(duì)路徑 buildModule(name, modulePath) { //省略其他 //6.2.1 讀取模塊內(nèi)容,獲取源代碼 //6.2.2 創(chuàng)建模塊對(duì)象 //6.2.3 找到對(duì)應(yīng)的 `Loader` 對(duì)源代碼進(jìn)行翻譯和替換 //自右向左對(duì)模塊進(jìn)行轉(zhuǎn)譯 sourceCode = loaders.reduceRight((code, loader) => { return loader(code); }, sourceCode); //通過(guò)loader翻譯后的內(nèi)容一定得是js內(nèi)容,因?yàn)樽詈蟮米呶覀僢abel-parse,只有js才能成編譯AST //第七步:找出此模塊所依賴(lài)的模塊,再對(duì)依賴(lài)模塊進(jìn)行編譯+ //7.1:先把源代碼編譯成 [AST](https://astexplorer.net/)+ let ast = parser.parse(sourceCode, { sourceType: "module" });+ traverse(ast, {+ CallExpression: (nodePath) => {+ const { node } = nodePath;+ //7.2:在 `AST` 中查找 `require` 語(yǔ)句,找出依賴(lài)的模塊名稱(chēng)和絕對(duì)路徑+ if (node.callee.name === "require") {+ let depModuleName = node.arguments[0].value; //獲取依賴(lài)的模塊+ let dirname = path.posix.dirname(modulePath); //獲取當(dāng)前正在編譯的模所在的目錄+ let depModulePath = path.posix.join(dirname, depModuleName); //獲取依賴(lài)模塊的絕對(duì)路徑+ let extensions = this.options.resolve?.extensions || [ ".js" ]; //獲取配置中的extensions+ depModulePath = tryExtensions(depModulePath, extensions); //嘗試添加后綴,找到一個(gè)真實(shí)在硬盤(pán)上存在的文件+ //7.3:將依賴(lài)模塊的絕對(duì)路徑 push 到 `this.fileDependencies` 中+ this.fileDependencies.push(depModulePath);+ //7.4:生成依賴(lài)模塊的`模塊 id`+ let depModuleId = "./" + path.posix.relative(baseDir, depModulePath);+ //7.5:修改語(yǔ)法結(jié)構(gòu),把依賴(lài)的模塊改為依賴(lài)`模塊 id` require("./name")=>require("./src/name.js")+ node.arguments = [types.stringLiteral(depModuleId)];+ //7.6:將依賴(lài)模塊的信息 push 到該模塊的 `dependencies` 屬性中+ module.dependencies.push({ depModuleId, depModulePath });+ }+ },+ });+ //7.7:生成新代碼,并把轉(zhuǎn)譯后的源代碼放到 `module._source` 屬性上+ let { code } = generator(ast);+ module._source = code;+ //7.8:對(duì)依賴(lài)模塊進(jìn)行編譯(對(duì) `module 對(duì)象`中的 `dependencies` 進(jìn)行遞歸執(zhí)行 `buildModule` )+ module.dependencies.forEach(({ depModuleId, depModulePath }) => {+ //考慮到多入口打包 :一個(gè)模塊被多個(gè)其他模塊引用,不需要重復(fù)打包+ let existModule = this.modules.find((item) => item.id === depModuleId);+ //如果modules里已經(jīng)存在這個(gè)將要編譯的依賴(lài)模塊了,那么就不需要編譯了,直接把此代碼塊的名稱(chēng)添加到對(duì)應(yīng)模塊的names字段里就可以+ if (existModule) {+ //names指的是它屬于哪個(gè)代碼塊chunk+ existModule.names.push(name);+ } else {+ //7.9:對(duì)依賴(lài)模塊編譯完成后得到依賴(lài)模塊的 `module 對(duì)象`,push 到 `this.modules` 中+ let depModule = this.buildModule(name, depModulePath);+ this.modules.push(depModule);+ }+ });+ //7.10:等依賴(lài)模塊全部編譯完成后,返回入口模塊的 `module` 對(duì)象+ return module; } //省略其他}
運(yùn)行流程圖(點(diǎn)擊可放大):chunk
chunk
,每個(gè)代碼塊chunk
里面會(huì)放著本入口模塊和它依賴(lài)的模塊,這里暫時(shí)不考慮代碼分割。class Compilation { constructor(webpackOptions) { this.options = webpackOptions; this.modules = []; //本次編譯所有生成出來(lái)的模塊 this.chunks = []; //本次編譯產(chǎn)出的所有代碼塊,入口模塊和依賴(lài)的模塊打包在一起為代碼塊 this.assets = {}; //本次編譯產(chǎn)出的資源文件 this.fileDependencies = []; //本次打包涉及到的文件,這里主要是為了實(shí)現(xiàn)watch模式下監(jiān)聽(tīng)文件的變化,文件發(fā)生變化后會(huì)重新編譯 } buildModule(name, modulePath) { //省略其他 } build(callback) { //第五步:根據(jù)配置文件中的`entry`配置項(xiàng)找到所有的入口 //省略其他 //第六步:從入口文件出發(fā),調(diào)用配置的 `loader` 規(guī)則,對(duì)各模塊進(jìn)行編譯 for (let entryName in entry) { //entryName="main" entryName就是entry的屬性名,也將會(huì)成為代碼塊的名稱(chēng) let entryFilePath = path.posix.join(baseDir, entry[entryName]); //path.posix為了解決不同操作系統(tǒng)的路徑分隔符,這里拿到的就是入口文件的絕對(duì)路徑 //6.1 把入口文件的絕對(duì)路徑添加到依賴(lài)數(shù)組(`this.fileDependencies`)中,記錄此次編譯依賴(lài)的模塊 this.fileDependencies.push(entryFilePath); //6.2 得到入口模塊的的 `module` 對(duì)象 (里面放著該模塊的路徑、依賴(lài)模塊、源代碼等) let entryModule = this.buildModule(entryName, entryFilePath); //6.3 將生成的入口文件 `module` 對(duì)象 push 進(jìn) `this.modules` 中 this.modules.push(entryModule); //第八步:等所有模塊都編譯完成后,根據(jù)模塊之間的依賴(lài)關(guān)系,組裝代碼塊 `chunk`(一般來(lái)說(shuō),每個(gè)入口文件會(huì)對(duì)應(yīng)一個(gè)代碼塊`chunk`,每個(gè)代碼塊`chunk`里面會(huì)放著本入口模塊和它依賴(lài)的模塊)+ let chunk = {+ name: entryName, //entryName="main" 代碼塊的名稱(chēng)+ entryModule, //此代碼塊對(duì)應(yīng)的module的對(duì)象,這里就是src/index.js 的module對(duì)象+ modules: this.modules.filter((item) => item.names.includes(entryName)), //找出屬于該代碼塊的模塊+ };+ this.chunks.push(chunk); } //編譯成功執(zhí)行callback callback() }}
運(yùn)行流程圖(點(diǎn)擊可放大):chunk
轉(zhuǎn)換成一個(gè)一個(gè)文件加入到輸出列表output.filename
去生成輸出文件的文件名稱(chēng),同時(shí)還需要生成運(yùn)行時(shí)代碼://生成運(yùn)行時(shí)代碼+ function getSource(chunk) {+ return `+ (() => {+ var modules = {+ ${chunk.modules.map(+ (module) => `+ "${module.id}": (module) => {+ ${module._source}+ }+ `+ )} + };+ var cache = {};+ function require(moduleId) {+ var cachedModule = cache[moduleId];+ if (cachedModule !== undefined) {+ return cachedModule.exports;+ }+ var module = (cache[moduleId] = {+ exports: {},+ });+ modules[moduleId](module, module.exports, require);+ return module.exports;+ }+ var exports ={};+ ${chunk.entryModule._source}+ })();+ `;+ }class Compilation { constructor(webpackOptions) { this.options = webpackOptions; this.modules = []; //本次編譯所有生成出來(lái)的模塊 this.chunks = []; //本次編譯產(chǎn)出的所有代碼塊,入口模塊和依賴(lài)的模塊打包在一起為代碼塊 this.assets = {}; //本次編譯產(chǎn)出的資源文件 this.fileDependencies = []; //本次打包涉及到的文件,這里主要是為了實(shí)現(xiàn)watch模式下監(jiān)聽(tīng)文件的變化,文件發(fā)生變化后會(huì)重新編譯 } //當(dāng)編譯模塊的時(shí)候,name:這個(gè)模塊是屬于哪個(gè)代碼塊chunk的,modulePath:模塊絕對(duì)路徑 buildModule(name, modulePath) { //省略 } build(callback) { //第五步:根據(jù)配置文件中的`entry`配置項(xiàng)找到所有的入口 //第六步:從入口文件出發(fā),調(diào)用配置的 `loader` 規(guī)則,對(duì)各模塊進(jìn)行編譯 for (let entryName in entry) { //省略 //6.1 把入口文件的絕對(duì)路徑添加到依賴(lài)數(shù)組(`this.fileDependencies`)中,記錄此次編譯依賴(lài)的模塊 //6.2 得到入口模塊的的 `module` 對(duì)象 (里面放著該模塊的路徑、依賴(lài)模塊、源代碼等) //6.3 將生成的入口文件 `module` 對(duì)象 push 進(jìn) `this.modules` 中 //第八步:等所有模塊都編譯完成后,根據(jù)模塊之間的依賴(lài)關(guān)系,組裝代碼塊 `chunk`(一般來(lái)說(shuō),每個(gè)入口文件會(huì)對(duì)應(yīng)一個(gè)代碼塊`chunk`,每個(gè)代碼塊`chunk`里面會(huì)放著本入口模塊和它依賴(lài)的模塊) } //第九步:把各個(gè)代碼塊 `chunk` 轉(zhuǎn)換成一個(gè)一個(gè)文件加入到輸出列表+ this.chunks.forEach((chunk) => {+ let filename = this.options.output.filename.replace("[name]", chunk.name);+ this.assets[filename] = getSource(chunk);+ });+ callback(+ null,+ {+ chunks: this.chunks,+ modules: this.modules,+ assets: this.assets,+ },+ this.fileDependencies+ ); }}
到了這里,Compilation
的邏輯就走完了。class Compiler { constructor(webpackOptions) { this.options = webpackOptions; //存儲(chǔ)配置信息 //它內(nèi)部提供了很多鉤子 this.hooks = { run: new SyncHook(), //會(huì)在編譯剛開(kāi)始的時(shí)候觸發(fā)此run鉤子 done: new SyncHook(), //會(huì)在編譯結(jié)束的時(shí)候觸發(fā)此done鉤子 }; } compile(callback) { //省略 } //第四步:執(zhí)行`Compiler`對(duì)象的`run`方法開(kāi)始執(zhí)行編譯 run(callback) { this.hooks.run.call(); //在編譯前觸發(fā)run鉤子執(zhí)行,表示開(kāi)始啟動(dòng)編譯了 const onCompiled = (err, stats, fileDependencies) => {+ //第十步:確定好輸出內(nèi)容之后,根據(jù)配置的輸出路徑和文件名,將文件內(nèi)容寫(xiě)入到文件系統(tǒng)(這里就是硬盤(pán))+ for (let filename in stats.assets) {+ let filePath = path.join(this.options.output.path, filename);+ fs.writeFileSync(filePath, stats.assets[filename], "utf8");+ } + callback(err, {+ toJson: () => stats,+ }); this.hooks.done.call(); //當(dāng)編譯成功后會(huì)觸發(fā)done這個(gè)鉤子執(zhí)行 }; this.compile(onCompiled); //開(kāi)始編譯,成功之后調(diào)用onCompiled }}
運(yùn)行流程圖(點(diǎn)擊可放大):node ./debugger.js
,通過(guò)我們手寫(xiě)的 Webpack 進(jìn)行打包,得到輸出文件 dist/main.js:Compilation
中的 this.fileDependencies
(本次打包涉及到的文件)是用來(lái)做什么的?為什么沒(méi)有地方用到該屬性?this.fileDependencies
里面的文件進(jìn)行監(jiān)聽(tīng),當(dāng)文件發(fā)生變化時(shí),重新執(zhí)行 compile
函數(shù)。class Compiler { constructor(webpackOptions) { //省略 } compile(callback) { //雖然webpack只有一個(gè)Compiler,但是每次編譯都會(huì)產(chǎn)出一個(gè)新的Compilation, //這里主要是為了考慮到watch模式,它會(huì)在啟動(dòng)時(shí)先編譯一次,然后監(jiān)聽(tīng)文件變化,如果發(fā)生變化會(huì)重新開(kāi)始編譯 //每次編譯都會(huì)產(chǎn)出一個(gè)新的Compilation,代表每次的編譯結(jié)果 let compilation = new Compilation(this.options); compilation.build(callback); //執(zhí)行compilation的build方法進(jìn)行編譯,編譯成功之后執(zhí)行回調(diào) } //第四步:執(zhí)行`Compiler`對(duì)象的`run`方法開(kāi)始執(zhí)行編譯 run(callback) { this.hooks.run.call(); //在編譯前觸發(fā)run鉤子執(zhí)行,表示開(kāi)始啟動(dòng)編譯了 const onCompiled = (err, stats, fileDependencies) => { //第十步:確定好輸出內(nèi)容之后,根據(jù)配置的輸出路徑和文件名,將文件內(nèi)容寫(xiě)入到文件系統(tǒng)(這里就是硬盤(pán)) for (let filename in stats.assets) { let filePath = path.join(this.options.output.path, filename); fs.writeFileSync(filePath, stats.assets[filename], "utf8"); } callback(err, { toJson: () => stats, });+ fileDependencies.forEach((fileDependencie) => {+ fs.watch(fileDependencie, () => this.compile(onCompiled));+ }); this.hooks.done.call(); //當(dāng)編譯成功后會(huì)觸發(fā)done這個(gè)鉤子執(zhí)行 }; this.compile(onCompiled); //開(kāi)始編譯,成功之后調(diào)用onCompiled }}
相信看到這里,你一定也理解了 compile 和 Compilation 的設(shè)計(jì),都是為了解耦和復(fù)用呀。關(guān)鍵詞:設(shè)計(jì),圖片
客戶&案例
營(yíng)銷(xiāo)資訊
關(guān)于我們
客戶&案例
營(yíng)銷(xiāo)資訊
關(guān)于我們
微信公眾號(hào)
版權(quán)所有? 億企邦 1997-2025 保留一切法律許可權(quán)利。