目前,幾乎所有業務的開發建置都會用到 webpack 。所以下面這篇文章主要給大家介紹了關於webpack構建的詳細流程的相關資料,文中透過範例程式碼介紹的非常詳細,對大家的學習或工作具有一定的參考學習價值,需要的朋友們下面來一起看看吧。
作為模組加載和打包神器,只需配置幾個文件,加載各種 loader 就可以享受無痛流程化開發。但對於 webpack 這樣一個複雜度較高的插件集合,它的整體流程及想法對我們來說還是很透明的。
本文旨在搞清楚從命令列下敲下webpack 命令,或者配置npm script 後執行package.json 中的命令,到工程目錄下出現打包的後的bundle 文件的過程中,webpack都替我們做了哪些工作。
測試用webpack版本為webpack@3.4.1
webpack.config.js中定義好相關配置,包括entry、output、module、plugins等,指令列執行webpack 指令,webpack便會根據設定檔中的配置進行打包處理文件,並產生最後打包後的文件。
命令列執行 webpack 時,如果全域命令列中找不到webpack命令的話,執行本地的node-modules/bin/webpack.js 檔案。
在bin/webpack.js中使用 yargs函式庫 解析了命令列的參數,處理了 webpack 的設定物件 options,呼叫 processOptions()
函式。
// 处理编译相关,核心函数 function processOptions(options) { // promise风格的处理,暂时还没遇到这种情况的配置 if(typeof options.then === "function") {...} // 处理传入的options为数组的情况 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 = {}; } // 处理各种现实相关的参数 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; } // 显示相关参数处理 if(argv.progress) { var ProgressPlugin = require("../lib/ProgressPlugin"); compiler.apply(new ProgressPlugin({ profile: argv.profile })); } // 编译完后的回调函数 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 // 调用run()函数,正式进入编译过程 compiler.run(compilerCallback); }
如下圖所示,lib/webpack.js 中的關鍵函數為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"); // 核心方法,调用该方法,返回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函數中主要定義了哪些操作
// 核心方法,调用该方法,返回Compiler的实例对象compiler function webpack(options, callback) { // 验证是否符合格式 const webpackOptionsValidationErrors = validateSchema(webpackOptionsSchema, options); if(webpackOptionsValidationErrors.length) { throw new WebpackOptionsValidationError(webpackOptionsValidationErrors); } let compiler; // 传入的options为数组的情况,调用MultiCompiler进行处理,目前还没遇到过这种情况的配置 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(); // 设置context的默认值为进程的当前目录,绝对路径 compiler.context = options.context; // 定义compiler的options属性 compiler.options = options; // Node环境插件,其中设置compiler的inputFileSystem,outputFileSystem,watchFileSystem,并定义了before-run的钩子函数 new NodeEnvironmentPlugin().apply(compiler); // 应用每个插件 if(options.plugins && Array.isArray(options.plugins)) { compiler.apply.apply(compiler, options.plugins); } // 调用environment插件 compiler.applyPlugins("environment"); // 调用after-environment插件 compiler.applyPlugins("after-environment"); // 处理compiler对象,调用一些必备插件 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函數中主要做了以下兩個操作,
實例化Compiler 類別。這類繼承自 Tapable 類,Tapable 是一個基於發布訂閱的插件架構。 webpack 就是基於Tapable的發布訂閱模式實現的整個流程。 Tapable 中透過 plugins 註冊插件名,以及對應的回呼函數,透過 apply,applyPlugins,applyPluginsWater,applyPluginsAsync等函數以不同的方式呼叫註冊在某一插件下的回調。
透過WebpackOptionsApply 處理webpack compiler對象,透過compiler.apply
的方式呼叫了一些必備插件,在這些插件中,註冊了一些plugins,在後面的編譯過程中,透過呼叫一些插件的方式,去處理一些流程。
run()呼叫
run函數中主要觸發了before-run事件,在before-run事件的回呼函數中觸發了run事件,run事件中呼叫了readRecord函數讀取文件,並呼叫compile()
函數進行編譯。
compile()呼叫
##compile函數中定義了編譯的相關流程,主要有以下流程:#建立compilation對象,負責整個編譯過程中具體細節的對象
觸發make 事件,開始建立模組和分析其依賴
根據入口配置的類型,決定是呼叫哪個plugin中的make 事件的回呼。如單入口的 entry,呼叫的是SingleEntryPlugin.js下 make 事件註冊的回呼函數,其他多入口同理。
呼叫 compilation 物件的 addEntry 函數,建立模組以及依賴關係。
make 事件的回呼函數中,透過seal 封裝建構的結果
###run 方法中定義的onCompiled回呼函數被調用,完成emit過程,將結果寫入目標檔案############compile函數的定義###compile(callback) { // 创建编译参数,包括模块工厂和编译依赖参数数组 const params = this.newCompilationParams(); // 触发before-compile 事件,开始整个编译过程 this.applyPluginsAsync("before-compile", params, err => { if(err) return callback(err); // 触发compile事件 this.applyPlugins("compile", params); // 构建compilation对象,compilation对象负责具体的编译细节 const compilation = this.newCompilation(params); // 触发make事件,对应的监听make事件的回调函数在不同的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 事件觸發後,有哪些外掛程式中註冊了make事件並得到了運作的機會呢? #########以單入口entry配置為例,在EntryOptionPlugin插件中定義了,不同配置的入口應該調用何種插件進行解析。在不同配置的入口插件中註冊了對應的 make 事件回呼函數,在make事件觸發後被呼叫。 ######如下所示:#########一個插件的apply方法是一個插件的核心方法,當說一個插件被呼叫時主要是其apply方法被呼叫。 #########EntryOptionPlugin 插件在webpackOptionsApply中被調用,其內部定義了使用何種插件來解析入口檔案。 ###
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字段的类型去调用不同的入口插件去处理 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 事件被觸發時,EntryOptionPlugin 外掛做了這幾個事情:######判斷入口的類型,透過entry 欄位來判斷,對應了entry 欄位為string object function的三種情況######每種不同的類型呼叫不同的插件去處理入口的配置。大致處理邏輯如下:###
数组类型的entry调用multiEntryPlugin插件去处理,对应了多入口的场景
function的entry调用了DynamicEntryPlugin插件去处理,对应了异步chunk的场景
string类型的entry或者object类型的entry,调用SingleEntryPlugin去处理,对应了单入口的场景
【问题】entry-option 事件是在什么时机被触发的呢?
如下代码所示,是在WebpackOptionsApply.js中,先调用处理入口的EntryOptionPlugin插件,然后触发 entry-option 事件,去调用不同类型的入口处理插件。
注意:调用插件的过程也就是一个注册事件以及回调函数的过程。
WebpackOptionApply.js
// 调用处理入口entry的插件 compiler.apply(new EntryOptionPlugin()); compiler.applyPluginsBailResult("entry-option", options.context, options.entry);
前面说到,make事件触发时,对应的回调逻辑都在不同配置入口的插件中注册的。下面以SingleEntryPlugin为例,说明从 make 事件被触发,到编译结束的整个过程。
SingleEntryPlugin.js
class SingleEntryPlugin { constructor(context, entry, name) { this.context = context; this.entry = entry; this.name = name; } apply(compiler) { // compilation 事件在初始化Compilation对象的时候被触发 compiler.plugin("compilation", (compilation, params) => { const normalModuleFactory = params.normalModuleFactory; compilation.dependencyFactories.set(SingleEntryDependency, normalModuleFactory); }); // make 事件在执行compile的时候被触发 compiler.plugin("make", (compilation, callback) => { const dep = SingleEntryPlugin.createDependency(this.entry, this.name); // 编译的关键,调用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中负责具体编译的细节,包括如何创建模块以及模块的依赖,根据模板生成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(); ... // 根据模块的类型获取对应的模块工厂并创建模块 const moduleFactory = this.dependencyFactories.get(dependency.constructor); ... // 创建模块,将创建好的模块module作为参数传递给回调函数 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); // 构建模块,包括调用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 主要做了以下几件事情:
调用对应的模块工厂类去创建module
buildModule,开始构建模块,收集依赖。构建过程中最耗时的一步,主要完成了调用loader处理模块以及模块之间的依赖,使用acorn生成AST的过程,遍历AST循环收集并构建依赖模块的过程。此处可以深入了解webpack使用loader处理模块的原理。
Compilation的 seal 函数在 make 事件的回调函数中进行了调用。
seal(callback) { const self = this; // 触发seal事件,提供其他插件中seal的执行时机 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); // 创建一个entrypoint const entrypoint = self.entrypoints[chunk.name] = new Entrypoint(chunk.name); // 将chunk创建的chunk保存在entrypoint中,并将该entrypoint的实例保存在chunk的entrypoints中 entrypoint.unshiftChunk(chunk); // 将module保存在chunk的_modules数组中 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"); // 创建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创建最后的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 中可以发现,调用了很多不同的插件,主要就是操作chunk和module的一些插件,生成最后的源代码。其中 createHash 用来生成hash,createChunkAssets 用来生成chunk的源码,createModuleAssets 用来生成Module的源码。在 createChunkAssets 中判断了是否是入口chunk,入口的chunk用mainTemplate生成,否则用chunkTemplate生成。
在compiler中的 run 方法中定义了compile的回调函数 onCompiled, 在编译结束后,会调用该回调函数。在该回调函数中调用了 emitAsset,触发了 emit 事件,将文件写入到文件系统中的指定位置。
webpack的源码通过采用Tapable控制其事件流,并通过plugin机制,在webpack构建过程中将一些事件钩子暴露给plugin,使得开发者可以通过编写相应的插件来自定义打包。
上面是我整理给大家的,希望今后会对大家有帮助。
相关文章:
以上是建構webpack的詳細介紹的詳細內容。更多資訊請關注PHP中文網其他相關文章!