摘要:引言通過前面幾張的鋪墊下面開始分析源碼核心流程大體上可以分為初始化編譯輸出三個階段下面開始分析初始化這個階段整體流程做了什么啟動構建,讀取與合并配置參數,加載,實例化。推薦源碼之源碼之機制源碼之簡介源碼之機制參考源碼
引言
通過前面幾張的鋪墊,下面開始分析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); 詳細展開如下所示
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內置插件真的很多啊
編譯這個階段整體流程做了什么? 從 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類
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對象,內部有聲明了很多鉤子,初始化模板等等
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方法,根據模塊的類型獲取對應的模塊工廠并創建模塊,開始構建模塊
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機制
參考源碼
webpack: "4.4.1"
webpack-cli: "2.0.13"
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/94025.html
摘要:流程劃分縱觀整個打包過程,可以流程劃分為四塊。核心類關系圖功能實現模塊通過將源碼解析為樹并拆分,以及直至基礎模塊。通過的依賴和切割文件構建出含有和包含關系的對象。通過模版完成主入口文件的寫入,模版完成切割文件的寫入。 前言 插件plugin,webpack重要的組成部分。它以事件流的方式讓用戶可以直接接觸到webpack的整個編譯過程。plugin在編譯的關鍵地方觸發對應的事件,極大的...
摘要:源碼分析安裝好包,根據上述方法,我們運行如下命令初始化在構造函數處打上斷點,可以看到繼承自,上面定義了一個函數。因為函數定義在原型上,并通過在構造函數中賦值。 Webpack源碼閱讀之Tapable webpack采用Tapable來進行流程控制,在這套體系上,內部近百個插件有條不紊,還能支持外部開發自定義插件來擴展功能,所以在閱讀webpack源碼前先了解Tapable的機制是很有必...
摘要:用于對模塊的源代碼進行轉換。甚至允許你直接在模塊中文件和區別之前一篇文章中介紹了機制和今天研究的對象他們兩者在一起極大的拓展了的功能。原理說明上面是源碼中執行關鍵步驟遞歸的方式執行執行機流程似于中間件機制參考源碼參考文檔 loader概念 loader是用來加載處理各種形式的資源,本質上是一個函數, 接受文件作為參數,返回轉化后的結構。 loader 用于對模塊的源代碼進行轉換。loa...
摘要:下面一步步拆解上述流程。切換至分支檢測本地和暫存區是否還有未提交的文件檢測本地分支是否有誤檢測本地分支是否落后遠程分支發布發布檢測到在分支上沒有沖突后,立即執行。 背景 最近一直在著手做一個與業務強相關的組件庫,一直在思考要從哪里下手,怎么來設計這個組件庫,因為業務上一直在使用ElementUI(以下簡稱Element),于是想參考了一下Element組件庫的設計,看看Element構...
閱讀 3596·2020-12-03 17:42
閱讀 2768·2019-08-30 15:54
閱讀 2223·2019-08-30 15:44
閱讀 571·2019-08-30 14:08
閱讀 970·2019-08-30 14:00
閱讀 1103·2019-08-30 13:46
閱讀 2784·2019-08-29 18:33
閱讀 2886·2019-08-29 14:11