webpack-plugin原理

webpack插件是什么

官方文档: webpack 插件是一个具有 apply 属性的 JavaScript 对象。apply 属性会被 webpack compiler 调用,并且 compiler 对象可在整个编译生命周期访问。插件目的在于解决 loader 无法实现的其他事。

个人理解:插件通常是在webpack在打包的某个时间节点做一些事情,类似vue中常说的钩子函数。

官方示例:

1
2
3
4
5
6
7
8
9
const pluginName = 'ConsoleLogOnBuildWebpackPlugin';

class ConsoleLogOnBuildWebpackPlugin {
apply(compiler) {
compiler.hooks.run.tap(pluginName, compilation => { //{4}
console.log("webpack 构建过程开始!");
});
}
}

自定义插件使用时只需要在配置文件中引入,并做相关配置即可。第4行的complier后面的一大坨代码为什么这么写呢?源码分析如下。

webpack源码

写本文时参考的源码版本
1
2
3
4
"devDependencies": {
"webpack": "^5.3.1",
"webpack-cli": "^4.1.0"
}

先说一下webpack入口文件的大致流程:

  • 首先检查是否安装webpack-cli。
  • 如果没安装,则提示用户是否安装,如果用户输入y开头的指令,则安装webpack-cli,安装成功后进入webpack-cli入口文件,开始干活,否则退出。
  • 如果已经安装,进入webpack-cli入口文件,开始干活。

具体代码如下,以下直接从源码拷贝过来后去除了一些没用的注释,加了一些中文注释,不感兴趣可直接跳过,这不是重头戏。

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110

const runCommand = (command, args) => { //封装执行命令的函数
const cp = require("child_process");
return new Promise((resolve, reject) => {
const executedCommand = cp.spawn(command, args, {
stdio: "inherit",
shell: true
});

executedCommand.on("error", error => {
reject(error);
});

executedCommand.on("exit", code => {
if (code === 0) {
resolve();
} else {
reject();
}
});
});
};

const isInstalled = packageName => { //判断是否下载指定的包,使用require.resolve来查询某个模块的完整路径
try {
require.resolve(packageName);
return true;
} catch (err) {
return false;
}
};

const runCli = cli => { //找webpackcli的入口文件,开始干活
const path = require("path");
const pkgPath = require.resolve(`${cli.package}/package.json`);
const pkg = require(pkgPath);
require(path.resolve(path.dirname(pkgPath), pkg.bin[cli.binName]));
};
const cli = {
name: "webpack-cli",
package: "webpack-cli",
binName: "webpack-cli",
installed: isInstalled("webpack-cli"),
url: "https://github.com/webpack/webpack-cli"
};

if (!cli.installed) { //如果没有下载webpack-cli,给一些警告或者建议容错之类的
const path = require("path");
const fs = require("graceful-fs");
const readLine = require("readline");

const notify =
"CLI for webpack must be installed.\n" + ` ${cli.name} (${cli.url})\n`;

console.error(notify);

const isYarn = fs.existsSync(path.resolve(process.cwd(), "yarn.lock"));

const packageManager = isYarn ? "yarn" : "npm";
const installOptions = [isYarn ? "add" : "install", "-D"];

console.error(
`We will use "${packageManager}" to install the CLI via "${packageManager} ${installOptions.join(
" "
)}".`
);

const question = `Do you want to install 'webpack-cli' (yes/no): `; //询问用户是否下载webpack cli

const questionInterface = readLine.createInterface({
input: process.stdin,
output: process.stderr
});

process.exitCode = 1;
questionInterface.question(question, answer => {
questionInterface.close();

const normalizedAnswer = answer.toLowerCase().startsWith("y"); // 用户是否输入了y开头的,如果是,直接开始下载

if (!normalizedAnswer) {
console.error(
"You need to install 'webpack-cli' to use webpack via CLI.\n" +
"You can also install the CLI manually."
);

return;
}
process.exitCode = 0;

console.log(
`Installing '${
cli.package
}' (running '${packageManager} ${installOptions.join(" ")} ${
cli.package
}')...`
);

runCommand(packageManager, installOptions.concat(cli.package)) //执行下载命令
.then(() => {
runCli(cli);
})
.catch(error => {
console.error(error);
process.exitCode = 1;
});
});
} else {
runCli(cli); //如果下载了webpackcli,直接进入其目录开始干活
}

webpack-cli入口文件,bin目录下的cli.js

代码如下:
主要拿到执行webpack命令时的参数,然后执行runCli,并检查是否安装了webpack做了容错

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
29
30
31
32
33
34
35
36
37
38
39
40
#!/usr/bin/env node

'use strict';

require('v8-compile-cache');

const importLocal = require('import-local');
const runCLI = require('../lib/bootstrap');
const { yellow } = require('colorette');
const { error, success } = require('../lib/utils/logger');
const { packageExists } = require('../lib/utils/package-exists');
const { promptInstallation } = require('../lib/utils/prompt-installation');


if (importLocal(__filename)) {// 允许全局安装的软件包使用自身的本地安装版本webpack-cli
return;
}

process.title = 'webpack';

const [, , ...rawArgs] = process.argv; //拿到package.json中scripts脚本参数

if (packageExists('webpack')) {//检查是否按转webpack
runCLI(rawArgs);//执行
} else {//没有安装webpack的容错
promptInstallation('webpack -W', () => {
error(`It looks like ${yellow('webpack')} is not installed.`);
})
.then(() => {
success(`${yellow('webpack')} was installed sucessfully.`);

runCLI(rawArgs);
})
.catch(() => {
error(`Action Interrupted, Please try once again or install ${yellow('webpack')} manually.`);

process.exit(2);
});
}

以上代码可以看到runCli函数是从*../lib/bootstrap*引入。这里代码较长,我们简化一下,保留主要内容:

1
2
3
4
//bootstrap.js
const WebpackCLI = require('./webpack-cli');
const cli = new WebpackCLI();
cli.run(...args);

所以主要文件是webpack-cli.js,webpack-cli.js中run方法通过createCompiler创建了一个编译器,代码简化一下主要内容大致代码如下:

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
29
const webpack = packageExists('webpack') ? require('webpack') : undefined;
const GroupHelper = require('./utils/GroupHelper');
class WebpackCLI extends GroupHelper {

//...其他业务代码

createCompiler(options) {
let compiler;
try {
compiler = webpack(options);
} catch (error) {
const ValidationError = webpack.ValidationError ? webpack.ValidationError : webpack.WebpackOptionsValidationError;
if (error instanceof ValidationError) {
logger.error(error.message);
} else {
logger.error(error);
}

process.exit(2);
}
return compiler;
}

async run(args, cliOptions) {
//...其他业务代码
const compiler = this.createCompiler(this.compilerConfiguration);
//...其他业务代码
}
}

接下来又要进入webpack的主文件,lib目下的index.js

index.js中就是各种文件导出合并,这里就随便粘贴了一点,可以看到引入了webpack.js然后做了一些处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const fn = lazyFunction(() => require("./webpack"));
module.exports = mergeExports(fn, {
get webpack() {
return require("./webpack");
},
get validate() {
const validateSchema = require("./validateSchema");
const webpackOptionsSchema = require("../schemas/WebpackOptions.json");
return options => validateSchema(webpackOptionsSchema, options);
},
get validateSchema() {
const validateSchema = require("./validateSchema");
return validateSchema;
},
//...其他业务代码
})

webpack.js内容:

这里也是简化之后的代码

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
const Compiler = require("./Compiler");
const createCompiler = rawOptions => {
const options = getNormalizedWebpackOptions(rawOptions);
applyWebpackOptionsBaseDefaults(options);
const compiler = new Compiler(options.context);
compiler.options = 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;
};

可以看到,这里引入了Compiler.js,在创建编译器的拿到配置项的plugins参数,遍历所有的插件,通过call或者apply将compiler传给plugin函数。 注意:⚠️这里并不是改变this指向的call或者apply,回想文章开头举的官方示例,可以看见,在插件内部定义了这个apply函数,并接收一个compiler参数!!!

Compiler.js

前面说到,在插件内部定义了apply函数并将compiler传递进去,回想文章开头的问题,插件为什么要写compiler.hooks.run.tap,肯定也是在构造函数内部定义的。这里文件代码太长,同样简化一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Compiler.js
const {
SyncHook,
SyncBailHook,
AsyncParallelHook,
AsyncSeriesHook
} = require("tapable");
//...其他业务代码
class Compiler {

constructor(context) {
this.hooks = Object.freeze({
//...其他业务代码
run: new AsyncSeriesHook(["compiler"]),
//...其他业务代码
})
}
}

可以看到,run返回了一个*new AsyncSeriesHook([“compiler”])*,这个构造函数是取自于tapable。webpack官网在介绍plugins的时候说了这么一句话,Plugins are the backbone of webpack。这里的backbone就是tapable。

tapable的作用是各种钩子函数,当new一个钩子函数时可以在这个钩子实例的tap上加上各种任务,个人理解类似把所有任务先放进一个队列,在合适的条件下调用实例的call方法,则会执行队列里面的任务,具体怎么执行要看这个钩子函数是什么类型。这里随便选一种类型的函数写了一个demo:

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
29
const {
SyncHook,
SyncBailHook,
SyncWaterfallHook,
SyncLoopHook,
AsyncParallelHook,
AsyncParallelBailHook,
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesWaterfallHook
} = require("tapable"); //所有的钩子

let queue = new SyncHook(['name']);
queue.tap('1',function (name){
name = name + 'test'
console.log('1',name);
})
queue.tap('1',function (name){
console.log('2',name);
})
queue.tap('3',function (name){
console.log('3',name);
})
queue.call('webpack')

//输出
// 1 webpack
// 2 webpack
// 3 webpack

这样一来前面的插件为什么那么写也就清楚了,就是把所有的插件通过钩子的tap方法放进一个任务队列,然后根据具体业务时机执行这个任务函数。

webpack中的compiler类作用:在webpack工作流程中,首先要通过compiler类来收集配置文件信息,然后指挥整体的编译流程,相当于通过compiler构建配置信息。

webpack工作流程。

Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程 :

  • 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数。
  • 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译。
  • 确定入口:根据配置中的 entry 找出所有的入口文件。
  • 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理。
  • 完成模块编译:在经过第 4 步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系。
  • 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会。
  • 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。

未完待续~,有时间再写

查看评论