webpack構(gòu)建的詳細(xì)流程探底
作為模塊加載和打包神器,只需配置幾個(gè)文件,加載各種 loader 就可以享受無(wú)痛流程化開(kāi)發(fā)。但對(duì)于 webpack 這樣一個(gè)復(fù)雜度較高的插件集合,它的整體流程及思想對(duì)我們來(lái)說(shuō)還是很透明的。
本文旨在搞清楚從命令行下敲下 webpack 命令,或者配置 npm script 后執(zhí)行 package.json 中的命令,到工程目錄下出現(xiàn)打包的后的 bundle 文件的過(guò)程中,webpack都替我們做了哪些工作。
測(cè)試用webpack版本為 webpack@3.4.1
webpack.config.js中定義好相關(guān)配置,包括 entry、output、module、plugins等,命令行執(zhí)行 webpack 命令,webpack 便會(huì)根據(jù)配置文件中的配置進(jìn)行打包處理文件,并生成最后打包后的文件。
第一步:執(zhí)行 webpack 命令時(shí),發(fā)生了什么?(bin/webpack.js)
命令行執(zhí)行 webpack 時(shí),如果全局命令行中未找到webpack命令的話,執(zhí)行本地的node-modules/bin/webpack.js 文件。
在bin/webpack.js中使用 yargs庫(kù) 解析了命令行的參數(shù),處理了 webpack 的配置對(duì)象 options,調(diào)用 processOptions() 函數(shù)。
// 處理編譯相關(guān),核心函數(shù)
function processOptions(options) {
// promise風(fēng)格的處理,暫時(shí)還沒(méi)遇到這種情況的配置
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)入編譯過(guò)程
compiler.run(compilerCallback);
}
第二步: 調(diào)用 webpack,返回 compiler 對(duì)象的過(guò)程(lib/webpack.js)
如下圖所示,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 = {}, {...});
接下來(lái)看在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)行處理,目前還沒(méi)遇到過(guò)這種情況的配置
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è)操作,
- 實(shí)例化 Compiler 類(lèi)。該類(lèi)繼承自 Tapable 類(lèi),Tapable 是一個(gè)基于發(fā)布訂閱的插件架構(gòu)。webpack 便是基于Tapable的發(fā)布訂閱模式實(shí)現(xiàn)的整個(gè)流程。Tapable 中通過(guò) plugins 注冊(cè)插件名,以及對(duì)應(yīng)的回調(diào)函數(shù),通過(guò) apply,applyPlugins,applyPluginsWater,applyPluginsAsync等函數(shù)以不同的方式調(diào)用注冊(cè)在某一插件下的回調(diào)。
- 通過(guò)WebpackOptionsApply 處理webpack compiler對(duì)象,通過(guò)
compiler.apply的方式調(diào)用了一些必備插件,在這些插件中,注冊(cè)了一些 plugins,在后面的編譯過(guò)程中,通過(guò)調(diào)用一些插件的方式,去處理一些流程。
第三步:調(diào)用compiler的run的過(guò)程(Compiler.js)
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)流程,主要有以下流程:
- 創(chuàng)建編譯參數(shù)
- 觸發(fā) before-compile 事件,
- 觸發(fā) compile 事件,開(kāi)始編譯
- 創(chuàng)建 compilation對(duì)象,負(fù)責(zé)整個(gè)編譯過(guò)程中具體細(xì)節(jié)的對(duì)象
- 觸發(fā) make 事件,開(kāi)始創(chuàng)建模塊和分析其依賴
- 根據(jù)入口配置的類(lèi)型,決定是調(diào)用哪個(gè)plugin中的 make 事件的回調(diào)。如單入口的 entry,調(diào)用的是SingleEntryPlugin.js下 make 事件注冊(cè)的回調(diào)函數(shù),其他多入口同理。
- 調(diào)用 compilation 對(duì)象的 addEntry 函數(shù),創(chuàng)建模塊以及依賴。
- make 事件的回調(diào)函數(shù)中,通過(guò)seal 封裝構(gòu)建的結(jié)果
- run 方法中定義的 onCompiled回調(diào)函數(shù)被調(diào)用,完成emit過(guò)程,將結(jié)果寫(xiě)入至目標(biāo)文件
compile函數(shù)的定義
compile(callback) {
// 創(chuàng)建編譯參數(shù),包括模塊工廠和編譯依賴參數(shù)數(shù)組
const params = this.newCompilationParams();
// 觸發(fā)before-compile 事件,開(kāi)始整個(gè)編譯過(guò)程
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)聽(tīng)make事件的回調(diào)函數(shù)在不同的EntryPlugin中注冊(cè),比如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);
});
});
});
});
}
【問(wèn)題】make 事件觸發(fā)后,有哪些插件中注冊(cè)了make事件并得到了運(yùn)行的機(jī)會(huì)呢?
以單入口entry配置為例,在EntryOptionPlugin插件中定義了,不同配置的入口應(yīng)該調(diào)用何種插件進(jìn)行解析。不同配置的入口插件中注冊(cè)了對(duì)應(yīng)的 make 事件回調(diào)函數(shù),在make事件觸發(fā)后被調(diào)用。
如下所示:
一個(gè)插件的apply方法是一個(gè)插件的核心方法,當(dāng)說(shuō)一個(gè)插件被調(diào)用時(shí)主要是其apply方法被調(diào)用。
EntryOptionPlugin 插件在webpackOptionsApply中被調(diào)用,其內(nèi)部定義了使用何種插件來(lá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字段的類(lèi)型去調(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è)事情:
判斷入口的類(lèi)型,通過(guò) entry 字段來(lái)判斷,對(duì)應(yīng)了 entry 字段為 string object function的三種情況
每種不同的類(lèi)型調(diào)用不同的插件去處理入口的配置。大致處理邏輯如下:
- 數(shù)組類(lèi)型的entry調(diào)用multiEntryPlugin插件去處理,對(duì)應(yīng)了多入口的場(chǎng)景
- function的entry調(diào)用了DynamicEntryPlugin插件去處理,對(duì)應(yīng)了異步chunk的場(chǎng)景
- string類(lèi)型的entry或者object類(lèi)型的entry,調(diào)用SingleEntryPlugin去處理,對(duì)應(yīng)了單入口的場(chǎng)景
【問(wèn)題】entry-option 事件是在什么時(shí)機(jī)被觸發(fā)的呢?
如下代碼所示,是在WebpackOptionsApply.js中,先調(diào)用處理入口的EntryOptionPlugin插件,然后觸發(fā) entry-option 事件,去調(diào)用不同類(lèi)型的入口處理插件。
注意:調(diào)用插件的過(guò)程也就是一個(gè)注冊(cè)事件以及回調(diào)函數(shù)的過(guò)程。
WebpackOptionApply.js
// 調(diào)用處理入口entry的插件
compiler.apply(new EntryOptionPlugin());
compiler.applyPluginsBailResult("entry-option", options.context, options.entry);
前面說(shuō)到,make事件觸發(fā)時(shí),對(duì)應(yīng)的回調(diào)邏輯都在不同配置入口的插件中注冊(cè)的。下面以SingleEntryPlugin為例,說(shuō)明從 make 事件被觸發(fā),到編譯結(jié)束的整個(gè)過(guò)程。
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)入編譯過(guò)程。
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ù)模塊的類(lèi)型獲取對(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;
}
// 開(kāi)始處理收集好的依賴
moduleReady.call(this);
});
function moduleReady() {
this.processModuleDependencies(module, err => {
if(err) {
return callback(err);
}
return callback(null, module);
});
}
});
}
_addModuleChain 主要做了以下幾件事情:
- 調(diào)用對(duì)應(yīng)的模塊工廠類(lèi)去創(chuàng)建module
- buildModule,開(kāi)始構(gòu)建模塊,收集依賴。構(gòu)建過(guò)程中最耗時(shí)的一步,主要完成了調(diào)用loader處理模塊以及模塊之間的依賴,使用acorn生成AST的過(guò)程,遍歷AST循環(huán)收集并構(gòu)建依賴模塊的過(guò)程。此處可以深入了解webpack使用loader處理模塊的原理。
第四步:模塊build完成后,使用seal進(jìn)行module和chunk的一些處理,包括合并、拆分等。
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 用來(lái)生成hash,createChunkAssets 用來(lái)生成chunk的源碼,createModuleAssets 用來(lái)生成Module的源碼。在 createChunkAssets 中判斷了是否是入口chunk,入口的chunk用mainTemplate生成,否則用chunkTemplate生成。
第五步:通過(guò) emitAssets 將生成的代碼輸入到output的指定位置
在compiler中的 run 方法中定義了compile的回調(diào)函數(shù) onCompiled, 在編譯結(jié)束后,會(huì)調(diào)用該回調(diào)函數(shù)。在該回調(diào)函數(shù)中調(diào)用了 emitAsset,觸發(fā)了 emit 事件,將文件寫(xiě)入到文件系統(tǒng)中的指定位置。
總結(jié)
webpack的源碼通過(guò)采用Tapable控制其事件流,并通過(guò)plugin機(jī)制,在webpack構(gòu)建過(guò)程中將一些事件鉤子暴露給plugin,使得開(kāi)發(fā)者可以通過(guò)編寫(xiě)相應(yīng)的插件來(lái)自定義打包。
好了,以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,如果有疑問(wèn)大家可以留言交流,謝謝大家對(duì)腳本之家的支持。
參考文章:
相關(guān)文章
JavaScript的級(jí)聯(lián)函數(shù)用法簡(jiǎn)單示例【鏈?zhǔn)秸{(diào)用】
這篇文章主要介紹了JavaScript的級(jí)聯(lián)函數(shù)用法,結(jié)合簡(jiǎn)單實(shí)例形式分析了javascript鏈?zhǔn)秸{(diào)用具體定義及使用方法,需要的朋友可以參考下2019-03-03
js 瀏覽器版本及版本號(hào)判斷函數(shù)2009年
這個(gè)是至2009年4月比較新的判斷瀏覽器版本的函數(shù)。js判斷瀏覽器版本及版本號(hào),本代碼支持最新的ie7,ie8,firefox2,firefox3的判斷2009-04-04
Egg Vue SSR 服務(wù)端渲染數(shù)據(jù)請(qǐng)求與asyncData
這篇文章主要介紹了Egg Vue SSR 服務(wù)端渲染數(shù)據(jù)請(qǐng)求與asyncData,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-11-11
微信小程序自定義tab實(shí)現(xiàn)多層tab嵌套
這篇文章主要為大家詳細(xì)介紹了微信小程序自定義tab實(shí)現(xiàn)多層tab嵌套,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-07-07
基于JS實(shí)現(xiàn)橫線提示輸入驗(yàn)證碼隨驗(yàn)證碼輸入消失(js驗(yàn)證碼的實(shí)現(xiàn))
最近在開(kāi)微信的頁(yè)面,在項(xiàng)目需求中遇到之前沒(méi)有做過(guò)的功能,要求橫線提示輸入驗(yàn)證碼隨驗(yàn)證碼輸入橫線消失,基于js怎么實(shí)現(xiàn)的呢?下面小編給大家分享基于js實(shí)現(xiàn)驗(yàn)證碼功能,感興趣的朋友一起看看吧2016-10-10
js自定義鼠標(biāo)右鍵的實(shí)現(xiàn)原理及源碼
這篇文章主要介紹了js自定義鼠標(biāo)右鍵的實(shí)現(xiàn)原理及源碼,需要的朋友可以參考下2014-06-06
JavaScript數(shù)組的定義及數(shù)字操作技巧
這篇文章主要介紹了JavaScript數(shù)組的定義及數(shù)字操作技巧的相關(guān)資料,需要的朋友可以參考下2016-06-06

