而本節(jié)最大的作用,就是幫大家一點(diǎn)一" />

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

15158846557 在線咨詢(xún) 在線咨詢(xún)
15158846557 在線咨詢(xún)
所在位置: 首頁(yè) > 營(yíng)銷(xiāo)資訊 > 網(wǎng)站運(yùn)營(yíng) > 二十張圖片徹底講明白 Webpack 設(shè)計(jì)理念

二十張圖片徹底講明白 Webpack 設(shè)計(jì)理念

時(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ì)理念:

一、前言

Webpack 一直都是有些人的心魔,不清楚原理是什么,不知道怎么去配置,只會(huì)基本的 API 使用。它就像一個(gè)黑盒,讓部分開(kāi)發(fā)者對(duì)它望而生畏。

而本節(jié)最大的作用,就是幫大家一點(diǎn)一點(diǎn)的消滅心魔。

大家之所以認(rèn)為 Webpack 復(fù)雜,很大程度上是因?yàn)樗栏街惶嫶蟮纳鷳B(tài)系統(tǒng)。其實(shí) Webpack 的核心流程遠(yuǎn)沒(méi)有我們想象中那么復(fù)雜,甚至只需百來(lái)行代碼就能完整復(fù)刻出來(lái)。

因此在學(xué)習(xí)過(guò)程中,我們應(yīng)注重學(xué)習(xí)它本身的設(shè)計(jì)思想,不管是它的 Plugin 系統(tǒng)還是 Loader 系統(tǒng),都是建立于這套核心思想之上。所謂萬(wàn)變不離其宗,一通百通。

在本文中,我將會(huì)從 Webpack 的整體流程出發(fā),通篇采用結(jié)論先行、自頂向下的方式進(jìn)行講解。在涉及到原理性的知識(shí)時(shí),盡量采用圖文的方式輔以理解,注重實(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.jswebpack.config.js

const 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.js

module.exports = "不要禿頭啊";src/age.js

module.exports = "99";文件依賴(lài)關(guān)系:

Webpack 本質(zhì)上是一個(gè)函數(shù),它接受一個(gè)配置信息作為參數(shù),執(zhí)行后返回一個(gè) compiler 對(duì)象,調(diào)用 compiler 對(duì)象中的 run 方法就會(huì)啟動(dòng)編譯。run 方法接受一個(gè)回調(diào),可以用來(lái)查看編譯過(guò)程中的錯(cuò)誤信息或編譯信息。

debugger.js

// 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)化):

運(yùn)行該文件,得到結(jié)果:

entry文件打印作者信息 不要禿頭啊 99

三、核心思想

我們先來(lái)分析一下源代碼和構(gòu)建產(chǎn)物之間的關(guān)系:

從圖中可以看出,入口文件(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 原理,那么核心問(wèn)題就變成了:如何將左邊的源代碼轉(zhuǎn)換成 dist/main.js 文件?


核心思想:
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);',},];在這過(guò)程中,由于瀏覽器并不認(rèn)識(shí)除 html、js、css 以外的文件格式,所以我們還需要對(duì)源文件進(jìn)行轉(zhuǎn)換 —— **Loader 系統(tǒng)**。

Loader 系統(tǒng) 本質(zhì)上就是接收資源文件,并對(duì)其進(jìn)行轉(zhuǎn)換,最終輸出轉(zhuǎn)換后的文件:

除此之外,打包過(guò)程中也有一些特定的時(shí)機(jī)需要處理,比如:

這個(gè)時(shí)候需要一個(gè)可插拔的設(shè)計(jì),方便給社區(qū)提供可擴(kuò)展的接口 —— **Plugin 系統(tǒng)**。

Plugin 系統(tǒng) 本質(zhì)上就是一種事件流的機(jī)制,到了固定的時(shí)間節(jié)點(diǎn)就廣播特定的事件,用戶可以在事件內(nèi)執(zhí)行特定的邏輯,類(lèi)似于生命周期:

這些設(shè)計(jì)也都是根據(jù)使用場(chǎng)景來(lái)的,只有理清需求后我們才能更好的理解它的設(shè)計(jì)思想。

四、架構(gòu)設(shè)計(jì)

在理清楚核心思想后,剩下的就是對(duì)其進(jìn)行一步步拆解。

上面提到,我們需要建立一套事件流的機(jī)制來(lái)管控整個(gè)打包過(guò)程,大致可以分為三個(gè)階段:

這其中又以編譯階段最為復(fù)雜,另外還考慮到一個(gè)場(chǎng)景:watch mode[1](當(dāng)文件變化時(shí),將重新進(jìn)行編譯),因此這里最好將編譯階段(也就是下文中的compilation)單獨(dú)解耦出來(lái)。

Webpack 源碼中,compiler 就像是一個(gè)大管家,它就代表上面說(shuō)的三個(gè)階段,在它上面掛載著各種生命周期函數(shù),而 compilation 就像專(zhuān)管伙食的廚師,專(zhuān)門(mén)負(fù)責(zé)編譯相關(guān)的工作,也就是打包過(guò)程中這個(gè)階段。畫(huà)個(gè)圖幫助大家理解:

大致架構(gòu)定下后,那現(xiàn)在應(yīng)該如何實(shí)現(xiàn)這套事件流呢?

這時(shí)候就需要借助 Tapable 了!它是一個(gè)類(lèi)似于 Node.js 中的 EventEmitter 的庫(kù),但更專(zhuān)注于自定義事件的觸發(fā)和處理。通過(guò) Tapable 我們可以注冊(cè)自定義事件,然后在適當(dāng)?shù)臅r(shí)機(jī)去執(zhí)行自定義事件。

類(lèi)比到 VueReact 框架中的生命周期函數(shù),它們就是到了固定的時(shí)間節(jié)點(diǎn)就執(zhí)行對(duì)應(yīng)的生命周期,tapable 做的事情就和這個(gè)差不多,我們可以通過(guò)它先注冊(cè)一系列的生命周期函數(shù),然后在合適的時(shí)間點(diǎn)執(zhí)行。

example :

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ò) tapablecomilercompilation 上像這樣掛載著一系列生命周期 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ā)此鉤子 }; }}

五、具體實(shí)現(xiàn)

整個(gè)實(shí)現(xiàn)過(guò)程大致分為以下步驟:

5.1、搭建結(jié)構(gòu),讀取配置參數(shù)

根據(jù) Webpack 的用法可以看出, Webpack 本質(zhì)上是一個(gè)函數(shù),它接受一個(gè)配置信息作為參數(shù),執(zhí)行后返回一個(gè) compiler 對(duì)象,調(diào)用 compiler 對(duì)象中的 run 方法就會(huì)啟動(dòng)編譯。run 方法接受一個(gè)回調(diào),可以用來(lái)查看編譯過(guò)程中的錯(cuò)誤信息或編譯信息。

修改 debugger.js 中 webpack 的引用:

+ 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)行流程圖:

5.2、用配置參數(shù)對(duì)象初始化 Compiler 對(duì)象

上面提到過(guò),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)行流程圖:

5.3、掛載配置文件中的插件

先寫(xiě)兩個(gè)自定義插件配置到 webpack.config.js 中:一個(gè)在開(kāi)始打包的時(shí)候執(zhí)行,一個(gè)在打包完成后執(zhí)行。

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)行流程圖:

5.4、執(zhí)行Compiler對(duì)象的run方法開(kāi)始執(zhí)行編譯

重點(diǎn)來(lái)了!

在正式開(kāi)始編譯前,我們需要先調(diào)用 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)擊可放大):

5.5、根據(jù)配置文件中的entry配置項(xiàng)找到所有的入口

接下來(lái)就正式開(kāi)始編譯了,邏輯均在 Compilation 中。

在編譯前我們首先需要知道入口文件,而 入口的配置方式[2] 有多種,可以配置成字符串,也可以配置成一個(gè)對(duì)象,這一步驟就是為了統(tǒng)一配置信息的格式,然后找出所有的入口(考慮多入口打包的場(chǎng)景)。

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)擊可放大):

5.6、從入口文件出發(fā),調(diào)用配置的loader規(guī)則,對(duì)各模塊進(jìn)行編譯

Loader 本質(zhì)上就是一個(gè)函數(shù),接收資源文件或者上一個(gè) Loader 產(chǎn)生的結(jié)果作為入?yún)ⅲ罱K輸出轉(zhuǎn)換后的結(jié)果。

寫(xiě)兩個(gè)自定義 Loader 配置到 webpack.config.js 中:

const loader1 = (source) => { return source + "//給你的代碼加點(diǎn)注釋?zhuān)簂oader1";};const loader2 = (source) => { return source + "//給你的代碼加點(diǎn)注釋?zhuān)簂oader2";};webpack.config.js

const { loader1, loader2 } = require("./webpack");module.exports = { //省略其他 module: { rules: [ { test: //.js$/, use: [loader1, loader2], }, ], },};這一步驟將從入口文件出發(fā),然后查找出對(duì)應(yīng)的 Loader 對(duì)源代碼進(jìn)行翻譯和替換。

主要有三個(gè)要點(diǎn):

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)擊可放大):

5.7、找出此模塊所依賴(lài)的模塊,再對(duì)依賴(lài)模塊進(jìn)行編譯

該步驟是整體流程中最為復(fù)雜的,一遍看不懂沒(méi)關(guān)系,可以先理解思路。

該步驟經(jīng)過(guò)細(xì)化可以將其拆分成十個(gè)小步驟:

+ 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)擊可放大):

5.8、等所有模塊都編譯完成后,根據(jù)模塊之間的依賴(lài)關(guān)系,組裝代碼塊 chunk

現(xiàn)在,我們已經(jīng)知道了入口模塊和它所依賴(lài)模塊的所有信息,可以去生成對(duì)應(yīng)的代碼塊了。

一般來(lái)說(shuō),每個(gè)入口文件會(huì)對(duì)應(yīng)一個(gè)代碼塊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)擊可放大):

5.9、把各個(gè)代碼塊 chunk 轉(zhuǎn)換成一個(gè)一個(gè)文件加入到輸出列表

這一步需要結(jié)合配置文件中的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 的邏輯就走完了。

運(yùn)行流程圖(點(diǎn)擊可放大):

5.10、確定好輸出內(nèi)容之后,根據(jù)配置的輸出路徑和文件名,將文件內(nèi)容寫(xiě)入到文件系統(tǒng)

該步驟就很簡(jiǎn)單了,直接按照 Compilation 中的 this.status 對(duì)象將文件內(nèi)容寫(xiě)入到文件系統(tǒng)(這里就是硬盤(pán))。

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)擊可放大):

完整流程圖

以上就是整個(gè) Webpack 的運(yùn)行流程圖,還是描述的比較清晰的,跟著一步步走看懂肯定沒(méi)問(wèn)題!

執(zhí)行 node ./debugger.js,通過(guò)我們手寫(xiě)的 Webpack 進(jìn)行打包,得到輸出文件 dist/main.js

六、實(shí)現(xiàn) watch 模式

看完上面的實(shí)現(xiàn),有些小伙伴可能有疑問(wèn)了:Compilation 中的 this.fileDependencies(本次打包涉及到的文件)是用來(lái)做什么的?為什么沒(méi)有地方用到該屬性?

這里其實(shí)是為了實(shí)現(xiàn) Webpack 的 watch 模式[4]:當(dāng)文件發(fā)生變更時(shí)將重新編譯。

思路:對(duì) 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ù)用呀。

七、總結(jié)

本文從 Webpack 的基本使用和構(gòu)建產(chǎn)物出發(fā),從思想和架構(gòu)兩方面深度剖析了 Webpack 的設(shè)計(jì)理念。最后在代碼實(shí)現(xiàn)階段,通過(guò)百來(lái)行代碼手寫(xiě)了 Webpack 的整體流程,盡管它只能對(duì)文件進(jìn)行打包,還缺少很多功能,但麻雀雖小,卻也五臟俱全。







關(guān)鍵詞:設(shè)計(jì),圖片

74
73
25
news

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

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