与 loader 相比,插件可以更加灵活的扩展 Webpack 的功能。借助于插件机制,我们可以访问到 Webpack 的编译器和编译过程,通过各种钩子,我们可以方便的处理各种定制逻辑。Webpack 本身就是一个插件的集合,这些插件都按照 tapableopen in new window 插件接口规范实现。
与 loader 不同,我们没有脱离于 Webpack 运行插件的方法,在插件的开发过程中,我们始终需要通过 Webpack 来运行插件。
Webpack 插件的基本定义
一个 Webpack 插件必须要定义一个 apply(compiler)
函数,同时在构造函数中接受一个 options
参数。我们可以通过 schema-utilsopen in new window 来对参数做一些校验。
当我们将插件集成到 Webpack 配置中后,Webpack 会调用插件的构造函数初始化插件,之后调用 apply(compiler)
函数。compiler
对象提供了 Webpack 的一些插件 API,提供也提供了非常多的钩子。这些钩子可以在官方文档open in new window 中查到。
设置插件的开发环境
为了更方便的开发插件,通常我们会在插件之上套一个壳子。在这个壳子中获取插件的输出,做一些断言等处理。我们可以借助于 memfsopen in new window 和 compiler.outputFileSystem
来处理插件中的文件系统输出。
先安装 memfs.
npm add memfs --develop
现在实现一个壳子。
plugins/test.js
const webpack = require('webpack');
const { createFsFromVolume, Volume } = require('memfs');
// The compiler helper accepts filenames should be in the output
// so it's possible to assert the output easily.
function compile(config, filenames = []) {
return new Promise((resolve, reject) => {
const compiler = webpack(config);
compiler.outputFileSystem = createFsFromVolume(new Volume());
const memfs = compiler.outputFileSystem;
compiler.run((err, stats) => {
if (err) {
return reject(err);
}
// Now only errors are captured from stats.
// It's possible to capture more to assert.
if (stats.hasErrors()) {
return reject(stats.toString('errors-only'));
}
const ret = {};
filenames.forEach((filename) => {
// 假设 Webpack 输出在 ./dist.
ret[filename] = memfs.readFileSync(`./dist/${filename}`, {
encoding: 'utf-8',
});
});
return resolve(ret);
});
});
}
async function test() {
console.log(
await compile({
entry: './test-entry.js',
})
);
}
test();
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
添加测试文件。
plugins/test-entry.js
console.log('hello from entry');
实现插件的功能
插件的核心要处理两件事情,一是提供一个 apply 函数,二是处理构造参数。
plugins/demo-plugin.js
module.exports = class DemoPlugin {
apply() {
console.log('applying');
}
};
2
3
4
5
现在测试一下这个插件,我们修改一下 plugins/test.js
的代码。
plugins/test.js
...
const DemoPlugin = require("./demo-plugin");
...
async function test() {
console.log(
await compile({
entry: "./test-entry.js",
plugins: [new DemoPlugin()],
})
);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
这时候执行 node ./test.js
,我们可以看到控制台输出了 applying
这条信息。
给插件传递参数
我们通过构造函数给插件传递参数。
plugins/demo-plugin.js
module.exports = class DemoPlugin {
constructor(options) {
this.options = options;
}
apply() {
console.log('apply', this.options);
}
};
2
3
4
5
6
7
8
添加一些参数。
plugins/test.js
async function test() {
console.log(
await compile({
entry: './test-entry.js',
plugins: [new DemoPlugin({ name: 'demo' })],
})
);
}
2
3
4
5
6
7
8
此时再执行 node ./test.js
,可以看到 apply { name: 'demo' }
的输出。
编译器和编译信息
apply
接受一个 compiler
参数。
plugins/demo-plugin.js
module.exports = class DemoPlugin {
constructor(options) {
this.options = options;
}
apply(compiler) {
console.log(compiler);
}
};
2
3
4
5
6
7
8
通过 Webpack 的插件文档open in new window,我们可以看到编译器提供了很多钩子,每一个钩子都与一个编译阶段对应。比如我们需要输出文件,我们可以监听 emit
事件获取文件内容。
修改插件代码,监听 compilation
事件。
plugins/demo-plugin.js
module.exports = class DemoPlugin {
constructor(options) {
this.options = options;
}
apply(compiler) {
compiler.hooks.thisCompilation.tap('DemoPlugin', (compilation) =>
console.log(compilation)
);
}
};
2
3
4
5
6
7
8
9
10
此时执行 node ./test.js
我们可以看到输出了非常多的信息。这个编译信息里包含了 Webpack 整个的依赖图信息。我们可以通过 compilation
访问 entry
,chunk
,module
等。
通过编译信息输出文件
compilation
上的 assets
可以用来输出一些资源文件。我们需要借助于 webpack-sourcesopen in new window 这个文件抽象工具来实现。从 Webpack 5 开始,webpack-sources 已经被默认集成到了 webpack 中。
我们通过 webpack-sources
的 RawSource
来输出文件。
plugins/demo-plugin.js
const { sources, Compilation } = require('webpack');
module.exports = class DemoPlugin {
constructor(options) {
this.options = options;
}
apply(compiler) {
const pluginName = 'DemoPlugin';
const { name } = this.options;
compiler.hooks.thisCompilation.tap(pluginName, (compilation) => {
compilation.hooks.processAssets.tap(
{
name: pluginName,
// See lib/Compilation.js in webpack for more
stage: Compilation.PROCESS_ASSETS_STAGE_ADDITIONAL,
},
() => compilation.emitAsset(name, new sources.RawSource('hello', true))
);
});
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
为了确保文件已经输出,我们调整下 test.js
的代码。
plugins/test.js
async function test() {
console.log(
await compile(
{
entry: './test-entry.js',
plugins: [new DemoPlugin({ name: 'demo' })],
},
['demo']
)
);
}
2
3
4
5
6
7
8
9
10
11
执行 node ./test.js
,可以看到控制台输出 { demo: 'hello' }
。
处理 warning 和 error
我们可以通过 throw new Error("Message")
的方式来终止插件的运行。如果我们希望将告警信息或者错误信息提示给用户,我们可以通过 compilation.warnings
和 compilation.errors
来实现。
compilation.warnings.push('warning');
compilation.errors.push('error');
2
Webpack 还提供了记录日志的 API。
const logger = compiler.getInfrastructureLogger('Demo Plugin');
logger.log('hello from compiler');
2
这个日志 API 与普通的 console.error
、console.warning
等相似,可以通过官方的日志文档open in new window了解详细信息。
插件中还可以有插件
插件还可以提供自己的钩子函数。html-webpack-pluginopen in new window 就是一个很好的例子。
总结
插件可以截断 Webpack 的执行过程,做一些定制处理逻辑。虽然 loader 也可以实现类似的能力,但是插件比 loader 更加的灵活。
插件可以和 loader 结合使用,比如 MiniCssExtractPlugin
。
插件可以访问 Webpack 的 compiler
和 compilation
。这两个对象都提供了丰富的钩子函数以供调用。
插件可以输出新的资源文件,也可以将已有的资源文件处理后重新输出。
插件可以有自己的插件体系,HtmlWebpackPlugin
就是一个很好的例子。
关注微信公众号,获取最新推送~
加微信,深入交流~