引言
本文章基于webpack5源码进行解读,编写本文仅为方便作者学习以及记忆,如果有什么说的不对或者瞎猜的不对的地方,请大家指出,请勿乱喷,大家都是自己人感谢大家~
接上回
首先我们基于上回的一些内容,我们继续开始画我们图~
我们找到
webpack-cli
构造函数中的这一段代码this.webpack = require(process.env.WEBPACK_PACKAGE || 'webpack');
,就知道我们的this.webpack
实际上就拉取自node_modules
中的webpack
,那么我们首先关注到node_modules/webpack/package.json
的main
字段
这边可以定位到我们的入口文件就是node_modules/webpack/lib/index.js
,接着我们打开文档文件进行查看,文件主要是做了大量的参数入口声明,这边我们简化了一下文件,讲一些我们的关键路径
webpack/lib/index.js
//node_modules/webpack/lib/index.js
const util = require("util");
const memoize = require("./util/memoize");
// 实际上通过柯里化的方式去做了一个懒加载,等函数真的被调用才去进行拉取
const lazyFunction = factory => {
// memoize 内部实现就比较简单,用闭包的方式针对factory做了个封装,缓存了factory调用的结果
const fac = memoize(factory);
const f = /** @type {any} */ (
(...args) => {
return fac()(...args);
}
);
return /** @type {T} */ (f);
};
/**
* @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));
};
// 包装webpack的拉取方法
const fn = lazyFunction(() => require("./webpack"));
// 合并对象并且使合并出来对外的对象成为不可变对象
module.exports = mergeExports(fn, {
get webpack() {
return require("./webpack");
},
...
get cli() {
return require("./cli");
},
...
// 这里省略了很多很多的声明
})
实际看完这段代码明白当前入口文件就做了三件事情:
- 暴漏webpack包中大量属性方法的入口
- 缓存/懒加载导出对象
- 修改暴露属性的叙述属性,让其不可变
webpack/lib/webpack.js
另外关键路径为node_modules/webpack/lib/webpack.js
,因为我们在webpack-cli
中用用到的主要是暴漏构建函数,构建函数很明显是从上方代码的const fn = lazyFunction(() => require("./webpack"))
获取的那么我们继续往下走(文件较大,为了保持关注点,我还是拆分了一下,我们还是按照顺序来看)
// node_modules/webpack/lib/webpack.js
const webpack = ((options, callback) => {
const create = () => {
... // 这里先省略
}
// 这里callback并不是我们原生设定的,而是来自于node_modules/webpack-cli/lib/webpack-cli.js 的 1847行,感兴趣可以看一下,看过来主要是一些错误处理以及日志输入,我们继续
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);
});
});
}
// 无论是否输出watch都会返回一个compiler,虽然我们暂时不知道compiler是什么不过我们可以慢慢了解一下~
return compiler;
} catch (err) {
process.nextTick(() => callback(err));
return null;
}
} else {
const { compiler, watch } = create();
if (watch) {
util.deprecate(
() => {},
"A 'callback' argument need to be provided to the 'webpack(options, callback)' function when the 'watch' option is set. There is no way to handle the 'watch' option without a callback.",
"DEP_WEBPACK_WATCH_WITHOUT_CALLBACK"
)();
}
// 这里跟没有call的场景是一样,同样都是返回一个compiler
return compiler;
}
});
那么我们看完这一小段代码,实际上知道的是我们webpack(...)
的这个函数,最主要的作用就是输出一个compiler
,然后根据不同场景回调用compiler
的run(...)
和watch(...)
这两个不同的方法,并且挂载我们传入的callback(...)
, 那么在生产compiler
的过程中,最重要的不外乎我们省略的create(...)
函数中的内容,那么下面我们来看下create(...)
函数做什么!
// node_modules/webpack/lib/webpack.js webpack(...)=>create(...)
const create = () => {
/*
* 这里做了两次校验,一个使用的是检验包,一个是用的"node_modules/schema-utils/dist/index.js",当然这里作者没有细看详细的一些差距或者内容
* 基本为作者猜测,检查方式看起来都是通过JSON Schema规则进行的,有可能第一层检查速度较快,第二层检查更加全面~
*/
if (!webpackOptionsSchemaCheck(options)) {
getValidateSchema()(webpackOptionsSchema, options);
}
let compiler;
let watch = false;
let watchOptions;
/*
* 这里判断options是否为数组,根据官方文档的说明,webpack会为每一个配置去生产一个compiler
* 也就是针对一个生产过程,去制造一个流水线,这个很好理解,毕竟不同的产品所需要的流水线是不一样的
* 那么后续我们在理解上面,先暂时的将compiler理解为一个“生产线”
*/
if (Array.isArray(options)) {
compiler = createMultiCompiler(
options,
options
);
watch = options.some(options => options.watch);
watchOptions = options.map(options => options.watchOptions || {});
} else {
/*
* 那么根据我们的初始化配置我们将会进入到这个流程
* 根据代码我们知道关键路径一定是createCompiler这个听起来相当工厂的方法
* 我们下一段就直接放这一块相关的代码吧
*/
const webpackOptions = options;
compiler = createCompiler(webpackOptions);
watch = webpackOptions.watch;
watchOptions = webpackOptions.watchOptions || {};
}
return { compiler, watch, watchOptions };
};
// node_modules/webpack/lib/webpack.js createCompiler(...)
const createCompiler = rawOptions => {
/*
* 首先进入扁平化方法,大部分options类的三方公共库,都会存在的normalize, 那么我们主要关注前后差异即可
* 那么我们这里的rawOptions是跟我们在最外层的webpack.config.js一致的吗?
* 其实不是在这里我们options已经被webpack-cli做了一些初始化操作,比如plugins 以及 stats,我给大家在下面贴了本人debug的环境数据,大家对照的看就行
*/
const options = getNormalizedWebpackOptions(rawOptions);
/*
* 大家自行DEBUG以后会发现options上多了很多一些webpack官方的配置项,那这里就不废话继续向下
* applyWebpackOptionsBaseDefaults(...) 中主要基于process做了context的值绑定
* 以及看起来像是日志参数的初始化操作 applyInfrastructureLoggingDefaults(...)
*/
applyWebpackOptionsBaseDefaults(options);
/*
* Compiler的构造函数,options.context是刚才在 applyWebpackOptionsBaseDefaults中赋值的() => process.cwd()
* Compiler的构造函数中主要做了大量的参数初始化操作,这里会放在下一个代码段
*/
const compiler = new Compiler(options.context);
// 赋值options(废话一句)
compiler.options = options;
/*
* 在这里传入的infrastructureLogging参数就是来自于applyInfrastructureLoggingDefaults(...) 所初始化的部分日志参数
* NodeEnvironmentPlugin 主要做三件事情:
* 1. 文件读取相关的方法挂载,缓存,操作
* 2. 初始化文件监听(Watchpack)
* 3. 挂载插件hook beforeRun
*/
new NodeEnvironmentPlugin({
infrastructureLogging: options.infrastructureLogging
}).apply(compiler);
// 执行之前挂载的CLI的插件方法,主要是一些控制台相关的内容node_modules/webpack-cli/lib/plugins/CLIPlugin.js 感兴趣的同学可以去看看
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);
// 调用所有“environment”的hook
compiler.hooks.environment.call();
// 调用所有“afterEnvironment”的hook
compiler.hooks.afterEnvironment.call();
// WebpackOptionsApply 中了大量的根据options初始化插件的操作,包括相关钩子的触发,各类关键节点的钩子初始化如果找不到的话 都可以看看这里
new WebpackOptionsApply().process(options, compiler);
// 调用所有“initialize”的hook
compiler.hooks.initialize.call();
// 返回compiler
return compiler;
};
rawOptions
数据内容~
好的,跟到这里我们发现,当前方法并没有触发compiler
进入一个编译环节,主要还是做了全量的一个初始化方案。然后跟着调试我们会发现,关键路径实际上是在于compiler.run(...)
,那么我们下一章节,就主要来看看 从compiler.run(...)
的执行过程中究竟发生了什么~
下一章节地址: 暂未发布