經過前面幾張的鋪墊,下面開始分析webpack源碼核心流程,大致上能夠分爲初始化,編譯,輸出三個階段,下面開始分析webpack
這個階段總體流程作了什麼? 啓動構建,讀取與合併配置參數,加載 Plugin,實例化 Compiler。
//經過yargs得到shell中的參數 yargs.parse(process.argv.slice(2), (err, argv, output) => { //把webpack.config.js中的參數和shell參數整合到options對象上 let options; options = require("./convert-argv")(argv); function processOptions(options) { const firstOptions = [].concat(options)[0]; const webpack = require("webpack"); let compiler; //經過webpack方法建立compile對象,Compiler 負責文件監聽和啓動編譯。 //Compiler 實例中包含了完整的 Webpack 配置,全局只有一個 Compiler 實例。 compiler = webpack(options); if (firstOptions.watch || options.watch) { compiler.watch(watchOptions, compilerCallback); //啓動一次新的編譯。 } else compiler.run(compilerCallback); } processOptions(options); });
說明 從源碼中摘取了初始化的的第一步,作了簡化,當運行webpack命令的的時候,運行的是webpack-cli下webpack.js,其內容是一個自執行函數,上面是執行的第一步,進行參數的解析合併處理,並建立compiler實例,而後啓動編譯運行run方法,其中關鍵步驟 compiler = webpack(options); 詳細展開以下所示web
const webpack = (options, callback) => { //參數合法性校驗 const webpackOptionsValidationErrors = validateSchema( webpackOptionsSchema, options ); let compiler; if (Array.isArray(options)) { compiler = new MultiCompiler(options.map(options => webpack(options))); } else if (typeof options === "object") { options = new WebpackOptionsDefaulter().process(options); //建立compiler對象 compiler = new Compiler(options.context); compiler.options = options; new NodeEnvironmentPlugin().apply(compiler); //註冊配置文件中的插件,依次調用插件的 apply 方法,讓插件能夠監聽後續的全部事件節點。同時給插件傳入 compiler 實例的引用,以方便插件經過 compiler 調用 Webpack 提供的 API。 if (options.plugins && Array.isArray(options.plugins)) { for (const plugin of options.plugins) { plugin.apply(compiler); } } //開始應用 Node.js 風格的文件系統到 compiler 對象,以方便後續的文件尋找和讀取。 compiler.hooks.environment.call(); compiler.hooks.afterEnvironment.call(); //註冊內部插件 compiler.options = new WebpackOptionsApply().process(options, compiler); } return compiler; };
說明 註冊插件過程不在展開,webpack內置插件真的不少啊shell
這個階段總體流程作了什麼? 從 Entry 發出,針對每一個 Module 串行調用對應的 Loader 去翻譯文件內容,再找到該 Module 依賴的 Module,遞歸地進行編譯處理。
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方法開始,開始執行編譯流程,run方法觸發了before-run、run兩個事件,而後經過readRecords讀取文件,經過compile進行打包,該方法中實例化了一個Compilation類segmentfault
compile(callback) { const params = this.newCompilationParams(); this.hooks.beforeCompile.callAsync(params, err => { if (err) return callback(err); this.hooks.compile.call(params); // 每編譯一次都會建立一個compilation對象(好比watch 文件時,一改動就會執行),可是compile只會建立一次 const compilation = this.newCompilation(params); // make事件觸發了 事件會觸發SingleEntryPlugin監聽函數,調用compilation.addEntry方法 this.hooks.make.callAsync(compilation, err => { if (err) return callback(err); }); }); }
說明 打包時觸發before-compile、compile、make等事件,同時建立很是重要的compilation對象,內部有聲明瞭不少鉤子,初始化模板等等app
this.hooks = { buildModule: new SyncHook(["module"]), seal: new SyncHook([]), optimize: new SyncHook([]), }; //拼接最終生成代碼的主模板會用到 this.mainTemplate = new MainTemplate(this.outputOptions); //拼接最終生成代碼的chunk模板會用到 this.chunkTemplate = new ChunkTemplate(this.outputOptions); //拼接最終生成代碼的熱更新模板會用到 this.hotUpdateChunkTemplate = new HotUpdateChunkTemplate()
//監聽comple的make hooks事件,經過內部的 SingleEntryPlugin 從入口文件開始執行編譯 compiler.hooks.make.tapAsync( "SingleEntryPlugin", (compilation, callback) => { const { entry, name, context } = this; const dep = SingleEntryPlugin.createDependency(entry, name); compilation.addEntry(context, dep, name, callback); } );
說明 監聽compile的make hooks事件,經過內部的 SingleEntryPlugin 從入口文件開始執行編譯,調用compilation.addEntry方法,根據模塊的類型獲取對應的模塊工廠並建立模塊,開始構建模塊async
doBuild(options, compilation, resolver, fs, callback) { const loaderContext = this.createLoaderContext( resolver, options, compilation, fs ); //調用loader處理模塊 runLoaders( { resource: this.resource, loaders: this.loaders, context: loaderContext, readResource: fs.readFile.bind(fs) }, (err, result) => { const resourceBuffer = result.resourceBuffer; const source = result.result[0]; const sourceMap = result.result.length >= 1 ? result.result[1] : null; const extraInfo = result.result.length >= 2 ? result.result[2] : null; this._source = this.createSource( this.binary ? asBuffer(source) : asString(source), resourceBuffer, sourceMap ); //loader處理完以後 獲得_source 而後ast接着處理 this._ast = typeof extraInfo === "object" && extraInfo !== null && extraInfo.webpackAST !== undefined ? extraInfo.webpackAST : null; return callback(); } ); }
說明 SingleEntryPlugin這個內存插件主要做用是從entry讀取文件,根據文件類型和配置的 Loader 執行runLoaders,而後將loader處理後的文件經過acorn抽象成抽象語法樹AST,遍歷AST,構建該模塊的全部依賴。函數
這個階段總體流程作了什麼? 把編譯後的 Module 組合成 Chunk,把 Chunk 轉換成文件,輸出到文件系統。
//全部依賴build完成,開始對chunk進行優化(抽取公共模塊、加hash等) compilation.seal(err => { if (err) return callback(err); this.hooks.afterCompile.callAsync(compilation, err => { if (err) return callback(err); return callback(null, compilation); }); });
說明 compilation.seal主要是對chunk進行優化,生成編譯後的源碼,比較重要,詳細展開以下所示工具
//代碼生成前面優化 this.hooks.optimize.call(); this.hooks.optimizeTree.callAsync(this.chunks, this.modules, err => { this.hooks.beforeHash.call(); this.createHash(); this.hooks.afterHash.call(); if (shouldRecord) this.hooks.recordHash.call(this.records); this.hooks.beforeModuleAssets.call(); this.createModuleAssets(); if (this.hooks.shouldGenerateChunkAssets.call() !== false) { this.hooks.beforeChunkAssets.call(); //生成最終打包輸出的chunk資源,根據template文件,詳細步驟以下所示 this.createChunkAssets(); } }); -------------------------------------- //取出最後文件須要的模板 const template = chunk.hasRuntime() ? this.mainTemplate : this.chunkTemplate; //經過模板最終生成webpack_require格式的內容,他這個是內部封裝的拼接渲染邏輯,也沒用什麼ejs,handlebar等這些模板工具 source = fileManifest.render(); //生成的資源保存在compilation.assets,方便下一步emitAssets步驟中,把文件輸出到硬盤 this.assets[file] = source;
//把處理好的assets輸出到output的path中 emitAssets(compilation, callback) { let outputPath; const emitFiles = err => { if (err) return callback(err); asyncLib.forEach( compilation.assets, (source, file, callback) => { const writeOut = err => { //輸出打包後的文件到配置中指定的目錄下 this.outputFileSystem.writeFile(targetPath, content, callback); }; writeOut(); } ); }; this.hooks.emit.callAsync(compilation, err => { if (err) return callback(err); outputPath = compilation.getPath(this.outputPath); this.outputFileSystem.mkdirp(outputPath, emitFiles); }); }
若是單獨看這篇文章的話,理解起來會比較困難,推薦一下與之相關的系列鋪墊文章,上面是我對webpack源碼運行流程的總結, 整個流程已經跑通了,不過還有蠻多點值得深刻挖掘的。清明在家宅了3天,過得好快,明天公司組織去奧森公園尋寶行動,期待ing 。優化
推薦
webpack源碼之tapable
webpack源碼之plugin機制
webpack源碼之ast簡介
webpack源碼之loader機制ui
參考源碼webpack: "4.4.1"webpack-cli: "2.0.13"