上一章分析了启动代码,并且在webpack-cli/cli.js里下了几处断点。今天从compiler = webpack(options)跳入,继续跟踪分析。
try {
compiler = webpack(options);// break here
} catch (err) {
.............
}
const webpack = (options, callback) => {
//验证option
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") {// 但配置情况,此次分析的入口
// 生成webpack默认参数
options = new WebpackOptionsDefaulter().process(options);
// 构建compiler,从这里跳入,继续跟踪分析
compiler = new Compiler(options.context);
compiler.options = options;
// 加入一个plugin
new NodeEnvironmentPlugin().apply(compiler);
// 安装所有的plugins
if (options.plugins && Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
if (typeof plugin === "function") {
plugin.apply(compiler);
} else {
plugin.apply(compiler);
}
}
}
// 触发几个生命周期钩子,一些plugin会起作用
compiler.hooks.environment.call();
compiler.hooks.afterEnvironment.call();
compiler.options = new WebpackOptionsApply().process(options, compiler);
} else {
throw new Error("Invalid argument: options");
}
// compiler = webpack(options); callback为undefine,这里没执行 暂时不管
if (callback) {
..............
}
return compiler;
};
代码很短,也很简单,validateSchema(验证)和WebpackOptionsDefaulter(默认配置)我就不分析了,因为我主要关注编译过程。对compiler = new Compiler(options.context);这一行下断点,f7跳入分析Compiler的构造函数。
constructor(compiler) {
super();
// compiler的生命周期,重点分析一下
this.hooks = {
/** @type {SyncHook} */
buildModule: new SyncHook(["module"]),
/** @type {SyncHook} */
rebuildModule: new SyncHook(["module"]),
/** @type {SyncHook} */
failedModule: new SyncHook(["module", "error"]),
/** @type {SyncHook} */
succeedModule: new SyncHook(["module"]),
...............省略一些hook,实在是太多了
};
// 一些属性,暂时不做分析
/** @type {string=} */
this.name = undefined;
/** @type {Compiler} */
this.compiler = compiler;
this.resolverFactory = compiler.resolverFactory;
this.inputFileSystem = compiler.inputFileSystem;
this.requestShortener = compiler.requestShortener;
const options = compiler.options;
this.options = options;
this.outputOptions = options && options.output;
..........
全是一些属性,暂时不做分析
}
this.hooks里包含了compiler所有的生命周期(类似于vue的生命周期,但compiler的生命周期是真的复杂),这里用到了SyncHook等几个类,下面就说一下这几个Hook类。
tapable这个包提供了几个Hook类,能够用来给plugin创建hook点。这里提前说一下,webpack的plugin的工作方式就是对compiler的生命周期进行hook,在某个编译阶段对源码进行一系列的处理。compiler本身只是一个架子,一个状态机而已。compiler.run()就会推动生命周期往下走,而具体的编译工作都由各种plugin实现。这就是为什么webpack有那么多的plugin,让人头皮发麻。webpack4.0采用了大量的默认配置,做到了开箱即用,简单了不少。回到这一节的内容,tapable的文档已经写的很明白了,大家可以去github上看,这里做一个简单的解释。tapable的源码分析这里就不去写了,因为和webpack的编译流程关系不大。
直接上demo代码和注释
const myHook = new SyncHook();// 创建一个hook对象
// 添加一个监听,第一个参数是一个随意的名字用来标识,第二个参数是该监听的回调函数
myHook.tap('helloPlugin', () => {console.log('hello world')})
myHook.tap('helloPlugin2', () => {console.log('hello world2')})
// 触发hook,所有的监听函数被执行
myHook.call()
加上参数
const myHook = new SyncHook(['message']);// 创建一个hook对象
// 添加一个监听,第一个参数是一个随意的名字用来标识,第二个参数是该监听的回调函数
myHook.tap('echoPlugin', (message) => {console.log(message)})
myHook.tap('echoPlugin2', (message) => {console.log(message + ' twice')})
// 触发hook,所有的监听函数被执行
myHook.call('hello world')
上面两个都是同步的Hook,回调函数里都是同步执行的代码,下面再来看一下异步Hook
const findRoute = new AsyncParallelHook(["source", "target", "routesList"]);// 创建一个异步hook对象
// 添加一个监听,第一个参数是一个随意的名字用来标识,第二个参数是该监听的回调函数
findRoute.tapPromise("GoogleMapsPlugin", (source, target, routesList) => {
// 返回一个Promise
return google.maps.findRoute(source, target).then(route => {
routesList.add(route);
});
});
// 触发hook,所有的监听函数被执行
findRoute.promise(source, target, routesList).then(() => {
// do something
})
不使用Promise的异步Hook,使用callback模式
const findRoute = new AsyncParallelHook(["source", "target", "routesList"]);// 创建一个异步hook对象
// 添加一个监听,第一个参数是一个随意的名字用来标识,第二个参数是该监听的回调函数,回调函数里最后加上一个callback参数,用于异步操作完成后执行回调
findRoute.tapAsync("GoogleMapsPlugin", (source, target, routesList, callback) => {
bing.findRoute(source, target, (err, route) => {
if(err) return callback(err);
routesList.add(route);
// 找到路径后执行callback
callback();
});
});
// 触发hook,所有的监听函数被执行,最后一个参数传入回调函数,用于被各个异步的监听函数回调
findRoute.callAsync(source, target, routesList,(err) => {
// do some thing
})
所以在webpack的plugin里,主要就是
apply (compiler) {// plugin的入口函数
// 对compiler的生命周期进行监听
compiler.hooks.xxxx1.tap('xxPlugin', (compilation) => {
})
compiler.hooks.xxxx2.tap('xxPlugin', (compilation) => {
})
}
在compiler.run里主要就是
// 触发监听函数
compiler.hooks.xxx1.call(compilation)
compiler.hooks.xxx2.call(compilation)
这么一看,编译功能的实现都在各个plugin里,我猜loader的调用,依赖图的构建,抽象语法树的生成等,搞不好都是在某几个plugin里,下面的工作就是把几个关键的plugin找出来,进行分析。