最近花了一点时间探究webpack5的源码,通过网上的一些文章的辅助去更好的理解这个项目。接下来分析一个常规的解析过程。
首先是是使用vscode的Node调试,通过vscode的run and debug添加调试配置,默认生成的launch.json文件如下{
"version": "0.2.0",
"configurations": [
{
"type": "pwa-node",
"request": "launch",
"name": "Launch Program",
"skipFiles": [
"<node_internals>/**"
],
"program": "${workspaceFolder}\\mytest\\index.js"
}
]
}
配置program调试入口,然后点击三角形按钮就可以调试代码。
如图在新建了一个mytest的文件夹下创建一个index.js作为调试的入口,掉用lib/index.js。引入webpack在lib/webpack.js中导出函数,调用这个函数一步步往下执行。
在开始之前我们先来了解一下Hooks类使用的是tapable, 它包含几个方法SyncHook,
SyncBailHook,
AsyncParallelHook,
AsyncSeriesHook
通过tap注册事件和回调,并通过call,callAsync,promise.then来触发。当然还可以注册拦截器来监听hook执行的各个阶段。
Tapable是webpack的精髓,通过hook注册和触发事件,贯穿整个构件解析的生命周期,后面的解析我们会讲到,compiler以及compiler.compilation都注册了很多操作的Hooks.
接下来我们正是进入源码,lib/webpack.js中导出的函数,主要是create函数创建并返回compiler实例和watch标记以及watchOptions配置。
当我们的调用的函数存在回调是会判断是否是watch,是的话执行compiler.watch,否则执行compiler.run。
const { compiler, watch, watchOptions } = create();
if (watch) {
compiler.watch(watchOptions, callback);
} else {
compiler.run((err, stats) => {
compiler.close(err2 => {
callback(err || err2, stats);
});
});
}
return compiler;
可以构件单个compiler调用的是createCompiler,后者多个createMultiCompiler,下面我们一单个为例
const createCompiler = rawOptions => {
const options = getNormalizedWebpackOptions(rawOptions);
applyWebpackOptionsBaseDefaults(options);
const compiler = new Compiler(options.context, options);
new NodeEnvironmentPlugin({
infrastructureLogging: options.infrastructureLogging
}).apply(compiler);
if (Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
if (typeof plugin === "function") {
plugin.call(compiler, compiler);
} else {
plugin.apply(compiler);
}
}
}
applyWebpackOptionsDefaults(options);
compiler.hooks.environment.call();
compiler.hooks.afterEnvironment.call();
new WebpackOptionsApply().process(options, compiler);
compiler.hooks.initialize.call();
return compiler;
};
首先是获取用户的配置加上默认的配置,生成最新的配置。然后创建compiler实例,
NodeEnvironmentPlugin插件是用来扩展node,加文件缓存、文件读取和写入、文件监听。这里文件监听使用的是Watchpack库。
执行用户配置的插件,在添加webpac默认配置。
接下来所有重要的逻辑都是在WebpackOptionsApply这里面处理的,通过配置去执行对应的插件。
compiler.hooks.make.tapAsync("EntryPlugin", (compilation, callback) => {
compilation.addEntry(context, dep, options, err => {
callback(err);
});
});
主要来看入口文件的处理, EntryOptionPlugin插件中EntryPlugin通过调用compilation.addEntry从入口文件开始处理。
此外如果入口是一个函数,则调用DynamicEntryPlugin也会添加入口文件。
在网上看到别人分析的源码,我觉得这张图结构很清晰,可以说明的生命周期的各个阶段所做的事:
addEntry方法中调用addModuleTree去处理相关的模块依赖,调用dependencyFactories
处理模块的依赖dependency.constructor。然后handleModuleCreation内部处理,最后调用factorizeModule解析模块。用factorizeQueue将模块解析压入模块解析队列中执行。
首先调用工程的factory.create函数,调用的是compilation.dependencyFactories.set(
EntryDependency,
normalModuleFactory
);
放入的normalModuleFactory。
大致步骤addModule添加模块,然后_handleModuleBuildAndDependencies构建模块buildModule和处理模块依赖processModuleDependencies。通过_processModuleDependencies方法中handleModuleCreation循环构建。
在构建过程中需要处理一些loader,在NormalModule中调用_doBuild中的runLoaders去处理,最终返回资源和webpackAST信息。
最后调用compilation.seal将模块转换成chunk代码块,最后通过emitAssets输出合并后的代码块输出文件。