歡迎你們前往騰訊雲社區,獲取更多騰訊海量技術實踐乾貨哦~javascript
做者介紹:陳柏信,騰訊前端開發,目前主要負責手Q遊戲中心業務開發,以及項目相關的技術升級、架構優化等工做。html
webpack 是一個強大的模塊打包工具,之因此強大的一個緣由在於它擁有靈活、豐富的插件機制。可是 webpack 的文檔不太友好,就我的的學習經從來說,官方的文檔並不詳細,網上的學習資料又少有完整的概述和例子。因此,在研究了一段時間的 webpack 源碼以後,本身但願寫個系列文章,結合本身的實踐一塊兒來談談 webpack 插件這個主題,也但願可以幫助其餘人更全面地瞭解 webpack。前端
這篇文章是系列文章的第一篇,將會講述 webpack 的基本架構以及構建流程。vue
P.S. 如下的分析都基於 webpack 3.6.0java
webpack 的基本架構,是基於一種相似事件的方式。下面的代碼中,對象可使用 plugin
函數來註冊一個事件,暫時能夠理解爲咱們熟悉的 addEventListener
。但爲了區分概念,後續的討論中會將事件名稱爲 任務點,好比下面有四個任務點 compilation
, optimize
, compile
, before-resolve
:node
compiler.plugin("compilation", (compilation, callback) => { // 當Compilation實例生成時 compilation.plugin("optimize", () => { // 當全部modules和chunks已生成,開始優化時 }) }) compiler.plugin("compile", (params) => { // 當編譯器開始編譯模塊時 let nmf = params.normalModuleFactory nmf.plugin("before-resolve", (data) => { // 在factory開始解析模塊前 }) })
webpack 內部的大部分功能,都是經過這種註冊任務點的形式來實現的,這在後面中咱們很容易發現這一點。因此這裏直接拋出結論:webpack 的核心功能,是抽離成不少個內部插件來實現的。
那這些內部插件是如何對 webpack 產生做用的呢?在咱們開始運行 webpack 的時候,它會先建立一個 Compiler
實例,而後調用 WebpackOptionsApply
這個模塊給 Compiler
實例添加內部插件:webpack
// https://github.com/webpack/webpack/blob/master/lib/webpack.js#L37 compiler = new Compiler(); // 其餘代碼.. compiler.options = new WebpackOptionsApply().process(options, compiler);
在 WebpackOptionsApply
這個插件內部會根據咱們傳入的 webpack 配置來初始化須要的內部插件:git
// https://github.com/webpack/webpack/blob/master/lib/WebpackOptionsApply.js JsonpTemplatePlugin = require("./JsonpTemplatePlugin"); NodeSourcePlugin = require("./node/NodeSourcePlugin"); compiler.apply( new JsonpTemplatePlugin(options.output), new FunctionModulePlugin(options.output), new NodeSourcePlugin(options.node), new LoaderTargetPlugin(options.target) ); // 其餘代碼.. compiler.apply(new EntryOptionPlugin()); compiler.applyPluginsBailResult("entry-option", options.context, options.entry); compiler.apply( new CompatibilityPlugin(), new HarmonyModulesPlugin(options.module), new AMDPlugin(options.module, options.amd || {}), new CommonJsPlugin(options.module), new LoaderPlugin(), new NodeStuffPlugin(options.node), new RequireJsStuffPlugin(), new APIPlugin(), new ConstPlugin(), new UseStrictPlugin(), new RequireIncludePlugin(), new RequireEnsurePlugin(), new RequireContextPlugin(options.resolve.modules, options.resolve.extensions, options.resolve.mainFiles), new ImportPlugin(options.module), new SystemPlugin(options.module) );
每個內部插件,都是經過監放任務點的方式,來實現自定義的邏輯。好比 JsonpTemplatePlugin
這個插件,是經過監聽 mainTemplate
對象的 require-ensure
任務點,來生成 jsonp
風格的代碼:github
// https://github.com/webpack/webpack/blob/master/lib/JsonpTemplatePlugin.js mainTemplate.plugin("require-ensure", function(_, chunk, hash) { return this.asString([ "var installedChunkData = installedChunks[chunkId];", "if(installedChunkData === 0) {", this.indent([ "return new Promise(function(resolve) { resolve(); });" ]), "}", "", "// a Promise means \"currently loading\".", "if(installedChunkData) {", this.indent([ "return installedChunkData[2];" ]), "}", "", "// setup Promise in chunk cache", "var promise = new Promise(function(resolve, reject) {", this.indent([ "installedChunkData = installedChunks[chunkId] = [resolve, reject];" ]), "});", "installedChunkData[2] = promise;", "", "// start chunk loading", "var head = document.getElementsByTagName('head')[0];", this.applyPluginsWaterfall("jsonp-script", "", chunk, hash), "head.appendChild(script);", "", "return promise;" ]); });
如今咱們理解了 webpack 的基本架構以後,可能會產生疑問,每一個插件應該監聽哪一個對象的哪一個任務點,又如何對實現特定功能呢?web
要徹底解答這個問題很難,緣由在於 webpack 中構建過程當中,會涉及到很是多的對象和任務點,要對每一個對象和任務點都進行討論是很困難的。可是,咱們仍然能夠挑選完整構建流程中涉及到的幾個核心對象和任務點,把 webpack 的構建流程講清楚,當咱們須要實現某個特定內容的時候,再去找對應的模塊源碼查閱任務點。
那麼下面咱們就來聊一聊 webpack 的構建流程。
爲了更清楚和方便地討論構建流程,這裏按照我的理解整理了 webpack 構建流程中比較重要的幾個對象以及對應的任務點,而且按照構建順序畫出了流程圖:
圖中每一列頂部名稱表示該列中任務點所屬的對象
圖中每一行表示一個階段
圖中每一個節點表示任務點名稱
圖中每一個節點括號表示任務點的參數,參數帶有callback是異步任務點
圖中的箭頭表示任務點的執行順序
圖中虛線表示存在循環流程
上面展現的只是 webpack 構建的一部分,好比與 Module
相關的對象只畫出了 NormalModuleFactory
,與 Template
相關的對象也只畫出了 MainTemplate
等。緣由在於上面的流程圖已經足以說明主要的構建步驟,另外有沒畫出來的對象和任務點跟上述的相似,好比 ContextModuleFactory
跟 NormalModuleFactory
是十分類似的對象,也有類似的任務點。有興趣的同窗能夠自行拓展探索流程圖。*
流程圖中已經展現了一些核心任務點對應的對象以及觸發順序,可是咱們仍然不明白這些任務點有什麼含義。因此剩下的內容會詳細講解 webpack 一些任務點詳細的動做,按照我的理解將流程圖分紅了水平的三行,表示三個階段,分別是:
webpack的準備階段
modules和chunks的生成階段
文件生成階段
這個階段的主要工做,是建立 Compiler
和 Compilation
實例。
首先咱們從 webpack 的運行開始講起,在前面咱們大概地講過,當咱們開始運行 webpack 的時候,就會建立 Compiler
實例而且加載內部插件。這裏跟構建流程相關性比較大的內部插件是 EntryOptionPlugin
,咱們來看看它到底作了什麼:
// https://github.com/webpack/webpack/blob/master/lib/WebpackOptionsApply.js compiler.apply(new EntryOptionPlugin()); compiler.applyPluginsBailResult("entry-option", options.context, options.entry); // 立刻觸發任務點運行 EntryOptionPlugin 內部邏輯 // https://github.com/webpack/webpack/blob/master/lib/EntryOptionPlugin.js module.exports = class EntryOptionPlugin { apply(compiler) { compiler.plugin("entry-option", (context, entry) => { if(typeof entry === "string" || Array.isArray(entry)) { compiler.apply(itemToPlugin(context, entry, "main")); } else if(typeof entry === "object") { Object.keys(entry).forEach(name => compiler.apply(itemToPlugin(context, entry[name], name))); } else if(typeof entry === "function") { compiler.apply(new DynamicEntryPlugin(context, entry)); } return true; }); } };
EntryOptionPlugin
的代碼只有寥寥數行可是很是重要,它會解析傳給 webpack 的配置中的 entry 屬性,而後生成不一樣的插件應用到 Compiler
實例上。這些插件多是 SingleEntryPlugin
, MultiEntryPlugin
或者 DynamicEntryPlugin
。但不論是哪一個插件,內部都會監聽 Compiler
實例對象的 make
任務點,以 SingleEntryPlugin
爲例:
// https://github.com/webpack/webpack/blob/master/lib/SingleEntryPlugin.js class SingleEntryPlugin { // 其餘代碼.. apply(compiler) { // 其餘代碼.. compiler.plugin("make", (compilation, callback) => { const dep = SingleEntryPlugin.createDependency(this.entry, this.name); compilation.addEntry(this.context, dep, this.name, callback); }); } }
這裏的 make
任務點將成爲後面解析 modules 和 chunks 的起點。
除了 EntryOptionPlugin
,其餘的內部插件也會監聽特定的任務點來完成特定的邏輯,但咱們這裏再也不仔細討論。當 Compiler
實例加載完內部插件以後,下一步就會直接調用 compiler.run
方法來啓動構建,任務點 run
也是在此時觸發,值得注意的是此時基本只有 options
屬性是解析完成的:
// 監放任務點 run compiler.plugin("run", (compiler, callback) => { console.log(compiler.options) // 能夠看到解析後的配置 callback() })
另外要注意的一點是,任務點 run
只有在 webpack 以正常模式運行的狀況下會觸發,若是咱們以監聽(watch)的模式運行 webpack,那麼任務點 run
是不會觸發的,可是會觸發任務點 watch-run
。
接下來, Compiler
對象會開始實例化兩個核心的工廠對象,分別是 NormalModuleFactory
和 ContextModuleFactory
。工廠對象顧名思義就是用來建立實例的,它們後續用來建立 NormalModule
以及 ContextModule
實例,這兩個工廠對象會在任務點 compile
觸發時傳遞過去,因此任務點 compile
是間接監聽這兩個對象的任務點的一個入口:
// 監放任務點 compile compiler.plugin("compile", (params) => { let nmf = params.normalModuleFactory nmf.plugin("before-resolve", (data, callback) => { // ... }) })
下一步 Compiler
實例將會開始建立 Compilation
對象,這個對象是後續構建流程中最核心最重要的對象,它包含了一次構建過程當中全部的數據。也就是說一次構建過程對應一個 Compilation
實例。在建立 Compilation
實例時會觸發任務點 compilaiion
和 this-compilation
:
// https://github.com/webpack/webpack/blob/master/lib/Compiler.js class Compiler extends Tapable { // 其餘代碼.. newCompilation(params) { const compilation = this.createCompilation(); compilation.fileTimestamps = this.fileTimestamps; compilation.contextTimestamps = this.contextTimestamps; compilation.name = this.name; compilation.records = this.records; compilation.compilationDependencies = params.compilationDependencies; this.applyPlugins("this-compilation", compilation, params); this.applyPlugins("compilation", compilation, params); return compilation; } }
這裏爲何會有 compilation
和 this-compilation
兩個任務點?實際上是跟子編譯器有關,Compiler
實例經過 createChildCompiler
方法能夠建立子編譯器實例 childCompiler
,建立時 childCompiler
會複製 compiler
實例的任務點監聽器。任務點 compilation
的監聽器會被複制,而任務點 this-compilation
的監聽器不會被複制。 更多關於子編譯器的內容,將在下一篇文章中討論。
compilation
和 this-compilation
是最快可以獲取到 Compilation
實例的任務點,若是你的插件功能須要儘早對 Compilation
實例進行一些操做,那麼這兩個任務點是首選:
// 監聽 this-compilation 任務點 compiler.plugin("this-compilation", (compilation, params) => { console.log(compilation.options === compiler.options) // true console.log(compilation.compiler === compiler) // true console.log(compilation) })
當 Compilation
實例建立完成以後,webpack 的準備階段已經完成,下一步將開始 modules 和 chunks 的生成階段。
**這個階段的主要內容,是先解析項目依賴的全部 modules,再根據 modules 生成 chunks。
module 解析,包含了三個主要步驟:建立實例、loaders應用以及依賴收集。
chunks 生成,主要步驟是找到 chunk 所須要包含的 modules。**
當上一個階段完成以後,下一個任務點 make
將被觸發,此時內部插件 SingleEntryPlugin
, MultiEntryPlugin
, DynamicEntryPlugin
的監聽器會開始執行。監聽器都會調用 Compilation
實例的 addEntry
方法,該方法將會觸發第一批 module 的解析,這些 module 就是 entry 中配置的模塊。
咱們先講一個 module 解析完成以後的操做,它會遞歸調用它所依賴的 modules 進行解析,因此當解析中止時,咱們就可以獲得項目中全部依賴的 modules,它們將存儲在 Compilation
實例的 modules
屬性中,並觸發任務點 finish-modules
:
// 監聽 finish-modules 任務點 compiler.plugin("this-compilation", (compilation) => { compilation.plugin("finish-modules", (modules) => { console.log(modules === compilation.modules) // true modules.forEach(module => { console.log(module._source.source()) // 處理後的源碼 }) }) })
下面將以 NormalModule
爲例講解一下 module 的解析過程,ContextModule
等其餘模塊實例的處理是相似的。
第一個步驟是建立 NormalModule
實例。這裏須要用到上一個階段講到的 NormalModuleFactory
實例, NormalModuleFactory
的 create
方法是建立 NormalModule
實例的入口,內部的主要過程是解析 module 須要用到的一些屬性,好比須要用到的 loaders
, 資源路徑 resource
等等,最終將解析完畢的參數傳給 NormalModule
構建函數直接實例化:
// https://github.com/webpack/webpack/blob/master/lib/NormalModuleFactory.js // 以 require("raw-loader!./a") 爲例 // 而且對 .js 後綴配置了 babel-loader createdModule = new NormalModule( result.request, // <raw-loader>!<babel-loader>!/path/to/a.js result.userRequest, // <raw-loader>!/path/to/a.js result.rawRequest, // raw-loader!./a.js result.loaders, // [<raw-loader>, <babel-loader>] result.resource, // /path/to/a.js result.parser );
這裏在解析參數的過程當中,有兩個比較實用的任務點 before-resolve
和 after-resolve
,分別對應瞭解析參數前和解析參數後的時間點。舉個例子,在任務點 before-resolve
能夠作到忽略某個 module 的解析,webpack 內部插件 IgnorePlugin
就是這麼作的:
// https://github.com/webpack/webpack/blob/master/lib/IgnorePlugin.js class IgnorePlugin { checkIgnore(result, callback) { // check if result is ignored if(this.checkResult(result)) { return callback(); // callback第二個參數爲 undefined 時會終止module解析 } return callback(null, result); } apply(compiler) { compiler.plugin("normal-module-factory", (nmf) => { nmf.plugin("before-resolve", this.checkIgnore); }); compiler.plugin("context-module-factory", (cmf) => { cmf.plugin("before-resolve", this.checkIgnore); }); } }
在建立完 NormalModule
實例以後會調用 build
方法繼續進行內部的構建。咱們熟悉的 loaders 將會在這裏開始應用,NormalModule
實例中的 loaders
屬性已經記錄了該模塊須要應用的 loaders。應用 loaders 的過程相對簡單,直接調用loader-runner 這個模塊便可:
// https://github.com/webpack/webpack/blob/master/lib/NormalModule.js const runLoaders = require("loader-runner").runLoaders; // 其餘代碼.. class NormalModule extends Module { // 其餘代碼.. doBuild(options, compilation, resolver, fs, callback) { this.cacheable = false; const loaderContext = this.createLoaderContext(resolver, options, compilation, fs); runLoaders({ resource: this.resource, loaders: this.loaders, context: loaderContext, readResource: fs.readFile.bind(fs) }, (err, result) => { // 其餘代碼.. }); } }
webpack 中要求 NormalModule
最終都是 js 模塊,因此 loader 的做用之一是將不一樣的資源文件轉化成 js 模塊。好比 html-loader
是將 html 轉化成一個 js 模塊。在應用完 loaders 以後,NormalModule
實例的源碼必然就是 js 代碼,這對下一個步驟很重要。
下一步咱們須要獲得這個 module 所依賴的其餘模塊,因此就有一個依賴收集的過程。webpack 的依賴收集過程是將 js 源碼傳給 js parser(webpack 使用的 parser 是 acorn):
// https://github.com/webpack/webpack/blob/master/lib/NormalModule.js class NormalModule extends Module { // 其餘代碼.. build(options, compilation, resolver, fs, callback) { // 其餘代碼.. return this.doBuild(options, compilation, resolver, fs, (err) => { // 其餘代碼.. try { this.parser.parse(this._source.source(), { current: this, module: this, compilation: compilation, options: options }); } catch(e) { const source = this._source.source(); const error = new ModuleParseError(this, source, e); this.markModuleAsErrored(error); return callback(); } return callback(); }); } }
parser 將 js 源碼解析後獲得對應的AST(抽象語法樹, Abstract Syntax Tree)。而後 webpack 會遍歷 AST,按照必定規則觸發任務點。 好比 js 源碼中有一個表達式:a.b.c
,那麼 parser
對象就會觸發任務點 expression a.b.c
。更多相關的規則 webpack 在官網有羅列出來,你們能夠對照着使用。
有了AST對應的任務點,依賴收集就相對簡單了,好比遇到任務點 call require
,說明在代碼中是有調用了require
函數,那麼就應該給 module 添加新的依賴。webpack 關於這部分的處理是比較複雜的,由於 webpack 要兼容多種不一樣的依賴方式,好比 AMD 規範、CommonJS規範,而後還要區分動態引用的狀況,好比使用了 require.ensure
, require.context
。但這些細節對於咱們討論構建流程並非必須的,由於不展開細節討論。
當 parser
解析完成以後,module 的解析過程就完成了。每一個 module 解析完成以後,都會觸發 Compilation
實例對象的任務點 succeed-module
,咱們能夠在這個任務點獲取到剛解析完的 module 對象。正如前面所說,module 接下來還要繼續遞歸解析它的依賴模塊,最終咱們會獲得項目所依賴的全部 modules。此時任務點 make
結束。
繼續往下走,Compialtion
實例的 seal
方法會被調用並立刻觸發任務點 seal
。在這個任務點,咱們能夠拿到全部解析完成的 module:
// 監聽 seal 任務點 compiler.plugin("this-compilation", (compilation) => { console.log(compilation.modules.length === 0) // true compilation.plugin("seal", () => { console.log(compilation.modules.length > 0) // true }) })
有了全部的 modules 以後,webpack 會開始生成 chunks。webpack 中的 chunk 概念,要不就是配置在 entry 中的模塊,要不就是動態引入(好比 require.ensure
)的模塊。這些 chunk 對象是 webpack 生成最終文件的一個重要依據。
每一個 chunk 的生成就是找到須要包含的 modules。這裏大體描述一下 chunk 的生成算法:
webpack 先將 entry 中對應的 module 都生成一個新的 chunk
遍歷 module 的依賴列表,將依賴的 module 也加入到 chunk 中
若是一個依賴 module 是動態引入的模塊,那麼就會根據這個 module 建立一個新的 chunk,繼續遍歷依賴
重複上面的過程,直至獲得全部的 chunks
在全部 chunks 生成以後,webpack 會對 chunks 和 modules 進行一些優化相關的操做,好比分配id、排序等,而且觸發一系列相關的任務點:
// https://github.com/webpack/webpack/blob/master/lib/Compilation.js class Compilation extends Tapable { // 其餘代碼 .. seal(callback) { // 生成 chunks 代碼.. self.applyPlugins0("optimize"); while(self.applyPluginsBailResult1("optimize-modules-basic", self.modules) || self.applyPluginsBailResult1("optimize-modules", self.modules) || self.applyPluginsBailResult1("optimize-modules-advanced", self.modules)) { /* empty */ } self.applyPlugins1("after-optimize-modules", self.modules); while(self.applyPluginsBailResult1("optimize-chunks-basic", self.chunks) || self.applyPluginsBailResult1("optimize-chunks", self.chunks) || self.applyPluginsBailResult1("optimize-chunks-advanced", self.chunks)) { /* empty */ } self.applyPlugins1("after-optimize-chunks", self.chunks); self.applyPluginsAsyncSeries("optimize-tree", self.chunks, self.modules, function sealPart2(err) { if(err) { return callback(err); } self.applyPlugins2("after-optimize-tree", self.chunks, self.modules); while(self.applyPluginsBailResult("optimize-chunk-modules-basic", self.chunks, self.modules) || self.applyPluginsBailResult("optimize-chunk-modules", self.chunks, self.modules) || self.applyPluginsBailResult("optimize-chunk-modules-advanced", self.chunks, self.modules)) { /* empty */ } self.applyPlugins2("after-optimize-chunk-modules", self.chunks, self.modules); const shouldRecord = self.applyPluginsBailResult("should-record") !== false; self.applyPlugins2("revive-modules", self.modules, self.records); self.applyPlugins1("optimize-module-order", self.modules); self.applyPlugins1("advanced-optimize-module-order", self.modules); self.applyPlugins1("before-module-ids", self.modules); self.applyPlugins1("module-ids", self.modules); self.applyModuleIds(); self.applyPlugins1("optimize-module-ids", self.modules); self.applyPlugins1("after-optimize-module-ids", self.modules); self.sortItemsWithModuleIds(); self.applyPlugins2("revive-chunks", self.chunks, self.records); self.applyPlugins1("optimize-chunk-order", self.chunks); self.applyPlugins1("before-chunk-ids", self.chunks); self.applyChunkIds(); self.applyPlugins1("optimize-chunk-ids", self.chunks); self.applyPlugins1("after-optimize-chunk-ids", self.chunks); // 其餘代碼.. }) } }
這些任務點通常是 webpack.optimize
屬性下的插件會使用到,好比 CommonsChunkPlugin
會使用到任務點 optimize-chunks
,但這裏咱們不深刻討論。
至此,modules 和 chunks 的生成階段結束。接下來是文件生成階段。
本文來源於 小時光茶社 微信公衆號
玩轉webpack(一)下篇:webpack的基本架構和構建流程
Webpack + vue 之抽離 CSS 的正確姿式
apt 與 JavaPoet 自動生成代碼