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 ><