前言
参考文章。在次感谢此文作者,此文打通了我对webpack源码主流程的仁都二脉。笔者也一直想写一篇webpack主流程相关以及loader和plugin内部运行机制的文章。
读本文前,假设你已经研究过webpack核心功能 -- tapable
if ( 搞懂了 tapable !== true ) {
return 请先研读 《显微镜下的webpack4:灵魂tapable,终于搞懂钩子系列!》
}
ok,到此,你已经有一定基础来梳理webpack主流程。
下面贴出来的代码可能和源码有些出入,经过删选过的,具体可以参照源码。
编写入口文件
编写一个简单启动webpack 的文件 debug.js
//载入webpack主体
let webpack=require('webpack');
//指定webpack配置文件
let config=require('./build/webpack.prod.js');
//执行webpack,返回一个compile的对象,这个时候编译并未执行
let compile=webpack(config);
//运行compile,执行编译
compile.run();
初始化
假设你已经有了webpack源码,找到webpack项目下的package.json文件。
发现:
// package.json
"main": "./lib/webpack.js",
// ./lib/webpack.js
exports = module.exports = webpack;
所以, let webpack = require('webpack'),其实就是这个函数
const webpack = (options, callback) => {
// 代码块1:
const webpackOptionsValidationErrors = validateSchema(
webpackOptionsSchema,
options
);
if (webpackOptionsValidationErrors.length) {
throw new WebpackOptionsValidationError(webpackOptionsValidationErrors);
}
// 代码块1 end---------------
let compiler;
if (typeof options === "object") {
options = new WebpackOptionsDefaulter().process(options);
compiler = new Compiler(options.context);
compiler.options = options;
new NodeEnvironmentPlugin({
infrastructureLogging: options.infrastructureLogging
}).apply(compiler);
// 代码块2
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);
}
}
}
// 2 end---------
compiler.hooks.environment.call();
compiler.hooks.afterEnvironment.call();
compiler.options = new WebpackOptionsApply().process(options, compiler);
} else {
throw new Error("Invalid argument: options");
}
return compiler;
};
-
其中,一个好的系统都会做参数校验,代码块1,就是对传进来的options做判断。
-
options = new WebpackOptionsDefaulter().process(options); webpack4能做到0配置是因为它。
这一句,设置了webpack的许多默认属性,具体在WebpackOptionsDefaulter.js文件中,并且继承了OptionsDefaulter类。
看完就明白了 entry为什么默认是 ./src下,cache只有在development下打开,optimization下splitChunks的默认分割文件规则等等。
以后要是不明白webpack4的默认行为,常来这里看看。
-
compiler = new Compiler(options.context); 然后实例化了一个Complier(继承了Tapable)实例,为编译做准备。其中在complier对象上定义了hook属性,里面包含了各种钩子。
this.hooks = { done: new AsyncSeriesHook(["stats"]), beforeRun: new AsyncSeriesHook(["compiler"]), run: new AsyncSeriesHook(["compiler"]), emit: new AsyncSeriesHook(["compilation"]), assetEmitted: new AsyncSeriesHook(["file", "content"]), afterEmit: new AsyncSeriesHook(["compilation"]), compilation: new SyncHook(["compilation", "params"]), normalModuleFactory: new SyncHook(["normalModuleFactory"]), contextModuleFactory: new SyncHook(["contextModulefactory"]), beforeCompile: new AsyncSeriesHook(["params"]), compile: new SyncHook(["params"]), make: new AsyncParallelHook(["compilation"]), afterCompile: new AsyncSeriesHook(["compilation"]), // TODO the following hooks are weirdly located here // TODO move them for webpack 5 environment: new SyncHook([]), afterEnvironment: new SyncHook([]), afterPlugins: new SyncHook(["compiler"]), afterResolvers: new SyncHook(["compiler"]), entryOption: new SyncBailHook(["context", "entry"]) };所以如果写插件,想在哪个阶段做事情,就和这个有关了。
-
compiler.options = options; 并把默认的参数挂载到了complier实例。
-
NodeEnvironmentPlugin插件, 在文件中 NodeEnvironmentPlugin.js提供了node.js 的fs 操作文件的能力,同时注册了beforeRun钩子的监听函数,功能是在运行前清理文件缓存。
-
代码块2的功能是,调用在webpack.config.js 配置文件中传入的各个插件实例的apply 方法,并把编译实例complier作为参数传进去了。所以暴露了webpack合并的默认参数和各阶段的钩子函数,供君使用。到这里,最简单的插件产生了。
class HookTest { constructor() { console.log('HookTest被实例化.....'); } apply(compiler) { console.log('apply方法被调用'); //compiler.hooks.run.tap('testHook', (compiler, err) => { // console.log('beforeRun 执行完了,到run了', compiler, err); //}); } } module.exports = HookTest; -
compiler.hooks.environment.call();/compiler.hooks.afterEnvironment.call(); 触发这2个钩子函数
-
compiler.options = new WebpackOptionsApply().process(options, compiler); 这句就吊了。你们知道得webpack 一切皆插件。但是!我们的配置怎么转化为具体功能的呢?就是这句代码的作用,它把所有的配置转化成对应的插件并apply,即在对应阶段tap/tapAsync(监听)了事件,供之后call/callAsync/promise(调用)。意思是你的配置都转化成了功能,调用不调用它都在那里等你call。
到这初始化结束,总结下初始化做了哪些事情。
- 初始化了配置参数options
- 初始化了编译Complier 实例对象complier,并定义了各种阶段钩子。并将options挂载在了complier上
- complier上增加了文件操作能力
- 注册webpack.config.js 中传入的plugins
- 配置参数转化为插件,注册相应功能
编译
我们从上面知道,编译是从complier.run()开始,但是,真正编译的工作是在 Compilation类里面完成的, Complier却是作为一个主流程触发一系列钩子。看下我总结的一个流程:
从图中可以看到具体流程。
- addEntry 表示添加入口文件
- _addModuleChain 添加模块链
- runLoaders 获取对应的loader解析文件
- Seal 触发各种钩子,Chunk的优化打包构建
- onCompiled 完成了最后输出文件到磁盘的操作
编译模块的主要过程,参考这篇文章 说得很详细了没错就是抄来抄去得。你懂就好了。
所以loader 的解析我们就知道了是在runLoaders这个阶段。