前言
本篇紧接着上篇初始化参数,上篇文章中主要查看bin/webpack.js文件中合并相关命令行参数的逻辑得知下一个逻辑入口即webpack函数执行以及相关Compiler对象方法的执行。
本篇主要梳理webpack函数执行过程中的相关的逻辑。
流程逻辑
流程分析的实例webpack配置:
const path = require('path');
module.exports = {
// JS 执行入口文件
entry: './src/main.js',
output: {
// 把所有依赖的模块合并输出到一个 bundle.js 文件
filename: 'bundle.js',
// 输出文件都放到 dist 目录下
path: path.resolve(__dirname, '../dist'),
}
};
首先查看lib/webpack.js文件,该文件主要导出webpack函数以及该函数的相关方法,同时还有webpack的所有内置插件,例如:
exports = webapck;
exportPlugins(exports, {
// 环境变量定义插件
"DefinePlugin": () => require("./DefinePlugin")
});
exportPlugins(exports.optimize = {}, {
// 代码分割插件
"CommonsChunkPlugin": () => require("./optimize/CommonsChunkPlugin"),
// 压缩插件
"UglifyJsPlugin": () => require("./optimize/UglifyJsPlugin")
});
从上面可以得知输出webpack.*和webpack.optimize.*两类的插件。
回归到webpack函数这边,函数主要逻辑如下:
执行validateSchema函数
该函数的作用主要就是判断用户配置的webpack配置文件是否存在非法的格式或语法。
判断options是否是数组
webpack支持多种配置类型,其中就有配置多个配置对象即存在多个option,这里的逻辑就是判断是否是多个配置option从而调用不同的逻辑。
实际上多个配置option只是递归调用webpack函数而已,代码如下:
compiler = new MultiCompiler(options.map(options => webpack(options)));
所以主要关注还是单个option的具体处理逻辑,这部分代码比较少,就直接粘贴出来,如下:
new WebpackOptionsDefaulter().process(options);
compiler = new Compiler();
compiler.context = options.context;
compiler.options = options;
new NodeEnvironmentPlugin().apply(compiler);
if(options.plugins && Array.isArray(options.plugins)) {
compiler.apply.apply(compiler, options.plugins);
}
compiler.applyPlugins("environment");
compiler.applyPlugins("after-environment");
compiler.options = new WebpackOptionsApply().process(options, compiler);
WebpackOptionsDefaulter对象构建
WebpackOptionsDefaulter来自于同目录下/lib/WebpackOptionsDefaulter.js,该文件中定义了WebpackOptionsDefaulter类。
class WebpackOptionsDefaulter extends OptionsDefaulter {}
WebpackOptionsDefaulter继承自OptionsDefaulter,父类定义了process方法和相关的set方法,同时定义两个实例属性对象config和defaults,而WebpackOptionsDefaulter类初始化的工作有两点:
- 将一些默认项写入defaults中
- 标记一些特殊的配置项,同时写入config实例对象中
之后调用process方法做相关处理:
process(options) {
for(let name in this.defaults) {
switch(this.config[name]) {
case undefined:
if(getProperty(options, name) === undefined)
setProperty(options, name, this.defaults[name]);
break;
}
}
}
需要注意的是,options是用户自己配置webpack项对应的配置对象。从上面的逻辑基本可知就是将一些默认项写入options对象中,得到webpack构建过程中必须的配置项即默认化参数处理。
Compiler相关
webpack函数中会调用Compiler类创建一个实例对象,相关Compiler初始化工作没有涉及到相关配置对象中内容的处理,这里就不具体展开得到具体调用相关方法时再有针对性的说明。
这里需要提及的一点是:Compiler继承自Tapable
Webpack本质上是一种事件流的机制,它的工作流程就是将各个插件串联起来,而实现这一切的核心就是Tapable,Tapable提供相应的钩子函数,webpack插件实现相应的功能就是依赖于此
当Compiler实例化一个compiler对象后,会对其context和options实例属性进行赋值:
compiler.context = options.context;
// options即写入了相关默认参数的配置对象
compiler.options = options;
之后的逻辑跟插件相关,主要的逻辑是:
new NodeEnvironmentPlugin().apply(compiler);
compiler.applyPlugins("environment");
compiler.applyPlugins("after-environment");
NodeEnvironmentPlugin对象
NodeEnvironmentPlugin类只有一个apply方法,主要是执行apply方法,该方法的逻辑主要是对compiler对象中相关实例属性进行赋值:
- inputFileSystem
- outputFileSystem
- watchFileSystem
- 注册before-run事件
这里主要关注注册before-run事件,tapable的_plugins是事件仓库,所有事件都注册在这里。
applyPlugins方法
这个方法是compiler继承自Tapable,该方法的作用就是执行注册在plugins中的回调函数。
Tapable.prototype.applyPlugins = function applyPlugins(name) {
// 判断是否注册了相应事件
if(!this._plugins[name]) return;
var args = Array.prototype.slice.call(arguments, 1);
var plugins = this._plugins[name];
for(var i = 0; i < plugins.length; i++)
plugins[i].apply(this, args);
};
这里的environment、after-environment事件在目前的逻辑流程中并没有被注册,所以并不会执行。
WebpackOptionsApply对象构建
WebpackOptionsApply类主要的作用就是依据options中相关字段做处理,调用相应插件的apply方法,做进一步的处理。
process(options, compiler) {
// 主要的逻辑就是对options中相应参数调用compiler.apply方法
compiler.apply(
// 相关插件对象
new LoaderTargetPlugin(options.target)
);
}
compiler.apply方法的逻辑就是调用对应插件的apply方法,即执行插件的逻辑。而这些插件的逻辑基本都是注册相应的事件到compiler的_plugins中,这里梳理了下上面实例webpack配置项下的WebpackOptionsApply对象的process方法注册的事件有:
- this-compilation
- compilation
- after-resolvers
- entry-option
- make
- after-emit
- watch-run
- run
- after-compile
上面的事件的含义可以查看webpack文档Compiler钩子相关的,可以具体去了解,这里暂时先不一一展开。
上面逻辑整个过程涉及到的插件数量比较多,弄清楚每个创建的细节实非易事,相对需要关注的点是调用compiler.applyPluginsBailResult方法的动作:
// 触发entry-option事件
compiler.applyPluginsBailResult("entry-option", options.context, options.entry);
Tapable.prototype.applyPluginsBailResult = funnction(name) {
// 调用_plugins中对应插件的apply方法
}
实际上这里就是执行之前entry-option注册的所有事件的callback。这里就需要关注下entry-option被注册时时哪个插件作用时的逻辑,通过源码可知是EntryOptionPlugin。
而EntryOptionPlugin类的主要作用就是:
判断entry的类型从而执行不同插件的逻辑
webpack的Entry配置支持三种类型:
- String,例如:entry: ‘./src/main.js’
- Object,例如:entry: { main: ‘./src/main.js’ }
- function即动态入口,例如:entry: () => ‘./src/main.js’
不同类型的entry由不同的插件来处理,这里涉及到:
- SingleEntryPlugin:单入口,处理String类型的entry
- MultiEntryPlugin:处理多入口的entry
- DynamicEntryPlugin:处理动态入口,即function类型的entry
这里以实例webpack配置来看:
entry: ‘./src/main.js’ 会调用SingleEntryPlugin插件的appy方法
SingleEntryPlugin
该对象的逻辑主要逻辑有两点:
- 调用compiler.plugin注册compilation 和 make事件
- 创建SingleEntryDependency对象即依赖
翻看相关webpack文档,可以看到类型这样的描述:
在 Webpack 里一切皆模块,一个模块对应着一个文件。Webpack 会从配置的 Entry 开始递归找出所有依赖的模块
compiler.plugin("make", (compilation, callback) => {
const dep = SingleEntryPlugin.createDependency(this.entry, this.name);
compilation.addEntry(this.context, dep, this.name, callback);
});
createDependency就是创建依赖对象的,从目前看来一个入口文件会对应一个dep对象,而这个dep对象可以表示是一个树结构来管理所有依赖的关系。
具体的逻辑还需要看看Compilation对象的addEntry内部涉及的逻辑,即make事件被触发。
MultiEntryPlugin的逻辑类似只是批量处理。
总结
webpack函数执行的主要逻辑是:
- 检查配置文件中是否存在非法或格式错误等的配置
- 创建Compiler对象
- 加载所有配置(自定义 + 默认额皮质)相关逻辑的插件执行对应的apply方法(主要是注册相关事件)
- 赋值Compiler对象的相关属性
- 返回初步被处理Compiler对象
webpack文档中对于Compiler的介绍:
Compiler 模块是 webpack 的支柱引擎,它通过 CLI 或 Node API 传递的所有选项,创建出一个 compilation 实例。它extend自 Tapable 类,以便注册和调用插件