1、前文回顾
前文末尾调用 this.runWebpack 方法把前面的加载所得的 webpack 模块执行:代码如下
js
class WebpackCLI {
// ....
async run () {
// 加载 webpack
this.webpack = await this.loadWebpack();
}
async loadWebpack(handleError = true) {
// 等效于 require('webpack')
return this.tryRequireThenImport(WEBPACK_PACKAGE, handleError);
}
async runWebpack(options, isWatchCommand) {
let compiler;
const callback = () => {}
compiler = await this.createCompiler(options, callback);
}
async createCompiler(options, callback) {
let config = await this.loadConfig(options);
config = await this.buildConfig(config, options);
let compiler;
try {
compiler = this.webpack(config.options, callback)
} catch (e) {}
return compiler;
}
}
- 命令行: $ npm run dev
- 解析上面命令所得 webpack 命令执行
- webpack 命令解析到 webpack/bin/webpack.js 脚本
- webpack/bin/webpack.js 加载 webpack-cli/lib/index.js 创建 WebpackCLI 实例并调用 .run 方法
- WebpackCLI.prototype.run 加载加载 webpack/lib/webpack.js 得到 this.webpack
- 最后执行 this.runWebpack() 方法,进而执行 webpack/lib/webpack.js 导出的 webpack 方法创建 compiler 对象;
本文接上文继续讨论 webpack 启动后的的第一个环节------ 创建 compiler 实例对象。
2、创建 compiler
创建 compiler 的工作是由 webpack/lib/webpack.js 模块完成的;
以下为模块基础信息:
- 路径:webpack/lib/webpack.js
- 导出:函数 webpack
- 参数:
- options: 创建 compiler 所需要的选项对象
- callback: 创建 compiler 对象处理编译结果的回调函数,内部用 callback 决定是否调用 compiler.run 自动启动编译
下面我们讨论该方法内部实现的细节问题:
2.1 整体结构
js
const webpack = (options, callback) => {
const create = () => {};
if (callback) {
// 传递 callback 的情况
try {
const { compiler, watch, watchOptions } = create();
if (watch) {
compiler.watch(watchOptions, callback);
} else {
compiler.run((err, stats) => {});
}
return compiler;
} catch (err) {}
} else {
// 未传递 callback 的情况
const { compiler, watch } = create();
return compiler;
}
}
module.exports = webpack;
形参:
- options: 用户传入的创建编译器实例的配置项
- callback: 处理 webpack 编译工作的回调函数
实参:
- options: webpack.config.js 配置文件导出的配置选项
- callback: WebpackCLI.prototype.runWebpack 中创建的 callback 方法(见上文 前文回顾 下有关 WebpackCLI 代码)
具体工作: webpack 方法做了以下工作
- 声明具体创建 compiler 的 create 内部方法,该方法内部处理创建 compiler 逻辑;
- 判断是否传入了 callback 决定是否启用编译(调用 compiler.run 方法)
- 2.1 传入 callback,则调用私有的 create 方法创建 compiler 对象;接着判断是否是 watch 模式,若是则调用 compiler.watch 否则调用 compiler.run 启动编译;
- 2.2 如果没有传入 callback 说明不需要自动启用编译,则在创建 compiler 对象后直接返回;
这里我们属于第一种情况,这里默认的情况中,webpack-cli 是创建了 callback 这个回调的,需要自动开启编译,这个也符合我们的直觉; 另一种 需要外界传入 callback 的场景是给自定义编译的情况准备的,比如很多的跨端框架,包括封装了构建流程的脚手架比如 React 的 script 都是这个特性的典型应用场景;
2.2 create 方法
下面我们看下 create 方法:
js
const create = () => {
if (!asArray(options).every(webpackOptionsSchemaCheck)) {
getValidateSchema()(webpackOptionsSchema, options);
}
let compiler;
let watch = false;
let watchOptions;
if (Array.isArray(options)) {
// opitons 是数组,创建多编译器
compiler = createMultiCompiler(
options,
);
watch = options.some(options => options.watch);
watchOptions = options.map(options => options.watchOptions || {});
} else {
// options 不是数组,创建一个编译器实例就好了
const webpackOptions = options;
compiler = createCompiler(webpackOptions);
watch = webpackOptions.watch;
watchOptions = webpackOptions.watchOptions || {};
}
// 返回结果:
// compiler: Compiler 编译器实例
// watch: 是否是 watch 模式表示
// watchOptions: watchOptions watch 模式的选项对象
return { compiler, watch, watchOptions };
};
create 方法做了以下工作:
- 校验用户传递的 options 是否符合 webpack 内部约定好的 schema,如果不符合就会立刻跑出异常信息;这一点还挺好用的,业务中可以校验一波;
- 根据传入的 options 类型分类讨论:
- 2.1 如果传入的 options 是数组,说明本项目需要多编译器进行编译,则调用 createMultiCompiler 方法创建多编译器;然后判断这些 options 中是否存在需要 watch 模式的,最后再取得各个配置项中 watchOption 配置;
- 2.2 不是数组的情况,则说明是一个编译器的情况,则调用 createCompiler 方法创建编译器实例对象,然后获取是否 watch 模式表示以及 watchOptions 配置项目;
- 返回前面两步创建的 compiler,watch,watchOptions
- 3.1 compiler: Compiler 编译器实例
- 3.2 watch: 是否是 watch 模式表示
- 3.3 watchOptions: watchOptions watch 模式的选项对象
我们这里讨论的是创建单个 Compiler 实例对象的情况,所以我们暂时不展开 createMultiCompiler 方法,只表 createCompiler 方法;
2.3 createCompiler 方法
createCompiler 函数创建了一个编译器对象,并根据传入的 rawOptions 配置对 compiler 进行初始化。接着设置了一些默认选项、初始化插件,并调用了一些钩子函数,最后返回 compiler 对象。
js
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;
};
下面我们来看这个方法的具体工作:
- 调用 getNormalizedWebpackOptions 格式化 rawOptions,返回经过处理的 webpack 标准配置对象,用以抹平各种语法的配置对象;
- 经过上一步拿到标准的 webpack 配置选项对象后,调用 applyWebpackOptionsBaseDefaults 进行基础配置如 context/target/mode 等选项的初始化,比如 context 的值被设置为当前的工作目录;
- 创建 Compiler 对象实例,传入经过处理的 options 对象;
- 初始化 NodeEnvironmentPlugin 插件,NodeEnvironmentPlugin 是 Compiler 工作中相当依赖的一个模块,主要负责文件 I/O、监听文件内容改变。由四个基本的文件系统组成:inputFileSystem、outputFileSystem 处理文件i/o,watchFileSystem 监听文件改动,intermediateFileSystem则处理所有不被看做是输入或输出文件系统操作,比如写记录,缓存或者分析输出等。
- 初始化插件调用,webpack 内部插件以及我们自定义的插件上定义的 apply 方法就是在此时调用的,调用这些插件的 apply 方法时会传入 compiler 对象,通过 compiler 对象则可以注册全生命周期的钩子;
- 调用 applyWebpackOptionsDefaults 方法对 webpack 设置默认选项,根据传入的配置对象 options,对设置webpack配置的默认值,根据不同的配置项进行赋值。
- 调用 new WebpackOptionsApply().process(options, compiler) 方法,利用 WebpackOptionsApply 类型完成 compiler 特性的应用工作。webpack 高度封装了内部特性,外部配置中的每个 key 对应的特性都是由一个或者组合多个插件实现,而 WebpackOptionsApply 就是根据 options 注册对应的插件,这个过程像不像
中医照方子抓药
的过程,所谓"方子"就是 "options" 配置,而"插件"就是各式各样的"中药材"; - 触发 compiler.hooks.environment、compiler.hooks.afterEnvironment、compiler.hooks.initialize 钩子;
- 返回 compiler 对象
2.4 WebpackOptionsApply
- 注册 JavascriptModulesPlugin、JsonModulesPlugin、WebAssemblyModulesPlugin 插件,为不同类型的模块注册 Parser 和 Generator;
- 为 entry 声明 EntryPlugin;
- 注册性能优化相关 ModuleConcatenationPlugin/SplitChunkPlugin,当然还有 treeshaking 相关的,这里没有列出;
- 处理 if (options.cache && ... 开启持久化缓存;
3、总结
本文主要讨论了 webpack/lib/webpack.js 导出的方法 webpack 及其创建 Compiler 实例(编译器)对象的主要过程,主要集中在 createCompiler 方法中,这篇你需要了解的点有以下这些:
- 创建编译器会根据 options 是否为数组决定创建多编译器或者是单独编译器,当然后面我们的主要精力都在单编译器实例的情境中;
- applyWebpackOptionsBaseDefaults 方法进行基础配置如 context/target/mode 等选项的初始化,比如 context 的值被设置为当前的工作目录;
- 通过 Compiler 这个类型实例化一个 compiler 实例对象;
- 用户定义的插件在 Compiler 实例化之后被应用;
- 调用 applyWebpackOptionsDefaults 方法对 webpack 设置默认选项;
- 调用 new WebpackOptionsApply().process(options, compiler) 方法,利用 WebpackOptionsApply 类型依据前面得出的 options 进行默认插件引用以及一些内建特性的应用;
- compiler.resolverFactory.hooks.resolveOptions.for() 为 normal/loader/context 声明创建 Resolver 路径解析器所需的配置项;