Question 2: webpack的編譯流程是啥?
應該會有面試官這樣問過你:
- webpack了解多少?
- 對webpack的編譯原理了解嗎?
- 寫過webpack插件嗎?
- 列舉webpack編譯流程中的hook節點
這些問題其實都可以被看作是同一個問題,那就是面試官在問你:你對webpack的編譯流程了解多少?
來總結一下我聽到過的答案,盡量完全復原候選人面試的時候說的原話。
答案1: webpack就是通過loader來加載資源,通過插件進行修改,最后打包生成bundles.js
答案2: Webpack 的運行流程是一個串行的過程,從啟動到結束會依次執行以下流程:
1. 初始化參數:從配置文件和 Shell 語句中讀取與合並參數,得出最終的參數;
2. 開始編譯:用上一步得到的參數初始化 Compiler 對象,加載所有配置的插件,執行對象的 run 方法開始執行編譯;
3. 確定入口: 根據配置中的 entry 找出所有的入口文件;
4. 編譯模塊: 從入口文件出發,調用所有配置的 Loader 對模塊進行翻譯,再找出該模塊依賴的模
塊,再遞歸本步驟直到所有入口依賴的文件都經過了本步驟的處理;
5. 完成模塊編譯: 在經過第 4 步使用 Loader 翻譯完所有模塊后,得到了每個模塊被翻譯后的最終內
容以及它們之間的依賴關系;
6. 輸出資源:根據入口和模塊之間的依賴關系,組裝成一個個包含多個模塊的 Chunk,再把每個
Chunk 轉換成一個單獨的文件加入到輸出列表,這步是可以修改輸出內容的最后機會;
7. 輸出完成:在確定好輸出內容后,根據配置確定輸出的路徑和文件名,把文件內容寫入到文件系
統。
復制代碼
哇哦! 這說起來一套一套的,有備而來啊! 這是從哪背的書?這時候,敢這么回答,那一定是作死現場。因為面試官一定會衍生出一系列問題,把你問哭。例如:
- 你剛說的輸出列表是什么?
- 其實是在問chunks是掛在哪個對象上。 - 入口模塊是怎么處理的?
- 其實在問如何生成入口模塊的 - 最后是怎么把文件內容寫入到文件系統的?
- 其實是在問compiler是如何具備文件讀寫能力的 - 什么時候構建階段開始?
- 其實是在問webpack開始構建之前做了哪些構建准備 - 你說的調用Loader對模塊進行翻譯是如何做到的?
- 其實是在問在哪個階段處理loader的 - ……
所以,你感覺快哭了沒?我給你講一個我的答案可好?
面試其實是門很糾結的藝術。webpack東西那么多,不是三兩句就能講明白的。 所以如何花最少的時間盡量多的講清楚內容,就是需要你認真琢磨的事情了。
答案3: 先把關鍵點說清楚,帶源碼那種,而且是越直觀越好。以下1-4點花30秒說完吧,畢竟是說給面試官聽。
- 初始化參數,
webpack.config.js的module.export,結合默認參數,merge出最終的參數。 - 開始編譯,通過初始化參數來實例化
Compiler對象,加載所有配置的插件,執行對象的run方法。 - 確認入口文件。
- 編譯模塊:從入口文件出發,調用所有配置的Loader對模塊進行加載,再找出該模塊依賴的模塊。通過遞歸這個過程直到所有入口文件都經過處理,得到一條依賴線。
webpack.js中核心的操作就是require了node_modules/webpack-cli/bin/cli.jscli.js- 01 當前文件一般有二個操作,處理參數,將參數交給不同的邏輯(分發業務)
- 02
options(初始化參數) - 03
complier(實例化Compiler對象) - 04
complier.run( 那run里面做了什么,后續再看 )
- 實例化complier對象,complier會貫穿整個webpack工作流的過程。
complier繼承Tabable,所以complier具有操作鈎子的能力。例如監聽、觸發事件,而webpack是個事件流。- 實例話
complier對象時,會把很多屬性掛載上去。其中NodeEnvironmentPlugin讓complier具備文件讀寫的能力。 - 將
plugins中的插件都掛載到了cpmplier身上 - 將內部默認的
plugins與complier建立聯系,其中有個EntryOptionsPlugin處理了入口模塊的id - 在
webpack/lib/SingleEntryPlugin.js里,compiler監聽了make鈎子- 在
singleEntryPlugin.js模塊的apply方法中有兩個鈎子監聽 dep = SingleEntryPlugin.createDependency(entry,name)- 其中
compilation鈎子就是讓compilation具備了利用normalModuleFactory工廠創建一個普通模塊的能力。因為compilation就是利用自己創建出來的模塊來加載需要被打包的模塊。 - 其中
make鈎子在Compiler.run的時候會被調用,到這里就意味着某個模塊執行打包之前的所有准備工作就做完了。 - 然后
Compilation調用addEntry就標志着make構建階段開始了。
- 在
run方法的執行- 剛說了Compiler.run方法執行會調用make鈎子,那run方法里就是有一堆鈎子按着順序觸發,例如
beforeRun、run、compile - compile 方法的執行
- 先准備些參數,例如剛才提到的
normalModuleFactory,用於后續創建模塊。 - 觸發
beforeCompile - 將准備參數傳入一個方法(
newCompilation),用於創建一個compilation。在newCompilation內部,先調用createCompilation,然后觸發this.compilation鈎子和compilation鈎子的監聽
- 先准備些參數,例如剛才提到的
- 創建了
compilation對象之后就觸發了make鈎子。當觸發make鈎子監聽的時候,會將compilation對象傳入。compilation.addEntry就意味着make構建階段開始。 make鈎子被觸發,接收到compilation對象,那么從compilation可以解構出三個值。entry:當前被打包模塊的相對路徑,name,context:當前項目的跟路徑processDependencies處理模塊間的依賴關系。函數內部通過async.forEach來遞歸創建每個被加載進來的模塊。compilation調用addEntry方法,內部調用_addModuleChain方法去處理依賴。compilation當中可以通過normalModuleFactory工廠來創建一個普通的模塊對象。webpack內部默認開啟來一個100並發量的打包操作. 源碼里看到的是normalModuleFactory.create這樣一個方法。- 然后在
beforeResolve方法里會觸發一個factory鈎子監聽。上述操作完成后,factory獲取到一個函數並對其進行調用。函數中又有一個resolver鈎子被觸發,resolver其實是處理loader。當觸發resolver鈎子,就意味着所有的Loader處理完畢。 - 接下里就會觸發
afterResolve這個鈎子,調用new NormalModule - 最后就是調用
buildModule方法開始編譯 -> 調用build-> 調用doBuild。bulid過程中會將js代碼轉化成ast語法樹,如果當前js模塊引用了其它模塊,那就需要遞歸重復bulid。當前所有入口模塊都被存放在compilation對象的entries數組里。 - 那我還需要對當前模塊的
ast語法樹進行一些修改,再轉化回js代碼。例如將require轉化成__webpack_require__ - 最后
compile方法最后調用compilation.seal方法去處理chunk。 生成代碼內容,最終輸出文件到指定打包路徑下。
- 剛說了Compiler.run方法執行會調用make鈎子,那run方法里就是有一堆鈎子按着順序觸發,例如
唉。 哭了沒。 面試的時候千萬別講這么細,因為面試官也細節不到這個程度。所以,你要講的話,就把你認為重要話術整理出來吧。
接下來直接上代碼,我們自己來實現webpack編譯流程
- run.js
let webpack = require('./youWebpack') let options = require('./webpack.config') let compiler = webpack(options); // webpack 初始化 webpack.config.js 的 module.exports // 執行run方法 compiler.run((err, stats) => { console.log(err) console.log(stats) }) 復制代碼
- 既然run方法require了
youWebpack,那就得寫出來不是。
/* * youWebpack.js */ const Compiler = require('./Compiler'); const NodeEnvironmentPlugin = require('./NodeEnvironmentPlugin') const WebpackOptionsApply = require('./WebpackOptionsApply') const webpack = function (options) { // 01 實例化 compiler 對象 let compiler = new Compiler(options.context) compiler.options = options // 02 初始化 NodeEnvironmentPlugin(讓compiler具體文件讀寫能力) new NodeEnvironmentPlugin().apply(compiler) // 03 掛載所有 plugins 插件至 compiler 對象身上 if (options.plugins && Array.isArray(options.plugins)) { for (const plugin of options.plugins) { plugin.apply(compiler) } } // 04 掛載所有 webpack 內置的插件(入口) new WebpackOptionsApply().process(options, compiler); // 05 返回 compiler 對象即可 return compiler } module.exports = webpack 復制代碼
- 你細品,
youWebpack里有require了 Compiler、NodeEnvironmentPlugin、WebpackOptionsApply。 沒辦法,繼續得寫出來。
/* * WebpackOptionsApply.js */ const EntryOptionPlugin = require("./EntryOptionPlugin") class WebpackOptionsApply { process(options, compiler) { new EntryOptionPlugin().apply(compiler) compiler.hooks.entryOption.call(options.context, options.entry) } } module.exports = WebpackOptionsApply 復制代碼
/* * NodeEnvironmentPlugin.js */ const fs = require('fs'); // webpack為提升文件讀寫性能, 源碼里是對 node 的 fs 模塊進行了二次封裝的。我們這勉強夠用,就不封裝了。 /捂臉 class NodeEnvironmentPlugin { constructor(options) { this.options = options || {} } apply(complier) { complier.inputFileSystem = fs complier.outputFileSystem = fs } } module.exports = NodeEnvironmentPlugin 復制代碼
Compiler,webpack 核心之一的Compiler來了。
/* * Compiler */ const { Tapable, SyncHook, SyncBailHook, AsyncSeriesHook, AsyncParallelHook } = require('tapable') const path = require('path') const mkdirp = require('mkdirp') const Stats = require('./Stats') const NormalModuleFactory = require('./NormalModuleFactory') const Compilation = require('./Compilation') const { emit } = require('process') class Compiler extends Tapable { constructor(context) { super() this.context = context this.hooks = { done: new AsyncSeriesHook(["stats"]), entryOption: new SyncBailHook(["context", "entry"]), beforeRun: new AsyncSeriesHook(["compiler"]), run: new AsyncSeriesHook(["compiler"]), thisCompilation: new SyncHook(["compilation", "params"]), compilation: new SyncHook(["compilation", "params"]), beforeCompile: new AsyncSeriesHook(["params"]), compile: new SyncHook(["params"]), make: new AsyncParallelHook(["compilation"]), afterCompile: new AsyncSeriesHook(["compilation"]), emit: new AsyncSeriesHook(['compilation']) } } emitAssets(compilation, callback) { // 當前需要做的核心: 01 創建dist 02 在目錄創建完成之后執行文件的寫操作 // 01 定義一個工具方法用於執行文件的生成操作 const emitFlies = (err) => { const assets = compilation.assets let outputPath = this.options.output.path for (let file in assets) { let source = assets[file] let targetPath = path.posix.join(outputPath, file) this.outputFileSystem.writeFileSync(targetPath, source, 'utf8') } callback(err) } // 創建目錄之后啟動文件寫入 this.hooks.emit.callAsync(compilation, (err) => { mkdirp.sync(this.options.output.path) emitFlies() }) } run(callback) { console.log('run 方法執行了~~~~') const finalCallback = function (err, stats) { callback(err, stats) } const onCompiled = (err, compilation) => { // 最終在這里將處理好的 chunk 寫入到指定的文件然后輸出至 dist this.emitAssets(compilation, (err) => { let stats = new Stats(compilation) finalCallback(err, stats) }) } this.hooks.beforeRun.callAsync(this, (err) => { this.hooks.run.callAsync(this, (err) => { this.compile(onCompiled) }) }) } compile(callback) { const params = this.newCompilationParams() this.hooks.beforeRun.callAsync(params, (err) => { this.hooks.compile.call(params) const compilation = this.newCompilation(params) this.hooks.make.callAsync(compilation, (err) => { // console.log('make鈎子監聽觸發了~~~~~') // callback(err, compilation) // 在這里我們開始處理 chunk compilation.seal((err) => { this.hooks.afterCompile.callAsync(compilation, (err) => { callback(err, compilation) }) }) }) }) } newCompilationParams() { const params = { normalModuleFactory: new NormalModuleFactory() } return params } newCompilation(params) { const compilation = this.createCompilation() this.hooks.thisCompilation.call(compilation, params) this.hooks.compilation.call(compilation, params) return compilation } createCompilation() { return new Compilation(this) } } module.exports = Compiler 復制代碼
- You look. 你還得自己實現
NormalModuleFactory、Compilation、Stats
/* * Stats 其實看代碼就能明白,Stats只是將compilation身上掛載的 入口模塊、模塊內容、chunks、文件目錄等拿了出來。 這里可以回頭看看run 方法 */ class Stats { constructor(compilation) { this.entries = compilation.entries this.modules = compilation.modules this.chunks = compilation.chunks this.files = compilation.files } toJson() { return this } } module.exports = Stats 復制代碼
/* * NormalModuleFactory */ const NormalModule = require("./NormalModule"); class NormalModuleFactory { create(data) { return new NormalModule(data) } // 源碼里頭還實現了其它方法,所以這里不要嫌棄為什么又要單獨require一個 NormalModule } module.exports = NormalModuleFactory 復制代碼
webpack 核心之一 Compilation,至於它干嘛的?請你回頭看看Stats就明白了。
const ejs = require('ejs') const Chunk = require('./Chunk') const path = require('path') const async = require('neo-async') const Parser = require('./Parser') const NormalModuleFactory = require('./NormalModuleFactory') const { Tapable, SyncHook } = require('tapable') // 實例化一個 normalModuleFactory parser const normalModuleFactory = new NormalModuleFactory() const parser = new Parser() class Compilation extends Tapable { constructor(compiler) { super() this.compiler = compiler this.context = compiler.context this.options = compiler.options // 讓 compilation 具備文件的讀寫能力 this.inputFileSystem = compiler.inputFileSystem this.outputFileSystem = compiler.outputFileSystem this.entries = [] // 存入所有入口模塊的數組 this.modules = [] // 存放所有模塊的數據 this.chunks = [] // 存放當前次打包過程中所產出的 chunk this.assets = [] this.files = [] this.hooks = { succeedModule: new SyncHook(['module']), seal: new SyncHook(), beforeChunks: new SyncHook(), afterChunks: new SyncHook() } } /** * 完成模塊編譯操作 * @param {*} context 當前項目的根 * @param {*} entry 當前的入口的相對路徑 * @param {*} name chunkName main * @param {*} callback 回調 */ addEntry(context, entry, name, callback) { this._addModuleChain(context, entry, name, (err, module) => { callback(err, module) }) } _addModuleChain(context, entry, name, callback) { this.createModule({ parser, name: name, context: context, rawRequest: entry, resource: path.posix.join(context, entry), moduleId: './' + path.posix.relative(context, path.posix.join(context, entry)) }, (entryModule) => { this.entries.push(entryModule) }, callback) } /** * 定義一個創建模塊的方法,達到復用的目的 * @param {*} data 創建模塊時所需要的一些屬性值 * @param {*} doAddEntry 可選參數,在加載入口模塊的時候,將入口模塊的id 寫入 this.entries * @param {*} callback */ createModule(data, doAddEntry, callback) { let module = normalModuleFactory.create(data) const afterBuild = (err, module) => { // 在 afterBuild 當中我們就需要判斷一下,當前次module 加載完成之后是否需要處理依賴加載 if (module.dependencies.length > 0) { // 當前邏輯就表示module 有需要依賴加載的模塊,因此我們可以再單獨定義一個方法來實現 this.processDependencies(module, (err) => { callback(err, module) }) } else { callback(err, module) } } this.buildModule(module, afterBuild) // 當我們完成了本次的 build 操作之后將 module 進行保存 doAddEntry && doAddEntry(module) this.modules.push(module) } /** * 完成具體的 build 行為 * @param {*} module 當前需要被編譯的模塊 * @param {*} callback */ buildModule(module, callback) { module.build(this, (err) => { // 如果代碼走到這里就意味着當前 Module 的編譯完成了 this.hooks.succeedModule.call(module) callback(err, module) }) } processDependencies(module, callback) { // 1 當前的函數核心功能就是實現一個被依賴模塊的遞歸加載 // 2 加載模塊的思想都是創建一個模塊,然后想辦法將被加載模塊的內容拿進來? // 3 當前我們不知道 module 需要依賴幾個模塊, 此時我們需要想辦法讓所有的被依賴的模塊都加載完成之后再執行 callback?【 neo-async 】 let dependencies = module.dependencies async.forEach(dependencies, (dependency, done) => { this.createModule({ parser, name: dependency.name, context: dependency.context, rawRequest: dependency.rawRequest, moduleId: dependency.moduleId, resource: dependency.resource }, null, done) }, callback) } seal(callback) { this.hooks.seal.call() this.hooks.beforeChunks.call() // 1 當前所有的入口模塊都被存放在了 compilation 對象的 entries 數組里 // 2 所謂封裝 chunk 指的就是依據某個入口,然后找到它的所有依賴,將它們的源代碼放在一起,之后再做合並 for (const entryModule of this.) { // 核心: 創建模塊加載已有模塊的內容,同時記錄模塊信息 const chunk = new Chunk(entryModule) // 保存 chunk 信息 this.chunks.push(chunk) // 給 chunk 屬性賦值 chunk.modules = this.modules.filter(module => module.name === chunk.name) } // chunk 流程梳理之后就進入到 chunk 代碼處理環節(模板文件 + 模塊中的源代碼==》chunk.js) this.hooks.afterChunks.call(this.chunks) // 生成代碼內容 this.createChunkAssets() callback() } createChunkAssets() { for (let i = 0; i < this.chunks.length; i++) { const chunk = this.chunks[i] const fileName = chunk.name + '.js' chunk.files.push(fileName) // 1 獲取模板文件的路徑 let tempPath = path.posix.join(__dirname, 'temp/main.ejs') // 2 讀取模塊文件中的內容 let tempCode = this.inputFileSystem.readFileSync(tempPath, 'utf8') // 3 獲取渲染函數 let tempRender = ejs.compile(tempCode) // 4 按ejs的語法渲染數據 let source = tempRender({ entryModuleId: chunk.entryModule.moduleId, modules: chunk.modules }) // 輸出文件 this.emitAssets(fileName, source) } } emitAssets(fileName, source) { this.assets[fileName] = source this.files.push(fileName) } } module.exports = Compilation 復制代碼
還有部分就不寫了。 像 Parser、 Chunk。
其實,主要是為懶,寫得太累了, 我得去覓食了。好了,開玩笑。主要webpack整個編譯過程到這應該就完全明白了。。
小結一下
webpack編譯過程是啥? 代碼里應該體現得非常清楚了。
step1: 實例化compiler
- 實例化 compiler 對象
- 初始化 NodeEnvironmentPlugin(讓compiler具體文件讀寫能力)
- 掛載所有 plugins 插件至 compiler 對象身上
- 掛載所有 webpack 內置的插件(入口)
step2: compiler.run
- this.hooks.beforeRun.callAsync -> this.hooks.run.callAsync -> this.compile
-
this.compile 接收 onCompiled
-
onCompiled 內容是: 最終在這里將處理好的 chunk 寫入到指定的文件然后輸出至 dist (文件輸出路徑,不一定是dist)
-
step3: compile方法做的事情
- newCompilationParams,實例化Compilation對象之前先初始化其所需參數
- 調用this.hooks.beforeRun.callAsync
- this.newCompilation(params) 實例化Compilation對象
- this.hooks.make.callAsync 觸發make鈎子監聽
- compilation.seal 開始處理 chunk
- this.hooks.afterCompile.callAsync(compilation,...)
- 流程進入compilation了。。。
step4: 完成模塊編譯操作
- addEntry
- _addModuleChain
- createModule:定義一個創建模塊的方法,達到復用的目的
- module = normalModuleFactory.create(data) : 創建普通模塊,目的是用來加載js模塊
- afterBuild
- this.processDependencies : 找到模塊與模塊之間的依賴關系
- this.buildModule(module, afterBuild)
- module.build : 到這里就意味着當前 Module 的編譯完成了
- createModule:定義一個創建模塊的方法,達到復用的目的
- _addModuleChain
- seal: 生成代碼內容,輸出文件
Over.
作者:在剝我的殼
鏈接:https://juejin.cn/post/6972378623281987621
來源:掘金
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
