写在前面
Webpack在整个打包过程中:
通过loader处理特殊类型资源的加载,例如加载样式、图片
通过plugin实现各种自动化的构建任务,例如自动压缩、自动发布
那么webpack的工作过程和原理又是如何实现的呢?
Webpack的工作过程
首先webpack会加载入口文件js,通过分析代码中import、require等去解析依赖,然后通过依赖形成依赖关系树,webpack会去遍历依赖关系树,去加载所依赖的资源模块。webpack会通过Loader配置去加载模块,通过plugins实现自动化构建。
对于依赖模块中无法通过js代码表示的资源模块,例如图片或字体文件,一般的Loader会将它们单独作为资源文件拷贝到输出目录中,然后将这个资源文件所对应的访问路径作为这个模块的导出成员暴露给外部。
webpack在每个打包环节都预留了钩子,我们可以通过plugins去配置其所依赖的插件。
具体的:
- Webpack cli启动打包流程
- 载入Webpack核心模块,创建Compiler对象
- 使用创建Compiler对象开始编译整个项目
- 从入口文件开始,解析模块依赖,形成依赖关系树
- 递归遍历依赖树,将每个模块交给对应的loader处理
- 合并loader处理完的结果,将打包结果输出到dist目录
Webpack cli的作用是将cli参数和webpack配置文件中的配置进行整合得到一个完整的配置对象。Webpack cli会通过yargs模块解析cli参数,运行webpack命令时通过命令行传入的参数。
- const config = { options: {}, path: new WeakMap() };
- // 判断是否指定了配置文件
- if (options.config && options.config.length > 0) {
- const loadedConfigs = await Promise.all(
- options.config.map((configPath) =>
- loadConfigByPath(path.resolve(configPath), options.argv),
- ),
- );
- config.options = [];
- loadedConfigs.forEach((loadedConfig) => {
- const isArray = Array.isArray(loadedConfig.options);
- // TODO we should run webpack multiple times when the `--config` options have multiple values with `--merge`, need to solve for the next major release
- if (config.options.length === 0) {
- config.options = loadedConfig.options;
- } else {
- if (!Array.isArray(config.options)) {
- config.options = [config.options];
- }
- if (isArray) {
- loadedConfig.options.forEach((item) => {
- config.options.push(item);
- });
- } else {
- config.options.push(loadedConfig.options);
- }
- }
- if (isArray) {
- loadedConfig.options.forEach((options) => {
- config.path.set(options, loadedConfig.path);
- });
- } else {
- config.path.set(loadedConfig.options, loadedConfig.path);
- }
- });
- config.options = config.options.length === 1 ? config.options[0] : config.options;
- } else {
- // 按照配置文件规则找到加载配置文件
- // Order defines the priority, in decreasing order
- const defaultConfigFiles = [
- "webpack.config",
- ".webpack/webpack.config",
- ".webpack/webpackfile",
- ]
- .map((filename) =>
- // Since .cjs is not available on interpret side add it manually to default config extension list
- [...Object.keys(interpret.extensions), ".cjs"].map((ext) => ({
- path: path.resolve(filename + ext),
- ext: ext,
- module: interpret.extensions[ext],
- })),
- )
- .reduce((accumulator, currentValue) => accumulator.concat(currentValue), []);
- let foundDefaultConfigFile;
- for (const defaultConfigFile of defaultConfigFiles) {
- if (!fs.existsSync(defaultConfigFile.path)) {
- continue;
- }
- foundDefaultConfigFile = defaultConfigFile;
- break;
- }
- if (foundDefaultConfigFile) {
- const loadedConfig = await loadConfigByPath(foundDefaultConfigFile.path, options.argv);
- config.options = loadedConfig.options;
- if (Array.isArray(config.options)) {
- config.options.forEach((item) => {
- config.path.set(item, loadedConfig.path);
- });
- } else {
- config.path.set(loadedConfig.options, loadedConfig.path);
- }
- }
- }
开始载入webpack核心模块,传入配置选项,创建Compiler对象。
- // 创建Compiler对象的函数
- 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 {
- // 开始调用webpack核心模块
- 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,
- );
- } catch (error) {
- if (this.isValidationError(error)) {
- this.logger.error(error.message);
- } else {
- this.logger.error(error);
- }
- process.exit(2);
- }
- // TODO webpack@4 return Watching and MultiWatching instead Compiler and MultiCompiler, remove this after drop webpack@4
- if (compiler && compiler.compiler) {
- compiler = compiler.compiler;
- }
- return compiler;
- }
make阶段
make阶段主体的目标是:根据entry配置找到入口模块,开始依次递归出所有依赖,形成依赖关系树,然后递归到的每个模块交给不同的loader处理。
- // 多路打包
- if (Array.isArray(options)) {
- await Promise.all(
- options.map(async (_, i) => {
- if (typeof options[i].then === "function") {
- options[i] = await options[i];
- }
- // `Promise` may return `Function`
- if (typeof options[i] === "function") {
- // when config is a function, pass the env from args to the config function
- options[i] = await options[i](argv.env, argv);
- }
- }),
- );
- } else {
- // 单线打包
- if (typeof options.then === "function") {
- options = await options;
- }
- // `Promise` may return `Function`
- if (typeof options === "function") {
- // when config is a function, pass the env from args to the config function
- options = await options(argv.env, argv);
- }
- }
默认使用的就是单一入口打包的方式,所以这里最终会执行其中的SingleEntryPlugin。
- SingleEntryPlugin中调用了Compilation对象的addEntry方法,开始解析入口。
- addEntry方法中又调用了_addModuleChain方法,将入口模块添加到模块依赖列表。
- 然后通过Compilation对象的buildModule方法进行模块构建
- buildModule方法中执行具体的Loader,处理特殊资源加载
- build完成后,通过acorn库生成模块代码的AST语法树
- 根据语法树分析这个模块是否还有依赖的模块,如果有则继续循环build每个依赖
- 所有依赖解析完成,build阶段结束
- 最后合并生成需要输出的bundle.js写入目录
参考文章
《webpack原理与实践》
《webpack中文文档》
写在最后
本文主要说明了webpack的工作过程和原理是如何实现的,并且对部分源码进行了分析,源码相当于牛津词典,你不可能专门单独设定时间去阅读,而应该是需要什么查阅什么,带着目的性去学习。