前言#
上篇说完了webpack-cli的执行顺序,还留了两个地方没有说明,一个是loadWebpack时发生了什么,另一个是createCompiler创建compiler的中间发生了什么。这里就涉及到这篇文章要说的。
如果没看过前面,可以先看下。
loadWebpack做了什么#
先回到webpack-cli/lib/webpack-cli文件内,找到loadCommandName方法 里的
...省略
if (isBuildCommandUsed || isWatchCommandUsed) {
await this.makeCommand(isBuildCommandUsed ? buildCommandOptions : watchCommandOptions, async () => {
this.webpack = await this.loadWebpack();
...省略
}
this.webpack = await this.loadWebpack()
const WEBPACK_PACKAGE = process.env.WEBPACK_PACKAGE || "webpack";
async loadWebpack(handleError = true) {
return this.tryRequireThenImport(WEBPACK_PACKAGE, handleError);
}
async tryRequireThenImport(module, handleError = true) {
let result;
try {
result = require(module);
}
catch (error) {
...省略,尝试ESM导入
}
...省略
return result || {};
}整理下就是以下这段代码,返回require('webpack')
this.webpack = (() => (require('webpack')))();先看下返回了什么

自带一大串属性的function
看这些属性你会发现很多眼熟的字眼,为什么会有这些东西,让我们进入到文件内看下是怎么回事
进入webpack/lib/index.js文件里#
一进来会发现有这么一段代码。
const fn = lazyFunction(() => require("./webpack"));再加上lazyFunction和memoize就变得很绕。
/** @template T @typedef {function(): T} FunctionReturning */
/**
* @template T
* @param {FunctionReturning<T>} fn memorized function
* @returns {FunctionReturning<T>} new function
*/
const memoize = fn => {
let cache = false;
/** @type {T} */
let result = undefined;
return () => {
if (cache) {
return result;
} else {
result = fn();
cache = true;
// Allow to clean up memory for fn
// and all dependent resources
fn = undefined;
return result;
}
};
};
/**
* @template {Function} T
* @param {function(): T} factory factory function
* @returns {T} function
*/
const lazyFunction = factory => {
const fac = memoize(factory);
const f = /** @type {any} */ (
(...args) => {
return fac()(...args);
}
);
return /** @type {T} */ (f);
}; 这块的作用看名字就知道是懒加载webpack模块,来具体分析下这一段做了什么。
先单独看下memoize:函数包裹缓存fn的执行结果 。相当于以下这段代码(不包含缓存逻辑),fn也被缓存了,所以赋值undefined
const memoize = (fn) => () => (fn());然后再单独看下lazyFunction:有些绕,相当于下面这段代码
const factory = () => require('webpack') // 这里webpack导出的也是一个function
//所以相当于 const factory = () => ((args) => {})
const lazyFunction = ((args) => {}) => {
const fac = () => ((args) => {});
const f = (...args) => ((args) => {})(...args)
return f
}你会发现绕了一大圈,还是() => fn(),但实际上是做了缓存效果 ,至于为什么要多嵌套一层f而不是直接在memoize里直接传入...args变成 const memoize = (fn) => (...args) => (fn()(...args)); 我猜是关注点分离?如果大佬知道是为了什么麻烦评论区告诉我一声。
接着往下看,找到module.exports
/**
* @template A
* @template B
* @param {A} obj input a
* @param {B} exports input b
* @returns {A & B} merged
*/
const mergeExports = (obj, exports) => {
const descriptors = Object.getOwnPropertyDescriptors(exports);
for (const name of Object.keys(descriptors)) {
const descriptor = descriptors[name];
if (descriptor.get) {
const fn = descriptor.get;
Object.defineProperty(obj, name, {
configurable: false,
enumerable: true,
get: memoize(fn)
});
} else if (typeof descriptor.value === "object") {
Object.defineProperty(obj, name, {
configurable: false,
enumerable: true,
writable: false,
value: mergeExports({}, descriptor.value)
});
} else {
throw new Error(
"Exposed values must be either a getter or an nested object"
);
}
}
return /** @type {A & B} */ (Object.freeze(obj));
};
module.exports = mergeExports(fn, {
get webpack() {
return require("./webpack");
},
...省略,类似的get然后return
})fn前面分析过了,最终是长这样(忽略了缓存逻辑的):(...args) => (() => (require('webpack'))()(...args))mergeExports第二个参数是一个对象,一大段的get- Object.getOwnPropertyDescriptors: 获取一个对象的所有自身属性的描述符,看下例子数据

const fn = descriptor.get;
Object.defineProperty(obj, name, {
configurable: false,
enumerable: true,
get: memoize(fn)
});
} else if (typeof descriptor.value === "object") {
Object.defineProperty(obj, name, {
configurable: false,
enumerable: true,
writable: false,
value: mergeExports({}, descriptor.value)
});Object.defineProperty相信大家都很熟了,如果不熟,看下**这里。**get: memoize(fn)这么做有两个好处。1、只有在第一次读这个属性并且执行了才会去加载这个模块/插件;2、缓存加载了的模块/插件
说了一大堆,来总结下webpack-cli/lib/webpack-cli.js里的loadWebpack方法做了什么,也就是webpack/lib/index.js做了什么。
- 对
require('webpack')做了懒加载 + 缓存处理,最终是一个function:(..args) => (()=>(require('webpack'))()(..args))(同样是省略了缓存逻辑)。 - 将一堆内置的
plugin、object等绑定到这个function上,plugin同样是懒加载 + 缓存 - 导出这个
function,赋值到webpack-cli实例上,这个时候的webpack并没有加载。
那何时加载的呢?
回到webpack-cli/lib/webpack-cli.js里#
loadwebpack方法做了什么已经知道,接着就是到什么时候去执行了this.webpack()。通过文件里匹配,发现只有一处地方执行了
async createCompiler(options, callback) {
if (typeof options.nodeEnv === "string") {
process.env.NODE_ENV = options.nodeEnv;
}
let config = await this.loadConfig(options);
config = await this.buildConfig(config, options);
let compiler;
try {
compiler = this.webpack(config.options, callback
? (error, stats) => {
if (error && this.isValidationError(error)) {
this.logger.error(error.message);
process.exit(2);
}
callback(error, stats);
}
: callback);
// @ts-expect-error error type assertion
}正好也是compiler被创建的时候,而此时的webpack才是第一次加载。
接着我们跟着调用栈进入webpack/lib/webpack.js里#
const webpack = (
(options, callback) => {
const create = () => {
...省略验证options
let compiler;
...省略当options是数组的情况
const webpackOptions =(options);
compiler = createCompiler(webpackOptions);
watch = webpackOptions.watch;
watchOptions = webpackOptions.watchOptions || {};
return { compiler, watch, watchOptions };
};
if (callback) {
try {
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;
} catch (err) {
process.nextTick(() => callback(err));
return null;
}
}
}
...省略没有callback的场景
);这一大段只有一个重点:createCompiler
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;
}; 我们来一点一点分析下这个方法,先看下rawOptions是什么

一看就能知道是之前从webpack.config.xx里拿的配置数据
getNormalizeWebpackOptions方法用于整合合并默认参数,由于篇幅问题,这里只展示合并后的options

applyWebpackOptionsBaseDefaults(options)也不多说,就是将当前的路径赋值到options.context上

- 接着是
const compiler = new Compiler(options.context, options);创建compiler实例 ,这里先不分析,后面再分析,简单的说就是初始化hooks和属性,看下数据

- 接着
new NodeEnvironmentPlugin({infrastructureLogging: options.infrastructureLogging}).apply(compiler);创建一个nodeEnvironmentPlugin实例 ,这是一个内置plugin。
nodeEnvironmentPlugin简单的说做了3件事
1、 就是先是为compiler创建一个基于node的fs模块(graceful-fs)封装的CachedInputFileSystem,对部分fs的方法多了一层缓存处理。

2、基于watchPack做了对文件的监听,创建一个watchFileSystem实例。
https://www.npmjs.com/package/watchpackwww.npmjs.com/package/watchpack
3、监听tapable的beforeRun的hooks,清空缓存。
tapablegithub.com/webpack/tapable
后面有机会会写一篇关于它的分析。相关链接 :
Internal webpack plugins | webpackv4.webpack.js.org/plugins/internal-plugins/
ok,扯远了,跳回到createCompiler方法里
- 接着判断传入的
options.plugins是否是数组并让compiler实例调用插件。 applyWebpackOptionsDefaults(options)简单的说就是对空值进行保护填补默认值,同时补充配置属性(很多很长,感兴趣的大佬可以去看下)。compiler.hooks.environment.call(),触发监听这个生命周期的所有方法,在初始化完配置文件中的插件之后。compiler.hooks.afterEnvironment.call();,触发监听这个生命周期的所有方法,在environment生命周期执行完后执行。

new WebpackOptionsApply().process(options, compiler);很长,简单的说就是根据前面处理完毕之后的options选项开始加工、加载内部plugin等,同时触发几个生命周期
entryOption: 在入口配置信息都被加工后(翻译的应该不太对,可以看下文档)。afterPlugins: 在内部插件都初始化完后。afterResolvers:在 处理器都设置完之后。
compiler.hooks.initialize.call();在compiler初始化完毕后触发监听这个事件的事件 。
至此,createCompiler已经分析完毕
总结下这个方法:
- 对配置文件获得的options进行合并格式化、空值保护。
- 创建
compiler实例,对hooks等初始化。 - 封装
node环境下的fs文件操作相关api,进行缓存、文件监听等。 - 执行我们在配置文件里传入的
plugin。 - 对
options进行补充。 - 触发
environment和afterEnvironment两个hook,执行监听生命周期的事件。 - 加工配置文件里的信息,触发
entryOption生命周期hook,执行监听它的事件。 - 加载内置
plugin,触发afterPlugins生命周期hook,执行监听它的事件。 - 注册处理器
resolver,触发监听afterResolvers生命周期hook的事件。 compiler初始化完毕,触发initialize生命周期hook,执行监听它的事件。
接着,让我们返回webpack/lib/webpack文件里的webpack方法,在执行完毕createCompiler方法之后
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; 忽略watch,我们是执行build的指令,所以实际上这段只有compiler.run。让compiler跑起来之后并返回就完成了webpack.js的任务。
总结#
这里我们跟踪分析了在webpack-cli/lib/webpack-cli文件内的loadWebpack和createCompiler两个方法。
loadWebpack方法require了webpack/lib/index.js文件,里面又做了两件事:
- 对
require('webpack/cli/webpack')进行懒加载 + 缓存,直到第一次执行时才加载并且缓存掉。 - 将各种内部
plugin和数据绑定到上面这个懒加载方法里,并且也是懒加载+缓存,只有第一次读并且执行才会加载插件。
\2. createCompiler方法执行1里面提到的webpack,然后创建compiler实例,至于在执行过程中发生了什么请网上拉,上面总结了。
至于compiler.run做了什么,以及前面的new compiler发生了什么,下一篇分析。
喜欢的话,麻烦点个赞,谢谢!
参考#
- webpack的hooks https://webpack.js.org/api/compiler-hooks/
编辑于 2022-08-25 17:27
