写作不易,Star是最大鼓励,感觉写的不错的可以给个Star⭐,请多多指教。本博客的Github地址。
[TOC]
Webpack的Plugin机制让其更加灵活,以适应各种应用场景。在Webpack运行的生命周期中会广播出许多事件,Plugin可以监听这些事件,在合适的时机通过Webpack
提供的API
改变输出结果。
一个最基础的Plugin
的代码是这样的:插件参数的获取是通过插件的构造函数进行获取的。
class BasicPlugin {
// 在构造函数中获取用户为该插件传入的参数
constructor(options) {
this.options = options;
}
// Webpack会调用BasicPlugin实例的apply方法给插件实例传入compiler对象
apply(compiler) {
compiler.plugin('compilation', function(compilation) {})
}
}
// 导出 Plugin
module.exports = BasicPlugin;
2
3
4
5
6
7
8
9
10
11
12
13
14
在使用这个Plugin
时,相关配置代码如下:
const BasicPlugin = require('./BasicPlugin.js');
module.export = {
plugins: [
new BasicPlugin(options)
]
}
2
3
4
5
6
Webpack启动后,在读取配置的过程中会先执行new BasicPlugin(options),初始化一个BasicPlugin获得其实例。在初始化compiler对象后,再调用basicPlugin.apply(compiler)为插件实例传入compiler对象。插件实例在获取到compiler对象后,就可以通过compiler.plugin(事件名称, 回调函数)监听到Webpack广播出来的事件。并且可以通过compiler对象去操作Webpack。
通过以上最简单的Plugin,相信我们大概明白了Plugin的工作原理,但在实际开发中还有很多细节需要注意,下面来详细介绍。
插件的错误处理
- 在参数校验阶段可以直接throw的方式抛出
throw new Error('Error Message');
- 通过Compilation对象的warnings和errors接收
compilation.warnings.push('warnings');
compilation.errors.push('error');
2
Compiler和Compilation
在开发Plugin时最常用的两个对象就是Compiler和Compilation,它们是Plugin和Webpack之间的桥梁。Compiler和Compilation的含义如下:
Compiler对象
Compiler对象包含了Webpack环境的所有配置信息,包含options,loaders,plugins等信息,这个对象在Webpack启动时被实例化,它是全局唯一的,可以简单地将它理解为Webpack实例。
Compilation对象
Compilation对象包含了当前的模块资源、编译生成资源、变化的文件等。当Webpack以开发模式运行时,每当检测到一个文件变化,便有一次新的Compilation被创建。Compilation对象也提供了很多事件回调供插件进行扩展。通过Compilation也能读取到Compiler对象。
Compiler和Compilation
的区别在于:Compiler代表了整个Webpack从启动到关闭的生命周期,而Compilation只是代表了一次新的编译。
Webpack构建流程
1. 初始化参数
从配置文件package.json和Shell 语句中读取与合并参数,得出最终的参数;
每次在命令行输入webpack后,操作系统都会去调用./node_modules/.bin/webpack
这个shell脚本。这个脚本会去调用./node_modules/webpack/bin/webpack.js
并追加输入的参数,如 -p, -w。
2. Webpack初始化
2.1 构建compiler对象
// /node_modules/webpack-cli/bin/cli.js
const webpack = require("webpack");
let compiler;
compiler = webpack(options);
2
3
4
// /node_modules/webpack/lib/webpack.js
const webpack = (options, callback) => {
const webpackOptionsValidationErrors = validateSchema(
webpackOptionsSchema,
options
);
if (webpackOptionsValidationErrors.length) {
throw new WebpackOptionsValidationError(webpackOptionsValidationErrors);
}
let compiler;
if (Array.isArray(options)) {
compiler = new MultiCompiler(
Array.from(options).map(options => webpack(options))
);
} else if (typeof options === "object") {
options = new WebpackOptionsDefaulter().process(options);
// 创建Compiler对象
compiler = new Compiler(options.context);
compiler.options = options;
// 注册NodeEnvironmentPlugin插件
new NodeEnvironmentPlugin({
infrastructureLogging: options.infrastructureLogging
}).apply(compiler);
// 初始化配置文件中的插件
if (options.plugins && Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
if (typeof plugin === "function") {
plugin.call(compiler, compiler);
} else {
// 调用插件的apply方法挂载插件监听
plugin.apply(compiler);
}
}
}
compiler.hooks.environment.call();
compiler.hooks.afterEnvironment.call();
// 挂载options中的基础插件,调用WebpackOptionsApply库初始化基础插件。
compiler.options = new WebpackOptionsApply().process(options, compiler);
} else {
throw new Error("Invalid argument: options");
}
if (callback) {
if (typeof callback !== "function") {
throw new Error("Invalid argument: callback");
}
if (
options.watch === true ||
(Array.isArray(options) && options.some(o => o.watch))
) {
const watchOptions = Array.isArray(options)
? options.map(o => o.watchOptions || {})
: options.watchOptions || {};
return compiler.watch(watchOptions, callback);
}
compiler.run(callback);
}
// 返回compiler对象
return compiler;
};
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
3. 调用run方法开始编译
这里分为两种情况:
- watch:监听文件变化
- run:执行编译
// /node_modules/webpack-cli/bin/cli.js
if (firstOptions.watch || options.watch) {
const watchOptions =
firstOptions.watchOptions || options.watchOptions || firstOptions.watch || options.watch || {};
if (watchOptions.stdin) {
process.stdin.on("end", function(_) {
process.exit(); // eslint-disable-line
});
process.stdin.resume();
}
compiler.watch(watchOptions, compilerCallback);
if (outputOptions.infoVerbosity !== "none") console.error("\nwebpack is watching the files…\n");
} else {
compiler.run((err, stats) => {
if (compiler.close) {
compiler.close(err2 => {
compilerCallback(err || err2, stats);
});
} else {
compilerCallback(err, stats);
}
});
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
4. 触发compile
在run()方法中,执行了this.compile()方法。
// /node_modules/webpack/lib/Compiler.js
this.hooks.beforeRun.callAsync(this, err => {
if (err) return finalCallback(err);
this.hooks.run.callAsync(this, err => {
if (err) return finalCallback(err);
this.readRecords(err => {
if (err) return finalCallback(err);
// 调用compile方法开始编译
this.compile(onCompiled);
});
});
});
2
3
4
5
6
7
8
9
10
11
12
构建了关键的Compilation对象
this.compile()中创建了compilation对象。
// /node_modules/webpack/lib/Compiler.js
compile(callback) {
const params = this.newCompilationParams();
this.hooks.beforeCompile.callAsync(params, err => {
if (err) return callback(err);
this.hooks.compile.call(params);
// 创建Compilation对象
const compilation = this.newCompilation(params);
// 在compile方法中触发make事件
this.hooks.make.callAsync(compilation, err => {
if (err) return callback(err);
compilation.finish(err => {
if (err) return callback(err);
compilation.seal(err => {
if (err) return callback(err);
this.hooks.afterCompile.callAsync(compilation, err => {
if (err) return callback(err);
return callback(null, compilation);
});
});
});
});
});
}
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
Compilation负责整个编译过程,包含了每个构建环节所对应的方法。对象内部保留了对compiler的引用。当 Webpack 以开发模式运行时,每当检测到文件变化,一次新的 Compilation 将被创建。
5. 触发make事件并调用addEntry
addEntry() make分析入口文件创建模块对象
// /node_modules/webpack/lib/DllEntryPlugin.js
compiler.hooks.make.tapAsync("DllEntryPlugin", (compilation, callback) => {
// 调用addEntry方法将所有的入口模块添加到编译构建队列中,开启编译流程
compilation.addEntry(
this.context,
new DllEntryDependency(
this.entries.map((e, idx) => {
const dep = new SingleEntryDependency(e);
dep.loc = {
name: this.name,
index: idx
};
return dep;
}),
this.name
),
this.name,
callback
);
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
webpack的make钩子中,tapAsync注册了一个DllEntryPlugin,就是将入口模块通过调用compilation。这一注册在Compiler.compile()方法中被执行。 addEntry方法将所有的入口模块添加到编译构建队列中,开启编译流程。
6. 构建模块
compilation.addEntry中执行_addModuleChain()这个方法主要做了两件事情。一是根据模块的类型获取对应的模块工厂并创建模块,二是构建模块。
addEntry(context, entry, name, callback) {
this.hooks.addEntry.call(entry, name);
const slot = {
name: name,
// TODO webpack 5 remove `request`
request: null,
module: null
};
if (entry instanceof ModuleDependency) {
slot.request = entry.request;
}
// TODO webpack 5: merge modules instead when multiple entry modules are supported
const idx = this._preparedEntrypoints.findIndex(slot => slot.name === name);
if (idx >= 0) {
// Overwrite existing entrypoint
this._preparedEntrypoints[idx] = slot;
} else {
this._preparedEntrypoints.push(slot);
}
this._addModuleChain(
context,
entry,
module => {
this.entries.push(module);
},
(err, module) => {
if (err) {
this.hooks.failedEntry.call(entry, name, err);
return callback(err);
}
if (module) {
slot.module = module;
} else {
const idx = this._preparedEntrypoints.indexOf(slot);
if (idx >= 0) {
this._preparedEntrypoints.splice(idx, 1);
}
}
this.hooks.succeedEntry.call(entry, name, module);
return callback(null, module);
}
);
}
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
7. 封装构建结果(seal)
webpack 会监听 seal事件调用各插件对构建后的结果进行封装,要逐次对每个 module 和 chunk 进行整理,生成编译后的源码,合并,拆分,生成 hash 。 同时这是我们在开发时进行代码优化和功能添加的关键环节。
8. 输出资源(emit)
把Assets输出到output的path中。
小结
webpack是一个插件合集,由 tapable 控制各插件在 webpack 事件流上运行。主要依赖的是compilation的编译模块和封装。
webpack的入口文件其实就实例了Compiler并调用了run方法开启了编译,webpack的主要编译都按照下面的钩子调用顺序执行。
比较关键的 webpack 事件节点:
- after-plugins 设置完一组初始化插件之后
- after-resolvers 设置完 resolvers 之后
- compile 开始编译
- make 从入口点分析模块及其依赖的模块,创建这些模块对象
- build-module 构建模块
- after-compile 完成构建
- seal 封装构建结果
- emit 把各个chunk输出到结果文件
- after-emit 完成输出
一个 Compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。
事件流
Webpack就像一条生产线,要经过一系列处理流程后才能将源文件转换成输出结果。这条生产线上的每个处理流程的职责都是单一的,多个流程之间存在依赖关系,只有在完成当前处理后才能交给下一个流程去处理。插件就像插入生产线中的某个功能,在特定的时机对生产线上的资源进行处理。
Webpack通过Tapable来组织这条复杂的生产线。Webpack在运行的过程中会广播事件,插件只需要监听它所关心的事件,就能加入这条生产线中,去改变生产线的运作。Webpack的事件流机制保证了插件的有序性,使得整个系统扩展性很好。
Webpack
的事件流机制应用了发布订阅模式,和Node.js
中的EventEmitter
非常相似。Compiler和Compilation
都继承自Tapable
,可以直接在Compiler和Compilation
对象上广播和监听事件,方法如下:
/**
* 广播事件
* event-name为事件名称,注意不要和现有的事件重名
* params 为附带的参数
*/
compiler.apply('event-name', params);
/**
* 监听名称为event-name的事件,当event-name事件发生时,函数就会被执行。
* 同时函数中的 params 参数为广播事件时附带的参数。
*/
compiler.plugin('event-name', function(params) {});
2
3
4
5
6
7
8
9
10
11
12
同理,compilation.apply和compilation.plugin
使用方法和上面一致。
在开发插件时,我们可能会不知道该如何下手,因为不知道该监听哪个事件才能完成任务。
在开发插件时,还需要注意以下两点:只要能拿到Compiler 或 Compilation
对象,就能广播出新的事件,所以在新开发的插件中也能广播事件,为其他插件监听使用。传给每个插件的Compiler 和 Compilation
对象都是同一个引用。也就是说:若在一个插件中修改了Compiler 或 Compilation
对象上的属性,会影响到后面的插件。有些事件是异步的,这些异步的事件会附带两个参数,第2个参数为回调函数,在插件处理完任务时需要调用回调函数通知Webpack,才会进入下一处理流程。例如:
compiler.plugin('emit', function(compilation, callback) {
// 支持处理逻辑
// 处理完毕后执行 callback 以通知 Webpack
// 如果不执行 callback,运行流程将会一直卡在这里而不往后执行
callback();
});
2
3
4
5
6
常用API
插件可以用来修改输出文件和增加输出文件,甚至可以提升Webpack
性能,等等。总之,插件可以通过调用Webpack
提供的API
完成很多事情。由于Webpack
提供的API
非常多,有很多API
很少用得上,下面来介绍一些常用的API。
读取输出资源、代码块、模块及其依赖
某些插件可能需要读取Webpack
的处理结果,例如输出资源、代码块、模块及其依赖,以便做下一步处理。
emit事件发生时,代表源文件的转换和组装已经完成,在这里可以读取到最终将输出的资源、代码块、模块及其依赖,并且可以修改输出资源的内容。插件的代码如下:
class Plugin {
apply(compiler) {
compiler.plugin('emit', function (compilation, callback) {
// compilation.chunks 存放所有代码块,是一个数组
compilation.chunks.forEach(function (chunk) {
// chunk 代表一个代码块
// 代码块由多个模块组成,通过 chunk.forEachModule 能读取代码块的每个模块
chunk.forEachModule(function (module) {
// module 代表一个模块
// module.fileDependencies 存放当前模块的所有依赖的文件路径,是一个数组
module.fileDependencies.forEach(function (filepath) {
});
});
// Webpack 会根据Chunk生成输出的文件资源,每个Chunk都对应一个及以上的输出文件
// 例如在Chunk中包含了CSS模块并且使用了ExtractTextPlugin 时,
// 该Chunk就会生成.js和.css两个文件
chunk.files.forEach(function (filename) {
// compilation.assets 存放当前即将输出的资源
// 调用一个输出资源的 source() 方法能获取到输出资源的内容
let source = compilation.assets[filename].source();
});
});
// 这是一个异步事件,要记得调用 callback 通知 Webpack 本次事件监听处理结束
// 如果忘记了调用 callback,Webpack 将一直卡在这里而不会往后执行
callback();
})
}
}
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
监听文件变化
Webpack
会从配置的入口模块出发,依次找出所有依赖模块,当入口模块或者其依赖的模块发生变化时,就会触发一次新的Compilation
。
在开发插件时经常需要知道是哪个文件发生变化导致了新的Compilation
,为此可以使用如下代码:
// 当依赖的文件发生变化时会触发 watch-run 事件
compiler.plugin('watch-run', (watching, callback) => {
// 获取发生变化的文件列表
const changedFiles = watching.compiler.watchFileSystem.watcher.mtimes;
// changedFiles 格式为键值对,键为发生变化的文件路径。
if (changedFiles[filePath] !== undefined) {
// filePath 对应的文件发生了变化
}
callback();
});
2
3
4
5
6
7
8
9
10
在默认情况下,Webpack
只会监视入口和其依赖的模块是否发生了变化,在某些情况下项目可能需要引入新的文件,例如引入一个HTML
文件。由于JavaScript
文件不会导入HTML
文件,所以Webpack
不会监听HTML
文件的变化,编辑 HTML
文件时就不会重新触发新的Compilation
。为了监听HTML
文件的变化,我们需要将HTML
文件加入到依赖列表中,为此可以使用如下代码:
compiler.plugin('after-compile', (compilation, callback) => {
// 把 HTML 文件添加到文件依赖列表,好让 Webpack 去监听 HTML 模块文件,在 HTML 模版文件发生变化时重新启动一次编译
compilation.fileDependencies.push(filePath);
callback();
});
2
3
4
5
修改输出资源
有些场景下插件需要修改、增加、删除输出的资源,要做到这一点,则需要监听 emit
事件,因为发生emit
事件时所有模块的转换和代码块对应的文件已经生成好,需要输出的资源即将输出,因此emit
事件是修改Webpack
输出资源的最后时机。
所有需要输出的资源都会存放在compilation.assets
中,compilation.assets
是一个键值对,键为需要输出的文件名称,值为文件对应的内容。
设置compilation.assets的代码如下:
compiler.plugin('emit', (compilation, callback) => {
// 设置名称为 fileName 的输出资源
compilation.assets[fileName] = {
// 返回文件内容
source: () => {
// fileContent 既可以是代表文本文件的字符串,也可以是代表二进制文件的 Buffer
return fileContent;
},
// 返回文件大小
size: () => {
return Buffer.byteLength(fileContent, 'utf8');
}
};
callback();
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
读取compilation.assets
的代码如下:
compiler.plugin('emit', (compilation, callback) => {
// 读取名称为 fileName 的输出资源
const asset = compilation.assets[fileName];
// 获取输出资源的内容
asset.source();
// 获取输出资源的文件大小
asset.size();
callback();
});
2
3
4
5
6
7
8
9
判断Webpack使用了哪些插件
在开发一个插件时,我们可能需要根据当前配置是否使用了其他插件来做下一步决定,因此需要读取Webpack
当前的插件配置情况。以判断当前是否使用了 ExtractTextPlugin
为例,可以使用如下代码:
// 判断当前配置是否使用了 ExtractTextPlugin,
// compiler 参数为 Webpack 在 apply(compiler) 中传入的参数
function hasExtractTextPlugin(compiler) {
// 当前配置使用的所有插件列表
const plugins = compiler.options.plugins;
// 去 plugins 中寻找有没有 ExtractTextPlugin 的实例
return plugins.find(plugin => plugin.__proto__.constructor === ExtractTextPlugin) != null;
}
2
3
4
5
6
7
8
实战
如何写一个插件
参考官方文档 writing-a-plugin 一个webpack插件的编写由以下几个步骤组成:
- 一个JavaScript类函数(或者class)。
- 在函数原型 (prototype)中定义一个注入compiler对象的apply方法。
- apply函数中通过compiler插入指定的事件钩子,在钩子回调中拿到compilation对象。
- 使用compilation操纵修改webapack内部实例数据。
- 异步插件,数据处理完后使用callback回调。
// A JavaScript class.
class MyExampleWebpackPlugin {
// Define `apply` as its prototype method which is supplied with compiler as its argument
apply(compiler) {
// 注册事件钩子
compiler.hooks.emit.tapAsync('MyExampleWebpackPlugin', (compilation, callback) => {
console.log('This is an example plugin!');
console.log('Here’s the `compilation` object which represents a single build of assets:', compilation);
// Manipulate the build using the plugin API provided by webpack
compilation.addModule(/* ... */);
// 异步钩子需要执行callback
callback();
}
);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
在项目根目录新建plugins目录,用来存放自定义插件;然后新建my-webpack-plugin
,即我们的第一个自定义插件。具体内容如下:
index.js:
class HelloWorldPlugin {
constructor(options) {
// options中为用户自定义配置参数
this.options = options;
console.log(this.options);
}
apply(compiler) {
console.log('This is my first plugin.')
}
}
module.exports = HelloWorldPlugin;
2
3
4
5
6
7
8
9
10
11
12
package.json:
{
"name": "my-webpack-plugin",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "liujie",
"license": "ISC"
}
2
3
4
5
6
7
8
9
10
11
12
在webpack.config.js
中配置:
const MyWebpackPlugin = require('./plugins/my-webpack-plugin');
module.exports = {
plugins: [
new MyWebpackPlugin('我是自定义的插件')
]
};
2
3
4
5
6
7
执行npm run build。
获取插件中传递的参数
首先需要知道插件如何使用:一般都是通过实例化来使用。
new ZipPlugin({
name: 'test'
})
2
3
那么,就可以通过插件的构造函数进行插件参数的获取:
CopyrightWebpackPlugin
class CopyrightWebpackPlugin {
constructor(options) {
console.log('插件被使用了');
// console.log(options); // { name: 'haha' }
}
apply(compiler) {
// 同步钩子--不需要传cb参数
compiler.hooks.compile.tap('CopyrightWebpackPlugin', compilation => {
console.log('同步钩子');
});
// compiler.hooks钩子函数,就是在某个时刻会执行的函数
// compiler中存放的是配置的内容以及包括打包的所有内容
// 而compilation中存放是某一次打包的内容
// 第一个参数是插件名称
// emit钩子函数会在将打包后的文件放入到dist目录后触发
compiler.hooks.emit.tapAsync('CopyrightWebpackPlugin', (compilation, cb) => {
// debugger
compilation.assets['copyright.txt'] = {
source: function() { // 指定文件内容
return 'copyright by liujie';
},
size: function() { // 指定文件大小
return 21;
}
};
cb(); // 异步钩子都需要执行cb
});
}
}
module.exports = CopyrightWebpackPlugin;
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
FileListPlugin
class FileListPlugin {
constructor(options) {
console.log(options);
this.options = options;
}
apply(compiler) {
// emit是异步hook,使用tapAsync触及它,还可以使用tapPromise(异步)/tap(同步)
compiler.hooks.emit.tapAsync('FileListPlugin', (compilation, callback) => {
// 在生成文件中,创建一个头部字符串
let fileList = 'In this build:\n\n';
// 遍历所有编译过的资源文件,
// 对于每个文件名称,都添加一行内容。
for (let filename in compilation.assets) {
console.log(compilation.assets);
fileList += `- ${filename}\n`;
}
// 将这个列表作为一个新的文件资源,插入到webpack构建中:
compilation.assets['fileList.md'] = {
source: function() {
return fileList;
},
size: function() {
return fileList.length;
}
};
callback();
});
}
}
module.exports = FileListPlugin;
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
在上述例子中,compilation.assets对象如下:
{ 'bundle.js':
CachedSource {
_source: ConcatSource { children: [Array] },
_cachedSource: undefined,
_cachedSize: undefined,
_cachedMaps: {},
node: [Function],
listMap: [Function] },
'copyright.txt': { source: [Function: source], size: [Function: size] } }
2
3
4
5
6
7
8
9
zip-plugin
class ZipPlugin {
// 需要接收插件传递的参数,这里需要写options
constructor(options) {
this.options = options;
}
apply(compiler) {
console.log('插件参数为:', this.options);
}
}
2
3
4
5
6
7
8
9
原理总结
Webpack是一个庞大的Node.js应用,如果你阅读过它的源码,你会发现实现一个完整的Webpack需要编写非常多的代码。 但你无需了解所有的细节,只需了解其整体架构和部分细节即可。
对Webpack的使用者来说,它是一个简单强大的工具;对Webpack的开发者来说,它是一个扩展性的高系统。
Webpack之所以能成功,在于它把复杂的实现隐藏了起来,给用户暴露出的只是一个简单的工具,让用户能快速达成目的。 同时整体架构设计合理,扩展性高,开发扩展难度不高,通过社区补足了大量缺失的功能,让Webpack几乎能胜任任何场景。