webpack運(yùn)行于node js之上,了解源碼的執(zhí)行涎拉,不僅可以讓我們對(duì)webpack的使用更為熟悉瑞侮,更會(huì)增強(qiáng)我們對(duì)應(yīng)用代碼的組織能力,
本篇文章重點(diǎn)從webpack核心的兩個(gè)特性loader鼓拧,plugin半火,進(jìn)行深入分析,
我們從一個(gè)例子出發(fā)來分析webpack執(zhí)行過程季俩,地址
我們使用 vscode 調(diào)試工具來對(duì)webpack進(jìn)行調(diào)試钮糖,
首先我們從入口出發(fā)
"build":"webpack --config entry.js"
示例項(xiàng)目通過npm run build 進(jìn)行啟動(dòng),npm run 會(huì)新建一個(gè)shell酌住,并將 node_modules/.bin 下的所有內(nèi)容加入環(huán)境變量店归,我們查看下.bin 文件夾下內(nèi)容
webpack
webpack-cli
webpack-dev-server
可以看到webpack便在其中,
打開文件酪我,可以看到文件頭部
#!/usr/bin/env node
使用node執(zhí)行此文件內(nèi)容消痛,webpack 文件的主要內(nèi)容是判斷webpack-cli或者webpack-command有沒有安裝,如果有安裝則執(zhí)行對(duì)應(yīng)文件內(nèi)容都哭,本例安裝了webpack-cli肄满,所以通過對(duì)目標(biāo)cli的require谴古,進(jìn)入到對(duì)應(yīng)cli的執(zhí)行,
webpack-cli
webpack-cli是一個(gè)自執(zhí)行函數(shù)稠歉,對(duì)我們?cè)诿钚袀魅氲囊恍﹨?shù)進(jìn)行了解析判斷掰担,核心內(nèi)容是把webpack入口文件作為參數(shù),執(zhí)行webpack怒炸,生成compiler
try {
compiler = webpack(options);
} catch (err) {
if (err.name === "WebpackOptionsValidationError") {
if (argv.color) console.error(`\u001b[1m\u001b[31m${err.message}\u001b[39m\u001b[22m`);
else console.error(err.message);
// eslint-disable-next-line no-process-exit
process.exit(1);
}
throw err;
}
生成compiler后带饱,執(zhí)行compiler.run()或者compiler.watch(),
本例未啟動(dòng)熱更新所以執(zhí)行的是 compiler.run()
if (firstOptions.watch || options.watch) {
const watchOptions = firstOptions.watchOptions || firstOptions.watch || options.watch || {};
if (watchOptions.stdin) {
process.stdin.on("end", function(_) {
process.exit(); // eslint-disable-line
});
process.stdin.resume();
}
compiler.watch(watchOptions, compilerCallback);
if (outputOptions.infoVerbosity !== "none") console.error("\nwebpack is watching the files…\n");
if (compiler.close) compiler.close(compilerCallback);
} else {
compiler.run(compilerCallback);
if (compiler.close) compiler.close(compilerCallback);
}
既然已經(jīng)知道核心是這兩個(gè)參數(shù)的執(zhí)行阅羹,我們即可模擬一個(gè)webpack的執(zhí)行過程勺疼,本例中,我們創(chuàng)建一個(gè)debug.js
const webpack = require('webpack');
const options = require('./entry.js');
const compiler = webpack(options);
我們?cè)趙ebpack()函數(shù)前面加上斷點(diǎn)捏鱼,即可通過vscode開始debug
我們先對(duì)生成compiler過程進(jìn)行分析执庐,
webpack函數(shù)
const webpack = (options, callback) => {
const webpackOptionsValidationErrors = validateSchema(
webpackOptionsSchema,
options
);
if (webpackOptionsValidationErrors.length) {
throw new WebpackOptionsValidationError(webpackOptionsValidationErrors);
}
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 = new Compiler(options.context);
compiler.options = options;
new NodeEnvironmentPlugin().apply(compiler);
if (options.plugins && Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
if (typeof plugin === "function") {
plugin.call(compiler, compiler);
} else {
plugin.apply(compiler);
}
}
}
compiler.hooks.environment.call();
compiler.hooks.afterEnvironment.call();
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;
};
我們可以看到,有對(duì)options參數(shù)的驗(yàn)證validateSchema(webpackOptionsSchema,options);
有對(duì)默認(rèn)配置的合并 options = new WebpackOptionsDefaulter().process(options);
合并內(nèi)容
然后對(duì)所有的plugins配置進(jìn)行注冊(cè)操作
if (options.plugins && Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
if (typeof plugin === "function") {
plugin.call(compiler, compiler);
} else {
plugin.apply(compiler);
}
}
}
關(guān)于這里的注冊(cè)导梆,我們可以通過寫一個(gè)plugin來描述執(zhí)行過程轨淌,
本例中我們新建一個(gè)testplugin文件,
testplugin
module.exports = class testPlugin{
apply(compiler){
console.log('注冊(cè)')
compiler.hooks.run.tapAsync("testPlugin",(compilation,callback)=>{
console.log("test plugin")
callback()
})
}
}
關(guān)于插件的編寫看尼,我們只需要提供一個(gè)類递鹉,prototype上含有apply函數(shù),同時(shí)擁有一個(gè)compiler參數(shù)藏斩,之后通過tap注冊(cè)compiler上的hook躏结,使得webpack執(zhí)行到指定時(shí)機(jī)執(zhí)行回調(diào)函數(shù),具體編寫方法參考寫一個(gè)插件
本示例插件中狰域,我們?cè)赾ompiler的run hook上注冊(cè)了testplugin插件媳拴,回調(diào)的內(nèi)容為打印 “test plugin”,并且兆览,在注冊(cè)的時(shí)候我們會(huì)打印 ”注冊(cè)“屈溉,來跟蹤plugin的注冊(cè)執(zhí)行流程,
回到webpack 函數(shù)拓颓,可以看到语婴,進(jìn)行完插件的注冊(cè)描孟,就會(huì)執(zhí)行兩個(gè)hook的回調(diào)驶睦,
compiler.hooks.environment.call();
compiler.hooks.afterEnvironment.call();
這時(shí),就會(huì)執(zhí)行我們注冊(cè)在environment匿醒,afterEnvironment上的plugin的回調(diào)场航,其他插件的回調(diào)執(zhí)行也是通過call或者callAsync 來觸發(fā)執(zhí)行,webpack整個(gè)源碼執(zhí)行過程中會(huì)在不同的階段執(zhí)行不同的hook的call函數(shù)廉羔,所以溉痢,在我們編寫插件的過程中要對(duì)流程有些了解,從而將插件注冊(cè)在合適的hook上,
webpack函數(shù)的最后孩饼,就是執(zhí)行compiler.run函數(shù)髓削,我們?cè)谶@里加上斷點(diǎn),進(jìn)入compiler.run函數(shù)镀娶,
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);
});
});
});
compiler.run 函數(shù)中也是執(zhí)行了一系列的hook立膛,我們編寫的testplugin就會(huì)在this.hooks.run.callAsync
處執(zhí)行,關(guān)于plugin的注冊(cè)和運(yùn)行具體細(xì)節(jié)梯码,本篇先不講宝泵,只需知道注冊(cè)通過tap,運(yùn)行通過call即可轩娶,儿奶,
到了這里,基本的plugin的運(yùn)行過程我們已經(jīng)了解鳄抒,接下來我們通過幾個(gè)目標(biāo)來對(duì)loader的執(zhí)行過程進(jìn)行分析闯捎,
- 模塊如何匹配到相對(duì)應(yīng)loader
- 模塊是如何遞歸的解析當(dāng)前模塊引用模塊的
- loader是在哪里執(zhí)行的
回到源代碼,執(zhí)行完一些hooks后嘁酿,進(jìn)入到compile隙券,
compile(callback) {
const params = this.newCompilationParams();
this.hooks.beforeCompile.callAsync(params, err => {
if (err) return callback(err);
this.hooks.compile.call(params);
const compilation = this.newCompilation(params);
this.hooks.make.callAsync(compilation, err => {
if (err) return callback(err);
compilation.finish();
compilation.seal(err => {
if (err) return callback(err);
this.hooks.afterCompile.callAsync(compilation, err => {
if (err) return callback(err);
return callback(null, compilation);
});
});
});
});
}
依舊是一些hooks的執(zhí)行,重點(diǎn)是make 的hook闹司,我們進(jìn)入娱仔,make hook通過htmlWebpackPlugin注冊(cè)了一個(gè)回調(diào),回調(diào)中又注冊(cè)了一個(gè)SingleEntryPlugin游桩,然后又重新執(zhí)行了make.callAsync牲迫,進(jìn)入了SingleEntryPlugin的回調(diào)
compiler.hooks.make.tapAsync(
"SingleEntryPlugin",
(compilation, callback) => {
const { entry, name, context } = this;
const dep = SingleEntryPlugin.createDependency(entry, name);
compilation.addEntry(context, dep, name, callback);
}
);
可以看到,主要執(zhí)行了addEntry方法借卧,addEntry中執(zhí)行addEntry hook盹憎,然后調(diào)用_addModuleChain,
addEntry(context, entry, name, callback) {
this.hooks.addEntry.call(entry, name);
const slot = {
name: name,
// TODO webpack 5 remove `request`
request: null,
module: null
};
if (entry instanceof ModuleDependency) {
slot.request = entry.request;
}
// TODO webpack 5: merge modules instead when multiple entry modules are supported
const idx = this._preparedEntrypoints.findIndex(slot => slot.name === name);
if (idx >= 0) {
// Overwrite existing entrypoint
this._preparedEntrypoints[idx] = slot;
} else {
this._preparedEntrypoints.push(slot);
}
this._addModuleChain(
context,
entry,
module => {
this.entries.push(module);
},
(err, module) => {
if (err) {
this.hooks.failedEntry.call(entry, name, err);
return callback(err);
}
if (module) {
slot.module = module;
} else {
const idx = this._preparedEntrypoints.indexOf(slot);
if (idx >= 0) {
this._preparedEntrypoints.splice(idx, 1);
}
}
this.hooks.succeedEntry.call(entry, name, module);
return callback(null, module);
}
);
}
然后_addModuleChain中通過moduleFactory.create 創(chuàng)建modeuleFactory對(duì)象铐刘,然后執(zhí)行buildModule
this.buildModule(module, false, null, null, err => {
if (err) {
this.semaphore.release();
return errorAndCallback(err);
}
if (currentProfile) {
const afterBuilding = Date.now();
currentProfile.building = afterBuilding - afterFactory;
}
this.semaphore.release();
afterBuild();
});
對(duì)于loader的匹配陪每,發(fā)生于moduleFactory.create()中,其中執(zhí)行beforeResolve hook,執(zhí)行完的回調(diào)函數(shù)中執(zhí)行factory镰吵,factory中執(zhí)行resolver檩禾,resolver是 resolver hook的回調(diào)函數(shù),其中通過this.ruleSet.exec和request的分割分別完成loader的匹配疤祭,對(duì)module匹配到的loader的生成即在這里完成盼产,之后注入到module對(duì)象中,接下來我們回到moduleFactory.create的回調(diào)函數(shù)
此時(shí)生成的module對(duì)象中有幾個(gè)顯著的屬性勺馆,
userRequest:
loaders
即當(dāng)前模塊的路徑和匹配到的loader戏售,本例中index.js模塊即匹配到了testloader侨核,我們編寫的測(cè)試loader,
testloader
module.exports = function(source){
console.log("test loader")
return source+";console.log(123)"
}
關(guān)于loader的編寫本篇也不細(xì)講灌灾,借用一句文檔的描述
A loader is a node module that exports a function. This function is called when a resource should be transformed by this loader. The given function will have access to the Loader API using the
this
context provided to it.
我們回到源碼搓译,moduleFactory.create回調(diào)函數(shù)中,執(zhí)行了buildModule锋喜,
buildModule中執(zhí)行了module.build(),build中執(zhí)行doBuild侥衬,doBuild中執(zhí)行runloaders,自此開始即為對(duì)loader的執(zhí)行跑芳,runloaders中執(zhí)行iteratePitchingLoaders轴总,然后執(zhí)行l(wèi)oadLoader,通過import或者require等模塊化方法加載loader資源博个,這里分為幾種loaders怀樟,根據(jù)不同情況,最終執(zhí)行runSyncOrAsync盆佣,runSyncOrAsync中
var result = (function LOADER_EXECUTION() {
return fn.apply(context, args);
}());
通過LOADER_EXECUTION()方法對(duì)loader進(jìn)行往堡,執(zhí)行,返回執(zhí)行結(jié)果共耍,繼續(xù)執(zhí)行其他loader虑灰,loader的執(zhí)行即為此處,
loader執(zhí)行完成之后痹兜,buildModule執(zhí)行完成穆咐,進(jìn)行callback的執(zhí)行,其中執(zhí)行了moduleFactory.create中定義的afterBuild函數(shù)字旭,afterBuild函數(shù)執(zhí)行了processModuleDependencies函數(shù)对湃,processModuleDependencies函數(shù)中通過內(nèi)部定義的addDependency和addDependenciesBlock方法,生成當(dāng)前module所依賴的module遗淳,執(zhí)行addModuleDependencies
this.addModuleDependencies(
module,
sortedDependencies,
this.bail,
null,
true,
callback
);
傳入此模塊的依賴拍柒,addModuleDependencies中循環(huán)對(duì)sortedDependencies進(jìn)行了factory.create,factory.create中又執(zhí)行了beforeResolve hook屈暗,從而又執(zhí)行上面流程拆讯,匹配loader,執(zhí)行l(wèi)oader养叛,對(duì)依賴進(jìn)行遍歷等步驟种呐,所以,通過這個(gè)深度優(yōu)先遍歷一铅,即可對(duì)所有模塊及其依賴模塊進(jìn)行l(wèi)oade的匹配和處理陕贮,自此堕油,loader學(xué)習(xí)的三個(gè)目標(biāo)已經(jīng)達(dá)成
make hook主要內(nèi)容即是這些潘飘,之后又執(zhí)行了seal肮之,afterCopile等等等hook,這些即為一些關(guān)于代碼分割卜录,抽離等等插件的執(zhí)行時(shí)機(jī)戈擒,為我們插件的編寫提供了一些入口,compiler和compilation執(zhí)行過程中的所有hook可以查看文檔艰毒,一共有九十多個(gè)(汗顏??)compiler hook
筐高,compilation hook
至此,loader的執(zhí)行過程和plugin的執(zhí)行過程已經(jīng)非常清晰丑瞧,本篇文章目的也已達(dá)到柑土,如果大家對(duì)某些hook的執(zhí)行位置感興趣或者對(duì)某些插件某些loader感興趣,即可使用debugger根據(jù)此流程進(jìn)行跟蹤绊汹,從而對(duì)插件稽屏,loader的使用更加得心應(yīng)手,
本篇文章示例代碼github地址
如果本篇文章對(duì)你了解webpack有一定的幫助西乖,順便留個(gè)star ><