作為模塊加載和打包神器,只需配置幾個(gè)文件,加載各種 loader 就可以享受無痛流程化開發(fā)。但對(duì)于 webpack 這樣一個(gè)復(fù)雜度較高的插件集合,它的整體流程及思想對(duì)我們來說還是很透明的。
讓客戶滿意是我們工作的目標(biāo),不斷超越客戶的期望值來自于我們對(duì)這個(gè)行業(yè)的熱愛。我們立志把好的技術(shù)通過有效、簡單的方式提供給客戶,將通過不懈努力成為客戶在信息化領(lǐng)域值得信任、有價(jià)值的長期合作伙伴,公司提供的服務(wù)項(xiàng)目有:域名注冊、雅安服務(wù)器托管、營銷軟件、網(wǎng)站建設(shè)、芒市網(wǎng)站維護(hù)、網(wǎng)站推廣。
本文旨在搞清楚從命令行下敲下 webpack 命令,或者配置 npm script 后執(zhí)行 package.json 中的命令,到工程目錄下出現(xiàn)打包的后的 bundle 文件的過程中,webpack都替我們做了哪些工作。
測試用webpack版本為 webpack@3.4.1
webpack.config.js中定義好相關(guān)配置,包括 entry、output、module、plugins等,命令行執(zhí)行 webpack 命令,webpack 便會(huì)根據(jù)配置文件中的配置進(jìn)行打包處理文件,并生成最后打包后的文件。
命令行執(zhí)行 webpack 時(shí),如果全局命令行中未找到webpack命令的話,執(zhí)行本地的node-modules/bin/webpack.js 文件。
在bin/webpack.js中使用 yargs庫 解析了命令行的參數(shù),處理了 webpack 的配置對(duì)象 options,調(diào)用 processOptions()
函數(shù)。
// 處理編譯相關(guān),核心函數(shù) function processOptions(options) { // promise風(fēng)格的處理,暫時(shí)還沒遇到這種情況的配置 if(typeof options.then === "function") {...} // 處理傳入的options為數(shù)組的情況 var firstOptions = [].concat(options)[0]; var statsPresetToOptions = require("../lib/Stats.js").presetToOptions; // 設(shè)置輸出的options var outputOptions = options.stats; if(typeof outputOptions === "boolean" || typeof outputOptions === "string") { outputOptions = statsPresetToOptions(outputOptions); } else if(!outputOptions) { outputOptions = {}; } // 處理各種現(xiàn)實(shí)相關(guān)的參數(shù) ifArg("display", function(preset) { outputOptions = statsPresetToOptions(preset); }); ... // 引入lib下的webpack.js,入口文件 var webpack = require("../lib/webpack.js"); // 設(shè)置最大錯(cuò)誤追蹤堆棧 Error.stackTraceLimit = 30; var lastHash = null; var compiler; try { // 編譯,這里是關(guān)鍵,需要進(jìn)入lib/webpack.js文件查看 compiler = webpack(options); } catch(e) { // 錯(cuò)誤處理 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; } // 顯示相關(guān)參數(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ù),正式進(jìn)入編譯過程 compiler.run(compilerCallback); }
如下圖所示,lib/webpack.js 中的關(guān)鍵函數(shù)為 webpack,其中定義了編譯相關(guān)的一些操作。
"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的實(shí)例對(duì)象compiler function webpack(options, callback) {...} exports = module.exports = webpack; // 設(shè)置webpack對(duì)象的常用屬性 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; // 對(duì)外暴露一些插件 function exportPlugins(obj, mappings) {...} exportPlugins(exports, {...}); exportPlugins(exports.optimize = {}, {...});
接下來看在webpack函數(shù)中主要定義了哪些操作
// 核心方法,調(diào)用該方法,返回Compiler的實(shí)例對(duì)象compiler function webpack(options, callback) { // 驗(yàn)證是否符合格式 const webpackOptionsValidationErrors = validateSchema(webpackOptionsSchema, options); if(webpackOptionsValidationErrors.length) { throw new WebpackOptionsValidationError(webpackOptionsValidationErrors); } let compiler; // 傳入的options為數(shù)組的情況,調(diào)用MultiCompiler進(jìn)行處理,目前還沒遇到過這種情況的配置 if(Array.isArray(options)) { compiler = new MultiCompiler(options.map(options => webpack(options))); } else if(typeof options === "object") { // 配置options的默認(rèn)參數(shù) new WebpackOptionsDefaulter().process(options); // 初始化一個(gè)Compiler的實(shí)例 compiler = new Compiler(); // 設(shè)置context的默認(rèn)值為進(jìn)程的當(dāng)前目錄,絕對(duì)路徑 compiler.context = options.context; // 定義compiler的options屬性 compiler.options = options; // Node環(huán)境插件,其中設(shè)置compiler的inputFileSystem,outputFileSystem,watchFileSystem,并定義了before-run的鉤子函數(shù) new NodeEnvironmentPlugin().apply(compiler); // 應(yīng)用每個(gè)插件 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對(duì)象,調(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ù)中主要做了以下兩個(gè)操作,
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ù)進(jìn)行編譯。
compile()調(diào)用
compile函數(shù)中定義了編譯的相關(guān)流程,主要有以下流程:
compile函數(shù)的定義
compile(callback) { // 創(chuàng)建編譯參數(shù),包括模塊工廠和編譯依賴參數(shù)數(shù)組 const params = this.newCompilationParams(); // 觸發(fā)before-compile 事件,開始整個(gè)編譯過程 this.applyPluginsAsync("before-compile", params, err => { if(err) return callback(err); // 觸發(fā)compile事件 this.applyPlugins("compile", params); // 構(gòu)建compilation對(duì)象,compilation對(duì)象負(fù)責(zé)具體的編譯細(xì)節(jié) const compilation = this.newCompilation(params); // 觸發(fā)make事件,對(duì)應(yīng)的監(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事件并得到了運(yùn)行的機(jī)會(huì)呢?
以單入口entry配置為例,在EntryOptionPlugin插件中定義了,不同配置的入口應(yīng)該調(diào)用何種插件進(jìn)行解析。不同配置的入口插件中注冊了對(duì)應(yīng)的 make 事件回調(diào)函數(shù),在make事件觸發(fā)后被調(diào)用。
如下所示:
一個(gè)插件的apply方法是一個(gè)插件的核心方法,當(dāng)說一個(gè)插件被調(diào)用時(shí)主要是其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ā)時(shí),EntryOptionPlugin 插件做了這幾個(gè)事情:
判斷入口的類型,通過 entry 字段來判斷,對(duì)應(yīng)了 entry 字段為 string object function的三種情況
每種不同的類型調(diào)用不同的插件去處理入口的配置。大致處理邏輯如下:
【問題】entry-option 事件是在什么時(shí)機(jī)被觸發(fā)的呢?
如下代碼所示,是在WebpackOptionsApply.js中,先調(diào)用處理入口的EntryOptionPlugin插件,然后觸發(fā) entry-option 事件,去調(diào)用不同類型的入口處理插件。
注意:調(diào)用插件的過程也就是一個(gè)注冊事件以及回調(diào)函數(shù)的過程。
WebpackOptionApply.js
// 調(diào)用處理入口entry的插件 compiler.apply(new EntryOptionPlugin()); compiler.applyPluginsBailResult("entry-option", options.context, options.entry);
前面說到,make事件觸發(fā)時(shí),對(duì)應(yīng)的回調(diào)邏輯都在不同配置入口的插件中注冊的。下面以SingleEntryPlugin為例,說明從 make 事件被觸發(fā),到編譯結(jié)束的整個(gè)過程。
SingleEntryPlugin.js
class SingleEntryPlugin { constructor(context, entry, name) { this.context = context; this.entry = entry; this.name = name; } apply(compiler) { // compilation 事件在初始化Compilation對(duì)象的時(shí)候被觸發(fā) compiler.plugin("compilation", (compilation, params) => { const normalModuleFactory = params.normalModuleFactory; compilation.dependencyFactories.set(SingleEntryDependency, normalModuleFactory); }); // make 事件在執(zhí)行compile的時(shí)候被觸發(fā) compiler.plugin("make", (compilation, callback) => { const dep = SingleEntryPlugin.createDependency(this.entry, this.name); // 編譯的關(guān)鍵,調(diào)用Compilation中的addEntry,添加入口,進(jìn)入編譯過程。 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中負(fù)責(zé)具體編譯的細(xì)節(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ù)模塊的類型獲取對(duì)應(yīng)的模塊工廠并創(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); // 構(gòu)建模塊,包括調(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ù)中進(jìn)行了調(diào)用。
seal(callback) { const self = this; // 觸發(fā)seal事件,提供其他插件中seal的執(zhí)行時(shí)機(jī) 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)建一個(gè)entrypoint const entrypoint = self.entrypoints[chunk.name] = new Entrypoint(chunk.name); // 將chunk創(chuàng)建的chunk保存在entrypoint中,并將該entrypoint的實(shí)例保存在chunk的entrypoints中 entrypoint.unshiftChunk(chunk); // 將module保存在chunk的_modules數(shù)組中 chunk.addModule(module); // module實(shí)例上記錄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, 在編譯結(jié)束后,會(huì)調(diào)用該回調(diào)函數(shù)。在該回調(diào)函數(shù)中調(diào)用了 emitAsset,觸發(fā)了 emit 事件,將文件寫入到文件系統(tǒng)中的指定位置。
webpack的源碼通過采用Tapable控制其事件流,并通過plugin機(jī)制,在webpack構(gòu)建過程中將一些事件鉤子暴露給plugin,使得開發(fā)者可以通過編寫相應(yīng)的插件來自定義打包。
好了,以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,如果有疑問大家可以留言交流,謝謝大家對(duì)創(chuàng)新互聯(lián)的支持。
參考文章:
細(xì)說 webpack 之流程篇
webpack 源碼解析
網(wǎng)站欄目:webpack構(gòu)建的詳細(xì)流程探底
標(biāo)題網(wǎng)址:http://m.newbst.com/article4/jicpie.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供標(biāo)簽優(yōu)化、小程序開發(fā)、響應(yīng)式網(wǎng)站、Google、靜態(tài)網(wǎng)站、移動(dòng)網(wǎng)站建設(shè)
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場,如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來源: 創(chuàng)新互聯(lián)