webpack運行於node js之上,瞭解源碼的執行,不僅可以讓我們對webpack的使用更爲熟悉,更會增強我們對應用代碼的組織能力,
本篇文章重點從webpack核心的兩個特性loader,plugin,進行深入分析,
我們從一個例子出發來分析webpack執行過程,地址
我們使用 vscode 調試工具來對webpack進行調試,
首先我們從入口出發
"build":"webpack --config entry.js"
示例項目通過npm run build 進行啓動,npm run 會新建一個shell,並將 node_modules/.bin 下的所有內容加入環境變量,我們查看下.bin 文件夾下內容
webpack
webpack-cli
webpack-dev-server
可以看到webpack便在其中,
打開文件,可以看到文件頭部
#!/usr/bin/env node
使用node執行此文件內容,webpack 文件的主要內容是判斷webpack-cli或者webpack-command有沒有安裝,如果有安裝則執行對應文件內容,本例安裝了webpack-cli,所以通過對目標cli的require,進入到對應cli的執行,
webpack-cli
webpack-cli是一個自執行函數,對我們在命令行傳入的一些參數進行了解析判斷,核心內容是把webpack入口文件作爲參數,執行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後,執行compiler.run()或者compiler.watch(),
本例未啓動熱更新所以執行的是 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);
}
既然已經知道核心是這兩個參數的執行,我們即可模擬一個webpack的執行過程,本例中,我們創建一個debug.js
const webpack = require('webpack');
const options = require('./entry.js');
const compiler = webpack(options);
我們在webpack()函數前面加上斷點,即可通過vscode開始debug
我們先對生成compiler過程進行分析,
webpack函數
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;
};
我們可以看到,有對options參數的驗證validateSchema(webpackOptionsSchema,options);
有對默認配置的合併 options = new WebpackOptionsDefaulter().process(options);
合併內容
然後對所有的plugins配置進行註冊操作
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);
}
}
}
關於這裏的註冊,我們可以通過寫一個plugin來描述執行過程,
本例中我們新建一個testplugin文件,
testplugin
module.exports = class testPlugin{
apply(compiler){
console.log('註冊')
compiler.hooks.run.tapAsync("testPlugin",(compilation,callback)=>{
console.log("test plugin")
callback()
})
}
}
關於插件的編寫,我們只需要提供一個類,prototype上含有apply函數,同時擁有一個compiler參數,之後通過tap註冊compiler上的hook,使得webpack執行到指定時機執行回調函數,具體編寫方法參考寫一個插件
本示例插件中,我們在compiler的run hook上註冊了testplugin插件,回調的內容爲打印 “test plugin”,並且,在註冊的時候我們會打印 ”註冊“,來跟蹤plugin的註冊執行流程,
回到webpack 函數,可以看到,進行完插件的註冊,就會執行兩個hook的回調,
compiler.hooks.environment.call();
compiler.hooks.afterEnvironment.call();
這時,就會執行我們註冊在environment,afterEnvironment上的plugin的回調,其他插件的回調執行也是通過call或者callAsync 來觸發執行,webpack整個源碼執行過程中會在不同的階段執行不同的hook的call函數,所以,在我們編寫插件的過程中要對流程有些瞭解,從而將插件註冊在合適的hook上,
webpack函數的最後,就是執行compiler.run函數,我們在這裏加上斷點,進入compiler.run函數,
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 函數中也是執行了一系列的hook,我們編寫的testplugin就會在this.hooks.run.callAsync
處執行,關於plugin的註冊和運行具體細節,本篇先不講,只需知道註冊通過tap,運行通過call即可,,
到了這裏,基本的plugin的運行過程我們已經瞭解,接下來我們通過幾個目標來對loader的執行過程進行分析,
- 模塊如何匹配到的loader
- 模塊是如何遞歸的解析當前模塊引用模塊的
- loader是在哪裏執行的
回到源代碼,執行完一些hooks後,進入到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的執行,重點是make 的hook,我們進入,make hook通過htmlWebpackPlugin註冊了一個回調,回調中又註冊了一個SingleEntryPlugin,然後又重新執行了make.callAsync,進入了SingleEntryPlugin的回調
compiler.hooks.make.tapAsync(
"SingleEntryPlugin",
(compilation, callback) => {
const { entry, name, context } = this;
const dep = SingleEntryPlugin.createDependency(entry, name);
compilation.addEntry(context, dep, name, callback);
}
);
可以看到,主要執行了addEntry方法,addEntry中執行addEntry hook,然後調用_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 創建modeuleFactory對象,然後執行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();
});
對於loader的匹配,發生於moduleFactory.create()中,其中執行beforeResolve hook,執行完的回調函數中執行factory,factory中執行resolver,resolver是 resolver hook的回調函數,其中通過this.ruleSet.exec和request的分割分別完成loader的匹配,對module匹配到的loader的生成即在這裏完成,之後注入到module對象中,接下來我們回到moduleFactory.create的回調函數
此時生成的module對象中有幾個顯著的屬性,
userRequest:
loaders
即當前模塊的路徑和匹配到的loader,本例中index.js模塊即匹配到了testloader,我們編寫的測試loader,
testloader
module.exports = function(source){
console.log("test loader")
return source+";console.log(123)"
}
關於loader的編寫本篇也不細講,借用一句文檔的描述
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回調函數中,執行了buildModule,
buildModule中執行了module.build(),build中執行doBuild,doBuild中執行runloaders,自此開始即爲對loader的執行,runloaders中執行iteratePitchingLoaders,然後執行loadLoader,通過import或者require等模塊化方法加載loader資源,這裏分爲幾種loaders,根據不同情況,最終執行runSyncOrAsync,runSyncOrAsync中
var result = (function LOADER_EXECUTION() {
return fn.apply(context, args);
}());
通過LOADER_EXECUTION()方法對loader進行,執行,返回執行結果,繼續執行其他loader,loader的執行即爲此處,
loader執行完成之後,buildModule執行完成,進行callback的執行,其中執行了moduleFactory.create中定義的afterBuild函數,afterBuild函數執行了processModuleDependencies函數,processModuleDependencies函數中通過內部定義的addDependency和addDependenciesBlock方法,生成當前module所依賴的module,執行addModuleDependencies
this.addModuleDependencies(
module,
sortedDependencies,
this.bail,
null,
true,
callback
);
傳入此模塊的依賴,addModuleDependencies中循環對sortedDependencies進行了factory.create,factory.create中又執行了beforeResolve hook,從而又執行上面流程,匹配loader,執行loader,對依賴進行遍歷等步驟,所以,通過這個深度優先遍歷,即可對所有模塊及其依賴模塊進行loade的匹配和處理,自此,loader學習的三個目標已經達成
make hook主要內容即是這些,之後又執行了seal,afterCopile等等等hook,這些即爲一些關於代碼分割,抽離等等插件的執行時機,爲我們插件的編寫提供了一些入口,compiler和compilation執行過程中的所有hook可以查看文檔,一共有九十多個(汗顏💧)compiler hook,compilation hook
至此,loader的執行過程和plugin的執行過程已經非常清晰,本篇文章目的也已達到,如果大家對某些hook的執行位置感興趣或者對某些插件某些loader感興趣,即可使用debugger根據此流程進行跟蹤,從而對插件,loader的使用更加得心應手,
本篇文章示例代碼github地址,
如果本篇文章對你瞭解webpack有一定的幫助,順便留個star ><