原文發佈在個人 GitHub 歡迎 star
收藏。webpack
本文源碼來源:webpack-4git
在開始編譯流程以前,webpack
會處理用戶配置,首先進行校驗,經過後與默認配置合併或者是調用相應函數進行處理,輸出最終的 options
。以後實例化 Compiler
,並傳入 options
,併爲註冊的 plugins
注入 compiler
實例。若是配置了 watch
選項,則添加監聽,在資源變化的時候會從新進行編譯流程;不然直接進入編譯流程:github
const webpack = (options, callback) => { // 校驗 validateSchema(webpackOptionsSchema, options); /* ... */ // 處理、合併 options options = new WebpackOptionsDefaulter().process(options); // 實例化 Compiler const compiler = new Compiler(options.context); /* ... */ // 註冊插件 if (Array.isArray(options.plugins)) { for (const plugin of options.plugins) { if (typeof plugin === "function") { plugin.call(compiler, compiler); } else { plugin.apply(compiler); } } } // 開始編譯流程 if (watch) { compiler.watch(watchOptions, callback); } else { compiler.run((err, stats) => { compiler.close(err2 => { callback(err || err2, stats); }); }); } }
在 compiler.run()
的過程當中,會調用 compile
方法。在 compile
方法中會實例化 Compilation
,模塊的編譯構建流程都由 Compilation
控制。實例化 Compilation
後緊接着觸發 compilation
和 make
事件,從 make
開始主編譯流程。這裏有一個很細節的點,其實也與 webpack
的插件機制有關,webpack
的插件機制當然讓其有很好的擴展性,但對於閱讀源碼來講會把人繞暈,衆多的鉤子,註冊鉤子函數的代碼與觸發的地方很難讓人找到其聯繫之處,只能經過編輯器的全局搜索去找到註冊的鉤子函數。在處理 options
的時候,會調用到 EntryPlugin
插件,其內部會註冊 compilation
與 make
事件:web
apply(compiler) { compiler.hooks.compilation.tap( "EntryPlugin", (compilation, { normalModuleFactory }) => { // 設置 DependencyFactories,在添加 moduleChain 的時候會用上 compilation.dependencyFactories.set( EntryDependency, normalModuleFactory ); } ); compiler.hooks.make.tapAsync("EntryPlugin", (compilation, callback) => { const { entry, name, context } = this; const dep = EntryPlugin.createDependency(entry, name); // 編譯文件模塊的入口 compilation.addEntry(context, dep, name, err => { callback(err); }); }); }
在 make
事件觸發後,會調用 compilation.addEntry
從入口文件開始,根據 dep
類型判斷使用哪種 moduleFactory
,而且將入口添加到 _preparedEntrypoints
屬性中,其結構以下:數組
const slot = { name: name, // entry 的 name request: null, // entry 的 request module: null // 對 entry 文件解析成的最終 module,在 afterBuild 中對其賦值 }; // ... this._preparedEntrypoints.push(slot);
調用 _addModuleChian
將模塊加入編譯鏈條,會傳入一個 dependency
對象,存有 entry
的 name
和 request
等信息。request
中,存儲的是文件的路徑信息:app
addEntry(context, entry, name, callback) { this.hooks.addEntry.call(entry, name); // ... this._addModuleChain( context, entry, module => { this.entries.push(module); }, (err, module) => { // ... } ); }
在 _addModuleChain
的回調中會調用到 xxxModuleFactory
的 create
方法。以 normalModuleFactory
的 create
方法爲例,先是觸發 beforeResolve
事件,而後觸發 normalModuleFactory
的 factory
事件,並調用返回的 factory
方法:async
create(data, callback) { // ... this.hooks.beforeResolve.callAsync({ ... }, (err, result) => { // ... const factory = this.hooks.factory.call(null); // ... factory(result, (err, module) => { // ... }); } ); }
在 factory
方法中,會觸發 resolver
事件,並返回一個函數,經過調用此函數執行文件路徑及相關 loader
路徑的解析,完成後觸發 afterResolve
事件,並在其回調中生成一個 module
實例,並將 resolve
的結果存入其中:編輯器
// factory 事件註冊 this.hooks.factory.tap("NormalModuleFactory", () => (result, callback) => { let resolver = this.hooks.resolver.call(null); // ... resolver(result, (err, data) => { // ... this.hooks.afterResolve.callAsync(data, (err, result) => { // ... let createdModule = this.hooks.createModule.call(result); if (!createdModule) { if (!result.request) { return callback(new Error("Empty dependency (no request)")); } // normalModule 實例 createdModule = new NormalModule(result); } createdModule = this.hooks.module.call(createdModule, result); return callback(null, createdModule); }); }); }); // resolve 事件註冊 this.hooks.resolver.tap("NormalModuleFactory", () => (data, callback) => { // ... // loader 和 normal 文件的 resolve 有所區別 const loaderResolver = this.getResolver("loader"); const normalResolver = this.getResolver("normal", data.resolveOptions); // resolve loader & normal path });
回到 normalModuleFactory.create
的 callback
中,會依次執行 addModule
---> addReason
---> buildModule
---> afterBuild
。函數
module
實例添加到全局 Compilation.modules
數組中和 _modules
對象中;module
添加 reason
,便是哪一個 module
依賴了該 module
;module
;module
;調用 buildModule
,最終會走到相應 module
實例的 build
方法中,前面咱們的 module
是 NormalModule
的實例,因此咱們來看看 NormalModule
的 build
方法:ui
// NormalModule 的 build 方法 build(options, compilation, resolver, fs, callback) { // ... return this.doBuild(options, compilation, resolver, fs, err => { // doBuild 回調 // ... try { // 進行 AST 的轉換 const result = this.parser.parse( // 若是在 runLoaders 的時候已經解析成 AST 則使用 _ast,不然傳入 JS 源代碼 this._ast || this._source.source(), { current: this, module: this, compilation: compilation, options: options }, (err, result) => { if (err) handleParseError(err); else handleParseResult(result); } ); if (result !== undefined) handleParseResult(result); } catch (e) { handleParseError(e); } }); } // doBuild 方法 doBuild(options, compilation, resolver, fs, callback) { // ... runLoaders( { resource: this.resource, loaders: this.loaders, context: loaderContext, readResource: fs.readFile.bind(fs) }, (err, result) => { // ... // createSource 將 loaders 處理的結果轉換爲字符串 this._source = this.createSource( this.binary ? asBuffer(source) : asString(source), resourceBuffer, sourceMap ); this._sourceSize = null; // 若是已經轉換爲 AST,則存在 _ast 中 this._ast = typeof extraInfo === "object" && extraInfo !== null && extraInfo.webpackAST !== undefined ? extraInfo.webpackAST : null; return callback(); } ); }
在 duBuild
方法中,會進行 loaders
的轉換。在 this.parser.parse
方法中,會將轉換後的源碼進行 AST
轉換,並在 import/export
等地方添加相應的 Dependency
,也能夠理解爲某種佔位符,在後續的解析中,會根據相應的模板等進行替換生成最終文件。buildModule
結束後,執行其回調,並調用 afterBuild
方法,在 afterBuild
方法中,調用了 processModuleDependencies
方法處理依賴:
processModuleDependencies(module, callback) { const dependencies = new Map(); // 省略對 dependencies 作的一些處理 const sortedDependencies = []; // 將依賴解析成下面的結構 for (const pair1 of dependencies) { for (const pair2 of pair1[1]) { sortedDependencies.push({ factory: pair1[0], dependencies: pair2[1] }); } } // 添加模塊依賴的解析 this.addModuleDependencies( module, sortedDependencies, this.bail, null, true, callback ); } // addModuleDependencies 遍歷依賴,並從新進行 module 的編譯 addModuleDependencies(module, dependencies, bail, cacheGroup, recursive, callback) { const start = this.profile && Date.now(); const currentProfile = this.profile && {}; asyncLib.forEach( dependencies, (item, callback) => { const dependencies = item.dependencies; // ... semaphore.acquire(() => { const factory = item.factory; // create factory.create( { // ... }, (err, dependentModule) => { // ... const iterationDependencies = depend => { for (let index = 0; index < depend.length; index++) { // ... // addReason dependentModule.addReason(module, dep); } }; // addModule const addModuleResult = this.addModule( dependentModule, cacheGroup ); // ... if (addModuleResult.build) { // buildModule this.buildModule( dependentModule, isOptional(), module, dependencies, err => { // ... // afterBuild afterBuild(); } ); } // ... } ); }); }, err => { // ... } ); }
能夠看到,在 addModuleDependencies
方法中,對前一個 module
的依賴進行遍歷,又從新執行 factory.create
---> addModule
---> buildModule
---> afterBuild
,進行編譯。當全部的依賴都編譯完,便完成了 module
的編譯過程。回到 make
事件的回調中,此時全部須要編譯的 module
都已經通過上面的步驟處理完畢,接下來會調用 compilation.seal
進行 chunk
圖的生成工做:
this.hooks.make.callAsync(compilation, err => { if (err) return callback(err); compilation.finish(err => { if (err) return callback(err); // seal 階段,組合 chunk,生成 chunkGroup compilation.seal(err => { if (err) return callback(err); // 在其回調中觸發 afterCompile 事件,編譯過程結束 this.hooks.afterCompile.callAsync(compilation, err => { if (err) return callback(err); return callback(null, compilation); }); }); }); });
未完待續...