【源码】webpack5 编译主流程

本文将向大家介绍当我们执行webpack命令的时候,整个流程主要做了哪些事情,用于更好的理解主流程相关节点。

webpack版本

  • 5.90.3

一、执行webpack命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const path = require('path')
module.exports = {
context: __dirname,
mode: 'development',
devtool: 'source-map',
entry: './src/index.js',
output: {
path: path.join(__dirname, './dist'),
},
module: {
rules: [
{
test: /\.js$/,
use: ['babel-loader'],
exclude: /node_modules/,
}
]
}
}

简单写了个webpack.config.js文件,执行 webpack –config webpack.config.js 命令实际上是通过 webpack-cli 实现的。在 webpack-cli 中,调用了 webpack 方法,并传入了一个 callback 函数,用于执行终端的后续操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
async loadWebpack(handleError = true) {
return this.tryRequireThenImport(WEBPACK_PACKAGE, handleError);
}

this.webpack = await this.loadWebpack();

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);

在 webpack 方法中,根据是否传入了 callback 函数,分别进行了不同的处理。如果传入了 callback,则创建编译器实例并执行编译过程,最后调用 callback 函数。如果没有传入 callback,则仅创建编译器实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const webpack = (options, callback) => {
const create = () => {

// ...

const webpackOptions = (options);
compiler = createCompiler(webpackOptions);

return { compiler, watch, watchOptions };
};
if (callback) {
try {
const { compiler, watch, watchOptions } = create();

compiler.run((err, stats) => {
compiler.close(err2 => {
callback();
});
});
return compiler;
} catch (err) { }
} else {
const { compiler, watch } = create();
return compiler;
}
}

module.exports = webpack;

总的来说,webpack-cli 的作用是调用 webpack 方法,并传入相应的配置选项,来执行编译过程,并在需要时执行后续操作。

二、创建编译器实例:createCompiler()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const createCompiler = rawOptions => {
const options = getNormalizedWebpackOptions(rawOptions);
const compiler = new Compiler(options.context, options);
if (Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
if (typeof plugin === "function") {
(plugin).call(compiler, compiler);
} else if (plugin) {
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配置选项。该函数的作用是根据传入的配置选项创建并配置一个Webpack编译器实例,并返回该实例。下面将逐行讲解。

getNormalizedWebpackOptions

1
const options = getNormalizedWebpackOptions(rawOptions);

对原始选项进行规范化处理,得到标准化的 webpack 配置选项对象,并将其赋值给 options 变量。举例如下:

这里仅仅只是简单的赋值,并没有进行相应的必填校验

new Compiler();

1
const compiler = new Compiler(options.context, options);

通过Compiler类创建一个新的编译器实例

plugin.apply();

1
2
3
4
5
6
7
8
9
if (Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
if (typeof plugin === "function") {
(plugin).call(compiler, compiler);
} else if (plugin) {
plugin.apply(compiler);
}
}
}

遍历插件选项,如果插件是一个函数,那么调用该函数。如果插件是一个对象,则调用它的 apply 方法。

applyWebpackOptionsDefaults

1
applyWebpackOptionsDefaults(options);

applyWebpackOptionsDefaults的作用是应用 webpack 配置选项的默认值,以确保配置对象中包含必要的属性,并且没有缺失或不合法的值。

例句源码中,第一行

F函数的作用就是为了当它的值未定义时,则调用传入的工厂函数进行初始化

environment和afterEnvironment

1
2
compiler.hooks.environment.call();
compiler.hooks.afterEnvironment.call();

执行了两个钩子函数:环境准备和环境准备完成。

WebpackOptionsApply

1
new WebpackOptionsApply().process(options, compiler);

应用 webpack 配置选项中的各种配置项,包括入口、输出路径、加载器、插件等,准备相应的方法去实现。

initialize

1
compiler.hooks.initialize.call();

调用编译器实例的 initialize 钩子,执行相关的钩子回调函数。这个钩子用于在编译器初始化之后执行一些操作。

至此,我们的compiler实例对象准备好了;接下来再来看下new Compiler()究竟做了什么。

三、编译器构造函数:new Compiler()

hooks初始化

在Compiler的构造函数中,初始化了一个hooks对象,准备收集用户注册的各种插件。

hooks注册

或许你已经发现,在初始化hooks对象时,使用SyncHook或者AsyncSeriesHook等创建了一个对象用于初始化。其实它来自鼎鼎大名的 Tapable 。插件的注册其实也就是一种发布订阅模式。更多事件类型如下:

三、compiler启动编译

一切准备就绪。等到被调用run方法时,才真正开始进入编译阶段。在run方法中,先执行了beforeRun(在 webpack 开始编译前执行。)、run(在 webpack 正式开始编译过程时执行。)两个钩子函数,接着开始调用compile方法执行编译,并传入了一个编译完成函数onCompiled。

Compile.compile()

先执行了beforeCompile(在 webpack 开始编译模块前执行。)、compile(在 webpack 开始编译模块时执行。)两个钩子函数,接着通过const compilation = this.newCompilation(params);创建了一个 compilation 对象。

接着又执行了 make(在编译器完成创建新的编译(Compilation)实例时执行。)、finishMake(在编译器完成创建新的编译实例后触发。)两个钩子。

最后在完成编译时执行了 afterCompile(编译后钩子,在编译过程完成后执行。) 钩子函数,并调用了传入的回调方法onCompiled。

在这个方法中,

onCompiled

在onCompiled方法中,Webpack 会调用 shouldEmit 钩子函数来判断是否需要输出编译后的文件。如果 shouldEmit 返回 true,则表示需要输出文件;如果返回 false,则表示不需要输出文件,webpack 将直接跳过输出阶段,节省了不必要的 IO 操作。

例如当我们启动dev模式时,则不会生成文件;启动build模式则会生成。

如果不需要生成文件,直接执行了 done 钩子并调用 finalCallback 回调函数,结束编译过程。

如果需要生成文件,调用 emitAssets 方法生成资源文件。

finalCallback

这段代码实际上是 webpack 编译过程中的最终回调函数,在编译完成后执行一些收尾工作,并触发 afterDone(在 done 钩子函数执行完毕后执行。) 钩子函数,用于通知编译过程的状态。

生成文件:emitAssets

在这里我们见到了 emit (生成资源到输出目录之前触发)钩子函数,经常写插件的人应该很熟悉。接着开始调用mkdirp方法把文件输出到指定目录。

而在emitFiles方法中,主要做了这几件事情。

  • 首先检查是否有错误,如果有错误则立即回调返回错误。

    1
    if (err) return callback(err);
  • 获取编译好的资源文件列表 assets。

    1
    const assets = compilation.getAssets();
  • 遍历资源文件列表,对每个资源文件进行处理:

    1
    asyncLib.forEachLimit()

所有资源文件处理完成后,清除临时数据,并触发钩子函数 afterEmit。

至此,一次编译主流程的线路已经完成。

四、扩展知识

1、如何创建一个插件?

  • 一个 JavaScript 命名函数或 JavaScript 类。
  • 在插件函数的 prototype 上定义一个 apply 方法。
  • 指定一个绑定到 webpack 自身的事件钩子。
  • 处理 webpack 内部实例的特定数据。
  • 功能完成后调用 webpack 提供的回调。

类写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MyWebpackPlugin {
constructor(options) {
this.options = options;
}

apply(compiler, callback) {
compiler.hooks.done.tap('MyWebpackPlugin', () => {
// ...
callback()
});
}
}

module.exports = MyWebpackPlugin;

函数写法

1
2
3
4
5
6
7
8
9
10
11
12
function MyWebpackPlugin(options) {
this.options = options;
}

MyWebpackPlugin.prototype.apply = function(compiler, callback) {
compiler.hooks.done.tap('MyWebpackPlugin', () => {
// ...
callback()
});
};

module.exports = MyWebpackPlugin;

2、compile和compilation

  • compile:

    • compile 是指整个编译过程的开始,也可以指代编译器的一个实例。在Webpack中,当你调用编译器的 run 方法时,就会触发一次编译过程,这个过程就可以称为一次 compile。compile 是一个动词,表示执行编译过程的行为。
  • compilation:

    • compilation 则是指每一次编译过程中生成的编译对象。每次调用编译器的 run 方法都会生成一个 compilation 对象,它包含了当前编译过程的所有信息,包括输入的模块、输出的资源、编译过程中的各种事件等。compilation 是一个名词,表示编译过程中产生的结果或对象。

综上所述,compile 是一个动词,表示执行编译过程的行为,而 compilation 是一个名词,表示编译过程中生成的结果或对象。在Webpack中,每次执行编译过程都会生成一个新的 compilation 对象,用于跟踪和管理当前编译过程的所有信息。

简单理解,当我们启用 webpack watch时,会启动一个compile和compilation;而当过程中又有文件变更,则会再产生一次compilation,而compile则是最开始创建的那一个。

3、主要钩子函数

  • entryOption:

    • 当Webpack开始解析配置文件中的 entry 配置项时触发。可以用于动态修改 entry 配置。
  • afterPlugins:

    • 在Webpack加载完插件之后触发,但在Webpack配置项中的其他选项开始解析之前。
  • afterResolvers:

    • 在Webpack初始化完解析器(resolvers)之后触发。可以用于对解析器进行进一步的配置或自定义。
  • beforeRun:

    • 在Webpack开始编译前触发,异步地编译前执行。
  • run:

    • 在Webpack开始编译前触发,同步地编译前执行。
  • beforeCompile:

    • 在Webpack即将开始编译模块前触发,这时候已经有了文件的监听。
  • compile:

    • 在Webpack开始编译模块前触发。
  • thisCompilation:

    • 当一个新的 compilation 创建时触发。
  • compilation:

    • 在一个 compilation 对象被创建后触发,这是一个异步的钩子。
  • make:

    • 在一个新的 compilation 开始时触发。
  • afterCompile:

    • 编译完成后触发,可以拿到编译后的 compilation 对象。
  • shouldEmit:

    • 在生成资源到 output 目录前触发,可以控制是否生成资源。
  • emit:

    • 在生成资源到 output 目录时触发。
  • afterEmit:

    • 在资源被输出到 output 目录后触发。
  • done:

    • 整个编译过程完成后触发,包括成功完成编译或发生错误时。

喜欢这篇文章?打赏一下支持一下作者吧!
【源码】webpack5 编译主流程
https://www.cccccl.com/20240114/源码/webpack/webpack5 编译主流程/
作者
Jeffrey
发布于
2024年1月14日
许可协议