您好,登錄后才能下訂單哦!
作為模塊加載和打包神器,只需配置幾個文件,加載各種 loader 就可以享受無痛流程化開發(fā)。但對于 webpack 這樣一個復雜度較高的插件集合,它的整體流程及思想對我們來說還是很透明的。
本文旨在搞清楚從命令行下敲下 webpack 命令,或者配置 npm script 后執(zhí)行 package.json 中的命令,到工程目錄下出現(xiàn)打包的后的 bundle 文件的過程中,webpack都替我們做了哪些工作。
測試用webpack版本為 webpack@3.4.1
webpack.config.js中定義好相關配置,包括 entry、output、module、plugins等,命令行執(zhí)行 webpack 命令,webpack 便會根據(jù)配置文件中的配置進行打包處理文件,并生成最后打包后的文件。
命令行執(zhí)行 webpack 時,如果全局命令行中未找到webpack命令的話,執(zhí)行本地的node-modules/bin/webpack.js 文件。
在bin/webpack.js中使用 yargs庫 解析了命令行的參數(shù),處理了 webpack 的配置對象 options,調(diào)用 processOptions()
函數(shù)。
// 處理編譯相關,核心函數(shù) function processOptions(options) { // promise風格的處理,暫時還沒遇到這種情況的配置 if(typeof options.then === "function") {...} // 處理傳入的options為數(shù)組的情況 var firstOptions = [].concat(options)[0]; var statsPresetToOptions = require("../lib/Stats.js").presetToOptions; // 設置輸出的options var outputOptions = options.stats; if(typeof outputOptions === "boolean" || typeof outputOptions === "string") { outputOptions = statsPresetToOptions(outputOptions); } else if(!outputOptions) { outputOptions = {}; } // 處理各種現(xiàn)實相關的參數(shù) ifArg("display", function(preset) { outputOptions = statsPresetToOptions(preset); }); ... // 引入lib下的webpack.js,入口文件 var webpack = require("../lib/webpack.js"); // 設置最大錯誤追蹤堆棧 Error.stackTraceLimit = 30; var lastHash = null; var compiler; try { // 編譯,這里是關鍵,需要進入lib/webpack.js文件查看 compiler = webpack(options); } catch(e) { // 錯誤處理 var WebpackOptionsValidationError = require("../lib/WebpackOptionsValidationError"); if(e instanceof WebpackOptionsValidationError) { if(argv.color) console.error("\u001b[1m\u001b[31m" + e.message + "\u001b[39m\u001b[22m"); else console.error(e.message); process.exit(1); // eslint-disable-line no-process-exit } throw e; } // 顯示相關參數(shù)處理 if(argv.progress) { var ProgressPlugin = require("../lib/ProgressPlugin"); compiler.apply(new ProgressPlugin({ profile: argv.profile })); } // 編譯完后的回調(diào)函數(shù) function compilerCallback(err, stats) {} // watch模式下的處理 if(firstOptions.watch || options.watch) { var watchOptions = firstOptions.watchOptions || firstOptions.watch || options.watch || {}; if(watchOptions.stdin) { process.stdin.on("end", function() { process.exit(0); // eslint-disable-line }); process.stdin.resume(); } compiler.watch(watchOptions, compilerCallback); console.log("\nWebpack is watching the files…\n"); } else // 調(diào)用run()函數(shù),正式進入編譯過程 compiler.run(compilerCallback); }
如下圖所示,lib/webpack.js 中的關鍵函數(shù)為 webpack,其中定義了編譯相關的一些操作。
"use strict"; const Compiler = require("./Compiler"); const MultiCompiler = require("./MultiCompiler"); const NodeEnvironmentPlugin = require("./node/NodeEnvironmentPlugin"); const WebpackOptionsApply = require("./WebpackOptionsApply"); const WebpackOptionsDefaulter = require("./WebpackOptionsDefaulter"); const validateSchema = require("./validateSchema"); const WebpackOptionsValidationError = require("./WebpackOptionsValidationError"); const webpackOptionsSchema = require("../schemas/webpackOptionsSchema.json"); // 核心方法,調(diào)用該方法,返回Compiler的實例對象compiler function webpack(options, callback) {...} exports = module.exports = webpack; // 設置webpack對象的常用屬性 webpack.WebpackOptionsDefaulter = WebpackOptionsDefaulter; webpack.WebpackOptionsApply = WebpackOptionsApply; webpack.Compiler = Compiler; webpack.MultiCompiler = MultiCompiler; webpack.NodeEnvironmentPlugin = NodeEnvironmentPlugin; webpack.validate = validateSchema.bind(this, webpackOptionsSchema); webpack.validateSchema = validateSchema; webpack.WebpackOptionsValidationError = WebpackOptionsValidationError; // 對外暴露一些插件 function exportPlugins(obj, mappings) {...} exportPlugins(exports, {...}); exportPlugins(exports.optimize = {}, {...});
接下來看在webpack函數(shù)中主要定義了哪些操作
// 核心方法,調(diào)用該方法,返回Compiler的實例對象compiler function webpack(options, callback) { // 驗證是否符合格式 const webpackOptionsValidationErrors = validateSchema(webpackOptionsSchema, options); if(webpackOptionsValidationErrors.length) { throw new WebpackOptionsValidationError(webpackOptionsValidationErrors); } let compiler; // 傳入的options為數(shù)組的情況,調(diào)用MultiCompiler進行處理,目前還沒遇到過這種情況的配置 if(Array.isArray(options)) { compiler = new MultiCompiler(options.map(options => webpack(options))); } else if(typeof options === "object") { // 配置options的默認參數(shù) new WebpackOptionsDefaulter().process(options); // 初始化一個Compiler的實例 compiler = new Compiler(); // 設置context的默認值為進程的當前目錄,絕對路徑 compiler.context = options.context; // 定義compiler的options屬性 compiler.options = options; // Node環(huán)境插件,其中設置compiler的inputFileSystem,outputFileSystem,watchFileSystem,并定義了before-run的鉤子函數(shù) new NodeEnvironmentPlugin().apply(compiler); // 應用每個插件 if(options.plugins && Array.isArray(options.plugins)) { compiler.apply.apply(compiler, options.plugins); } // 調(diào)用environment插件 compiler.applyPlugins("environment"); // 調(diào)用after-environment插件 compiler.applyPlugins("after-environment"); // 處理compiler對象,調(diào)用一些必備插件 compiler.options = new WebpackOptionsApply().process(options, compiler); } else { throw new Error("Invalid argument: options"); } if(callback) { if(typeof callback !== "function") throw new Error("Invalid argument: callback"); if(options.watch === true || (Array.isArray(options) && options.some(o => o.watch))) { const watchOptions = Array.isArray(options) ? options.map(o => o.watchOptions || {}) : (options.watchOptions || {}); return compiler.watch(watchOptions, callback); } compiler.run(callback); } return compiler; }
webpack函數(shù)中主要做了以下兩個操作,
compiler.apply
的方式調(diào)用了一些必備插件,在這些插件中,注冊了一些 plugins,在后面的編譯過程中,通過調(diào)用一些插件的方式,去處理一些流程。run()調(diào)用
run函數(shù)中主要觸發(fā)了before-run事件,在before-run事件的回調(diào)函數(shù)中觸發(fā)了run事件,run事件中調(diào)用了readRecord函數(shù)讀取文件,并調(diào)用compile()
函數(shù)進行編譯。
compile()調(diào)用
compile函數(shù)中定義了編譯的相關流程,主要有以下流程:
compile函數(shù)的定義
compile(callback) { // 創(chuàng)建編譯參數(shù),包括模塊工廠和編譯依賴參數(shù)數(shù)組 const params = this.newCompilationParams(); // 觸發(fā)before-compile 事件,開始整個編譯過程 this.applyPluginsAsync("before-compile", params, err => { if(err) return callback(err); // 觸發(fā)compile事件 this.applyPlugins("compile", params); // 構建compilation對象,compilation對象負責具體的編譯細節(jié) const compilation = this.newCompilation(params); // 觸發(fā)make事件,對應的監(jiān)聽make事件的回調(diào)函數(shù)在不同的EntryPlugin中注冊,比如singleEntryPlugin this.applyPluginsParallel("make", compilation, err => { if(err) return callback(err); compilation.finish(); compilation.seal(err => { if(err) return callback(err); this.applyPluginsAsync("after-compile", compilation, err => { if(err) return callback(err); return callback(null, compilation); }); }); }); }); }
【問題】make 事件觸發(fā)后,有哪些插件中注冊了make事件并得到了運行的機會呢?
以單入口entry配置為例,在EntryOptionPlugin插件中定義了,不同配置的入口應該調(diào)用何種插件進行解析。不同配置的入口插件中注冊了對應的 make 事件回調(diào)函數(shù),在make事件觸發(fā)后被調(diào)用。
如下所示:
一個插件的apply方法是一個插件的核心方法,當說一個插件被調(diào)用時主要是其apply方法被調(diào)用。
EntryOptionPlugin 插件在webpackOptionsApply中被調(diào)用,其內(nèi)部定義了使用何種插件來解析入口文件。
const SingleEntryPlugin = require("./SingleEntryPlugin"); const MultiEntryPlugin = require("./MultiEntryPlugin"); const DynamicEntryPlugin = require("./DynamicEntryPlugin"); module.exports = class EntryOptionPlugin { apply(compiler) { compiler.plugin("entry-option", (context, entry) => { function itemToPlugin(item, name) { if(Array.isArray(item)) { return new MultiEntryPlugin(context, item, name); } else { return new SingleEntryPlugin(context, item, name); } } // 判斷entry字段的類型去調(diào)用不同的入口插件去處理 if(typeof entry === "string" || Array.isArray(entry)) { compiler.apply(itemToPlugin(entry, "main")); } else if(typeof entry === "object") { Object.keys(entry).forEach(name => compiler.apply(itemToPlugin(entry[name], name))); } else if(typeof entry === "function") { compiler.apply(new DynamicEntryPlugin(context, entry)); } return true; }); } };
entry-option 事件被觸發(fā)時,EntryOptionPlugin 插件做了這幾個事情:
判斷入口的類型,通過 entry 字段來判斷,對應了 entry 字段為 string object function的三種情況
每種不同的類型調(diào)用不同的插件去處理入口的配置。大致處理邏輯如下:
【問題】entry-option 事件是在什么時機被觸發(fā)的呢?
如下代碼所示,是在WebpackOptionsApply.js中,先調(diào)用處理入口的EntryOptionPlugin插件,然后觸發(fā) entry-option 事件,去調(diào)用不同類型的入口處理插件。
注意:調(diào)用插件的過程也就是一個注冊事件以及回調(diào)函數(shù)的過程。
WebpackOptionApply.js
// 調(diào)用處理入口entry的插件 compiler.apply(new EntryOptionPlugin()); compiler.applyPluginsBailResult("entry-option", options.context, options.entry);
前面說到,make事件觸發(fā)時,對應的回調(diào)邏輯都在不同配置入口的插件中注冊的。下面以SingleEntryPlugin為例,說明從 make 事件被觸發(fā),到編譯結束的整個過程。
SingleEntryPlugin.js
class SingleEntryPlugin { constructor(context, entry, name) { this.context = context; this.entry = entry; this.name = name; } apply(compiler) { // compilation 事件在初始化Compilation對象的時候被觸發(fā) compiler.plugin("compilation", (compilation, params) => { const normalModuleFactory = params.normalModuleFactory; compilation.dependencyFactories.set(SingleEntryDependency, normalModuleFactory); }); // make 事件在執(zhí)行compile的時候被觸發(fā) compiler.plugin("make", (compilation, callback) => { const dep = SingleEntryPlugin.createDependency(this.entry, this.name); // 編譯的關鍵,調(diào)用Compilation中的addEntry,添加入口,進入編譯過程。 compilation.addEntry(this.context, dep, this.name, callback); }); } static createDependency(entry, name) { const dep = new SingleEntryDependency(entry); dep.loc = name; return dep; } } module.exports = SingleEntryPlugin;
Compilation中負責具體編譯的細節(jié),包括如何創(chuàng)建模塊以及模塊的依賴,根據(jù)模板生成js等。如:addEntry,buildModule, processModuleDependencies等。
Compilation.js
addEntry(context, entry, name, callback) { const slot = { name: name, module: null }; this.preparedChunks.push(slot); // 添加該chunk上的module依賴 this._addModuleChain(context, entry, (module) => { entry.module = module; this.entries.push(module); module.issuer = null; }, (err, module) => { if(err) { return callback(err); } if(module) { slot.module = module; } else { const idx = this.preparedChunks.indexOf(slot); this.preparedChunks.splice(idx, 1); } return callback(null, module); }); }
_addModuleChain(context, dependency, onModule, callback) { const start = this.profile && Date.now(); ... // 根據(jù)模塊的類型獲取對應的模塊工廠并創(chuàng)建模塊 const moduleFactory = this.dependencyFactories.get(dependency.constructor); ... // 創(chuàng)建模塊,將創(chuàng)建好的模塊module作為參數(shù)傳遞給回調(diào)函數(shù) moduleFactory.create({ contextInfo: { issuer: "", compiler: this.compiler.name }, context: context, dependencies: [dependency] }, (err, module) => { if(err) { return errorAndCallback(new EntryModuleNotFoundError(err)); } let afterFactory; if(this.profile) { if(!module.profile) { module.profile = {}; } afterFactory = Date.now(); module.profile.factory = afterFactory - start; } const result = this.addModule(module); if(!result) { module = this.getModule(module); onModule(module); if(this.profile) { const afterBuilding = Date.now(); module.profile.building = afterBuilding - afterFactory; } return callback(null, module); } if(result instanceof Module) { if(this.profile) { result.profile = module.profile; } module = result; onModule(module); moduleReady.call(this); return; } onModule(module); // 構建模塊,包括調(diào)用loader處理文件,使用acorn生成AST,遍歷AST收集依賴 this.buildModule(module, false, null, null, (err) => { if(err) { return errorAndCallback(err); } if(this.profile) { const afterBuilding = Date.now(); module.profile.building = afterBuilding - afterFactory; } // 開始處理收集好的依賴 moduleReady.call(this); }); function moduleReady() { this.processModuleDependencies(module, err => { if(err) { return callback(err); } return callback(null, module); }); } }); }
_addModuleChain 主要做了以下幾件事情:
Compilation的 seal 函數(shù)在 make 事件的回調(diào)函數(shù)中進行了調(diào)用。
seal(callback) { const self = this; // 觸發(fā)seal事件,提供其他插件中seal的執(zhí)行時機 self.applyPlugins0("seal"); self.nextFreeModuleIndex = 0; self.nextFreeModuleIndex2 = 0; self.preparedChunks.forEach(preparedChunk => { const module = preparedChunk.module; // 將module保存在chunk的origins中,origins保存了module的信息 const chunk = self.addChunk(preparedChunk.name, module); // 創(chuàng)建一個entrypoint const entrypoint = self.entrypoints[chunk.name] = new Entrypoint(chunk.name); // 將chunk創(chuàng)建的chunk保存在entrypoint中,并將該entrypoint的實例保存在chunk的entrypoints中 entrypoint.unshiftChunk(chunk); // 將module保存在chunk的_modules數(shù)組中 chunk.addModule(module); // module實例上記錄chunk的信息 module.addChunk(chunk); // 定義該chunk的entryModule屬性 chunk.entryModule = module; self.assignIndex(module); self.assignDepth(module); self.processDependenciesBlockForChunk(module, chunk); }); self.sortModules(self.modules); 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); self.sortItemsWithChunkIds(); if(shouldRecord) self.applyPlugins2("record-modules", self.modules, self.records); if(shouldRecord) self.applyPlugins2("record-chunks", self.chunks, self.records); self.applyPlugins0("before-hash"); // 創(chuàng)建hash self.createHash(); self.applyPlugins0("after-hash"); if(shouldRecord) self.applyPlugins1("record-hash", self.records); self.applyPlugins0("before-module-assets"); self.createModuleAssets(); if(self.applyPluginsBailResult("should-generate-chunk-assets") !== false) { self.applyPlugins0("before-chunk-assets"); // 使用template創(chuàng)建最后的js代碼 self.createChunkAssets(); } self.applyPlugins1("additional-chunk-assets", self.chunks); self.summarizeDependencies(); if(shouldRecord) self.applyPlugins2("record", self, self.records); self.applyPluginsAsync("additional-assets", err => { if(err) { return callback(err); } self.applyPluginsAsync("optimize-chunk-assets", self.chunks, err => { if(err) { return callback(err); } self.applyPlugins1("after-optimize-chunk-assets", self.chunks); self.applyPluginsAsync("optimize-assets", self.assets, err => { if(err) { return callback(err); } self.applyPlugins1("after-optimize-assets", self.assets); if(self.applyPluginsBailResult("need-additional-seal")) { self.unseal(); return self.seal(callback); } return self.applyPluginsAsync("after-seal", callback); }); }); }); }); }
在 seal 中可以發(fā)現(xiàn),調(diào)用了很多不同的插件,主要就是操作chunk和module的一些插件,生成最后的源代碼。其中 createHash 用來生成hash,createChunkAssets 用來生成chunk的源碼,createModuleAssets 用來生成Module的源碼。在 createChunkAssets 中判斷了是否是入口chunk,入口的chunk用mainTemplate生成,否則用chunkTemplate生成。
在compiler中的 run 方法中定義了compile的回調(diào)函數(shù) onCompiled, 在編譯結束后,會調(diào)用該回調(diào)函數(shù)。在該回調(diào)函數(shù)中調(diào)用了 emitAsset,觸發(fā)了 emit 事件,將文件寫入到文件系統(tǒng)中的指定位置。
webpack的源碼通過采用Tapable控制其事件流,并通過plugin機制,在webpack構建過程中將一些事件鉤子暴露給plugin,使得開發(fā)者可以通過編寫相應的插件來自定義打包。
好了,以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學習或者工作具有一定的參考學習價值,如果有疑問大家可以留言交流,謝謝大家對億速云的支持。
參考文章:
細說 webpack 之流程篇
webpack 源碼解析
免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權內(nèi)容。