文章首發於個人博客 https://github.com/mcuking/bl...
在開始分析源碼以前,筆者先把以前收集到的 webpack 構建流程圖貼在下面。後面的分析過程讀者能夠對照着這張圖來進行理解。css
回顧前面的文章,在 webpack-cli 從新調用 webpack 包時,首先執行的就是 node_module/webpack/lib/webpack.js
中的函數。以下:node
const webpack = (options, callback) => { ... let compiler; if (Array.isArray(options)) { compiler = new MultiCompiler( Array.from(options).map(options => webpack(options)) ); } else if (typeof options === "object") { // 檢查傳入的 options 並設置默認項 options = new WebpackOptionsDefaulter().process(options); // 初始化一個 compiler 對象實例 compiler = new Compiler(options.context); // 將 options 掛在到這個實例對象上 compiler.options = options; // 清理構建的緩存 new NodeEnvironmentPlugin({ infrastructureLogging: options.infrastructureLogging }).apply(compiler); // 遍歷 options.plugins 數組,將用戶配置的 plugins 所有初始化 // 並將插件內部業務邏輯綁定到 Compiler 實例對象上,等待實例對象觸發對應鉤子後執行 // 請參考上篇分析文章 if (options.plugins && Array.isArray(options.plugins)) { for (const plugin of options.plugins) { if (typeof plugin === "function") { plugin.call(compiler, compiler); } else { plugin.apply(compiler); } } } compiler.hooks.environment.call(); compiler.hooks.afterEnvironment.call(); // 根據 options 中配置的參數,實例化內部插件並綁定到 compiler 實例對象上 // 例如 externals 有配置,則須要配置 ExternalsPlugins compiler.options = new WebpackOptionsApply().process(options, compiler); } else { throw new Error("Invalid argument: options"); } ... if (callback) { ... compiler.run(callback); } return compiler; };
其中 WebpackOptionsDefaulter 這個類的做用就是檢測 options 並設置默認配置項,其關鍵代碼以下:webpack
class WebpackOptionsDefaulter extends OptionsDefaulter { constructor() { super(); this.set("entry", "./src"); this.set("devtool", "make", options => options.mode === "development" ? "eval" : false ); this.set("cache", "make", options => options.mode === "development"); this.set("context", process.cwd()); this.set("target", "web"); ... } }
set 方法和 process 方法都是繼承自父類 OptionsDefaulter,這裏就不贅述了。git
接着是初始化了一個 compiler 對象,並將處理好的 options 掛載到實例上。而後開始初始化 options.plugins 上的插件,將插件綁定到 compiler 實例對象上,若是 plugin 是函數,則直接調用;若是是對象,則調用對象上的 apply 方法。這也就爲何 webpack 的插件配置通常都是對象實例數組的緣由,以下:es6
{ plugins: [new HtmlWebpackPlugin()]; }
關於 webpack 插件機制的內容請參考上篇文章 Webpack 源碼分析(2)— Tapable 與 Webpack 的關聯。github
最後調用了一個名爲 WebpackOptionsApply 的類,咱們看下其實現的部分代碼:web
class WebpackOptionsApply extends OptionsApply { constructor() { super(); } process(options, compiler) { ... new EntryOptionPlugin().apply(compiler); compiler.hooks.entryOption.call(options.context, options.entry); ... if (options.externals) { ExternalsPlugin = require("webpack/lib/ExternalsPlugin"); new ExternalsPlugin( options.output.libraryTarget, options.externals ).apply(compiler); } ... } }
從中不難發現,WebpackOptionsApply 主要做用就是根據 options 中的設置,來掛載對應的插件到 compiler 實例對象上,例如若是設置了 externals,則掛載 ExternalsPlugin 插件。須要注意的是,有些插件是默認必需要掛載的,而不禁 options 中的設置決定,例如 EntryOptionPlugin 插件。算法
這裏咱們正好能夠到 EntryOptionPlugin 看下咱們在 options 裏常常設置的參數 entry 到底支持幾種類型,主要代碼以下:數組
const itemToPlugin = (context, item, name) => { if (Array.isArray(item)) { return new MultiEntryPlugin(context, item, name); } return new SingleEntryPlugin(context, item, name); }; module.exports = class EntryOptionPlugin { apply(compiler) { compiler.hooks.entryOption.tap('EntryOptionPlugin', (context, entry) => { if (typeof entry === 'string' || Array.isArray(entry)) { itemToPlugin(context, entry, 'main').apply(compiler); } else if (typeof entry === 'object') { for (const name of Object.keys(entry)) { itemToPlugin(context, entry[name], name).apply(compiler); } } else if (typeof entry === 'function') { new DynamicEntryPlugin(context, entry).apply(compiler); } return true; }); } };
有上面代碼咱們能夠知道 entry 能夠是字符串、數組、對象和函數,其中當是數組時,則掛載 MultiEntryPlugin 插件,也就是說 webpack 會將多個文件打包成一個文件。而當是對象時,則遍歷每一個鍵值對,而後執行 itemToPlugin 方法,也就是說 webpack 會將對象中的每一項入口對應的文件分別打包成不一樣的文件,這個就對應到了咱們常說的多頁面打包場景。緩存
到這裏是否是發現當看懂了源碼,就會對以前死記硬背的 webpack 配置有了更深刻的理解了呢?其實這就是閱讀源碼的一個很是棒的好處。
接下來則是調用了 compiler 對象的 run 方法,那麼咱們就回到 Compiler 文件中,進一步分析 Compiler 中到底作了哪些事情。
下面就是 Compiler 類的關鍵代碼:
class Compiler extends Tapable { constructor(context) { super(); this.hooks = { // 總共有 26 個鉤子,下面列舉的是比較常見的 run: new AsyncSeriesHook(["compiler"]), emit: new AsyncSeriesHook(["compilation"]), compilation: new SyncHook(["compilation", "params"]), compile: new SyncHook(["params"]), make: new AsyncParallelHook(["compilation"]), ... }, ... } watch(watchOptions, handler) { } run(callback) { const onCompiled = (err, compilation) => { ... }; this.hooks.beforeRun.callAsync(this, err => { if (err) return finalCallback(err); this.hooks.run.callAsync(this, err => { if (err) return finalCallback(err); this.readRecords(err => { if (err) return finalCallback(err); this.compile(onCompiled); }); }); }); } ... emitAssets(compilation, callback) { } ... createCompilation() { return new Compilation(this); } 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.hooks.thisCompilation.call(compilation, params); this.hooks.compilation.call(compilation, params); return compilation; } createNormalModuleFactory() { const normalModuleFactory = new NormalModuleFactory( this.options.context, this.resolverFactory, this.options.module || {} ); this.hooks.normalModuleFactory.call(normalModuleFactory); return normalModuleFactory; } newCompilationParams() { const params = { normalModuleFactory: this.createNormalModuleFactory(), contextModuleFactory: this.createContextModuleFactory(), compilationDependencies: new Set() }; return params; } ... compile(callback) { const params = this.newCompilationParams(); this.hooks.beforeCompile.callAsync(params, err => { if (err) return callback(err); this.hooks.compile.call(params); const compilation = this.newCompilation(params); this.hooks.make.callAsync(compilation, err => { if (err) return callback(err); compilation.finish(err => { if (err) return callback(err); compilation.seal(err => { if (err) return callback(err); this.hooks.afterCompile.callAsync(compilation, err => { if (err) return callback(err); return callback(null, compilation); }); }); }); }); }); } }
在分析這塊源碼前,咱們先明確下 webpack 兩個核心概念: Compiler 和 Compilation。
./lib/Compiler.js
): webpack 的主要引擎,在 compiler 對象記錄了完整的 webpack 環境信息,在 webpack 從啓動到結束,compiler 只會生成一次。你能夠在 compiler 對象上讀取到 webpack config 信息,outputPath 等;./lib/Compilation.js
):表明了一次單一的版本構建和生成資源。compilation 編譯做業能夠屢次執行,好比 webpack 工做在 watch 模式下,每次監測到源文件發生變化時,都會從新實例化一個 compilation 對象。一個 compilation 對象表現了當前的模塊資源、編譯生成資源、變化的文件、以及被跟蹤依賴的狀態信息。Compiler 表明的是不變的 webpack 環境; Compilation 表明的是一次編譯做業,每一次的編譯均可能不一樣。
單獨截取 run 方法以下:
run(callback) { const onCompiled = (err, compilation) => { ... if (this.hooks.shouldEmit.call(compilation) === false) { const stats = new Stats(compilation); stats.startTime = startTime; stats.endTime = Date.now(); this.hooks.done.callAsync(stats, err => { if (err) return finalCallback(err); return finalCallback(null, stats); }); return; } ... }; this.hooks.beforeRun.callAsync(this, err => { if (err) return finalCallback(err); this.hooks.run.callAsync(this, err => { if (err) return finalCallback(err); this.readRecords(err => { if (err) return finalCallback(err); this.compile(onCompiled); }); }); }); }
在 run 函數裏,首先觸發了一些鉤子:beforeRun -> run -> done
,並在觸發 run 鉤子的時候,執行了 this.compile 方法。那麼咱們就去看下這個 compile 方法具體作了些什麼。
首先截取 compile 方法關鍵代碼:
compile(callback) { const params = this.newCompilationParams(); this.hooks.beforeCompile.callAsync(params, err => { if (err) return callback(err); this.hooks.compile.call(params); const compilation = this.newCompilation(params); this.hooks.make.callAsync(compilation, err => { if (err) return callback(err); compilation.finish(err => { if (err) return callback(err); compilation.seal(err => { if (err) return callback(err); this.hooks.afterCompile.callAsync(compilation, err => { if (err) return callback(err); return callback(null, compilation); }); }); }); }); }); }
代碼中初始化了一個 compilation 實例對象,另外和 run 方法一些,compile 也觸發一系列鉤子:beforeCompile -> compile -> make -> afterCompile
。
其中根據最上面的流程圖,在 make 鉤子階段,webpack 開始了真正的對模塊的編譯。那麼咱們看下到底什麼邏輯訂閱了 make 鉤子。經過全局搜索 hooks.make.tapAsync
,咱們能夠看到 SingleEntryPlugin、MultiEntryPlugin、DllEntryPlugin、DynamicEntryPlugin 等插件中都訂閱了 make 鉤子。
那麼咱們先進入 SingleEntryPlugin 文件(./lib/SingleEntryPlugin.js
)中查看,關鍵代碼以下:
class SingleEntryPlugin { constructor(context, entry, name) { this.context = context; this.entry = entry; this.name = name; } apply(compiler) { ... compiler.hooks.make.tapAsync( "SingleEntryPlugin", (compilation, callback) => { const { entry, name, context } = this; const dep = SingleEntryPlugin.createDependency(entry, name); compilation.addEntry(context, dep, name, callback); } ); } ... }
其中主要調用了 compilation.addEntry 方法,繼續查看 compilation.addEntry(./lib/Compilation.js
)。
addEntry(context, entry, name, callback) { this.hooks.addEntry.call(entry, name); ... this._addModuleChain( context, entry, module => { this.entries.push(module); }, (err, module) => { ... if (module) { slot.module = module; } else { const idx = this._preparedEntrypoints.indexOf(slot); if (idx >= 0) { this._preparedEntrypoints.splice(idx, 1); } } this.hooks.succeedEntry.call(entry, name, module); return callback(null, module); } ); }
經過上面代碼咱們能夠看到 addEntry 又調用了 _addModuleChain,後面調用我就不在這裏展現代碼了,直接把調用棧列出來,感興趣的同窗能夠自行查看源碼。調用棧以下:
this.addEntry -> this._addModuleChain -> this.addModule -> this.buidModule -> module.build
addEntry 的做用是將模塊的入口信息傳遞給模塊鏈中,即 addModuleChain,隨後繼續調用 compiliation.factorizeModule,這些調用最後會將 entry 的入口信息」翻譯「成一個模塊(嚴格上說,模塊通常是 NormalModule 實例化後的對象)。
當模塊開始構建時,會觸發 buidModule 鉤子。下面是 buidModule 方法的關鍵代碼,其中 module.build 執行成功後,會觸發 succeedModule 鉤子,若是失敗則觸發 failedModule 鉤子。
buildModule(module, optional, origin, dependencies, thisCallback) { ... this.hooks.buildModule.call(module); module.build( this.options, this, this.resolverFactory.get("normal", module.resolveOptions), this.inputFileSystem, error => { ... const originalMap = module.dependencies.reduce((map, v, i) => { map.set(v, i); return map; }, new Map()); module.dependencies.sort((a, b) => { const cmp = compareLocations(a.loc, b.loc); if (cmp) return cmp; return originalMap.get(a) - originalMap.get(b); }); if (error) { this.hooks.failedModule.call(module, error); return callback(error); } this.hooks.succeedModule.call(module); return callback(); } ); }
那麼 module 又是從哪裏來的?從 Compilation.js 代碼中咱們能夠知道這個是 Module 類的實例,其中又具體分爲 NormalModule、ExternalModule、MutiModule、DelegatedModule 等。
咱們先進入到常見的 NormalModule 中查看源碼(文件地址 ./lib/NormalModule.js
)。nomalModule.build 又調用了自身的 nomalModule.doBuild 方法
doBuild(options, compilation, resolver, fs, callback) { ... runLoaders( { resource: this.resource, loaders: this.loaders, context: loaderContext, readResource: fs.readFile.bind(fs) }, (err, result) => {...} ) }
nomalModule.doBuild 方法又調用了 runLoaders 方法來調用對應的 loader 對模塊進行編譯,最終會經過 loader 的組合將全部模塊(css,less,jpg 等)編譯成標準的 js 模塊。
寫過 webpack loader 童鞋應該對 runLoader 比較熟悉,這個能夠獨立運行 webpack loader,而無需安裝整個 webpack,對於調試 webpack loader 很方便。
模塊構建完成以後,在 normalModule.doBuild 方法的最後一個參數即回調函數中,會使用 acorn 的 parse 方法將構建後的標準 js 模塊內容轉換成 AST 語法樹,經過其中的 require 語句來找到這個模塊所依賴的其餘模塊,而後將該模塊也添加到依賴列表中,最後遍歷依賴列表依次去構建。總結來講就是不斷的分析模塊的依賴和不斷的構建模塊,直到全部涉及到的模塊都構建完成。
// 將 js 模塊轉成 AST 語法書,並分析該模塊因此來的模塊 const result = this.parser.parse(source); ...
當全部模塊都構建完成,會存放在 Compilation 對象的 modules 數組屬性中。構建成功後會觸發 succeedModule 鉤子,不然會觸發 failedModule 鉤子。到此模塊構建(make)階段就結束了。
模塊構建完成後,就會調用 Compilation 對象上的 seal 方法,該方法主要是觸發 seal 鉤子,開始對模塊構建結果進行不少的優化操做,其中就包含了基於 module 生成 chunk 的邏輯。下面是 chunk 生成的算法:
生成 chunk 以後,接下來還會調用 Compilation.createHash 方法爲文件生成 hash,例如 js 通常設置 chunkHash,css 通常設置 contentHash 等。
文件 hash 建立完成以後,則會調用 createModuleAssets 方法將上個階段構建傳出來的標準 js 模塊,放在 Compilation 的 assets 對象屬性上去,key 是文件名,value 是構建後的模塊內容。到此優化階段就結束了。
優化階段完成以後,就會進入到文件生成階段。主要是在 Compiler 中觸發 emit 鉤子,調用 compilation.getPath 獲取到文件輸出的目錄,而後將生成的文件寫到磁盤對應的目錄中。
到此爲止,Webpack 的構建過程就完成了。經歷了整個源碼解讀過程,相信讀者對 Webpack 的理解會更加深刻了。