# webpack plugin 开发入门
下面要介绍的就是除了 loader
之外,在 webpack
中另一个常用的组成部分 plugin
。
要了解 plugin
,那得从 plugin
与 loader
的区别开始讲起。
- 从 webpack loader 开发入门 中我们了解到了
loader
主要用来将文件转换成webpack
所能识别的模块。 - 而
plugin
则是对能够对webpack
进行功能扩展,比如代码压缩、注入环境变量等等。通过plugin
我们能够在webpack
构建过程中引入自定义的功能、行为。
# plugin 基本组成
plugin
就是一个类,在JavaScript
中可以是一个命名了的function
或class
。- 类的原型对象
prototype
中应该有apply
这个方法。webpack
启动构建时会调用该方法,并且将当前的compiler
对象回传到该方法中。 - 在合适的 事件钩子 (opens new window) 中绑定、插入自定义函数。
- 在自定义函数中操作
webpack
的内部实例、数据。 - 自定义操作完成之后,调用
webpack
提供的回调函数,让构建流程继续执行或抛出错误。
所以一个插件可以是下面这样。
// A JavaScript class.
class MyExampleWebpackPlugin {
// Define `apply` as its prototype method which is supplied with compiler as its argument
apply(compiler) {
// Specify the event hook to attach to
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()
}
)
}
}
# Compiler & Compilation
在上面的例子中,我们看到了 compiler
和 compilation
这两个变量,它们是在开发 plugin
中最重要的两个对象,所以在开发 plugin
之前,必须先了解它们。
# Compiler
compiler (opens new window) 可以理解为每当启动一次 webpack
构建都会创建的对应的 compiler
(编译器)实例对象。所以在这个对象中能够拿到 webpack
的相关配置,比如 entry
、loader
、plugins
等等。
# Compilation
compilation (opens new window) 可以理解为 compiler
每次构建行为都会生成的一个对象,该对象含有本次构建产出的资源及其相关信息,代表本次构建。
在 compilation
阶段中,modules
(模块)会经过 loaded
, sealed
, optimized
, chunked
, hashed
,restored
等。
# 事件挂载与 tapable
compiler
和 compilation
等与 webpack
相关的对象都是一个 Tapable (opens new window) 的实例。
Tapable
类暴露出了 tap
、tapAsync
、tapPromise
等方法,让 plugin
能够通过这些方法在 webpack
的不同钩子函数中插入自定义的构建步骤、流程。
比如 compiler.hooks.emit.tapAsync
,表示在 compiler
的 emit hook (opens new window) 中插入一个异步执行函数。
从 compiler-hooks (opens new window) 和 compilation-hooks (opens new window) 中可以查看更多 hooks
详情。
# 编写插件
在简单了解了必要的 plugin
知识之后,下面尝试编写一个简单的 plugin
。
# 需求
在项目构建成功后,需要将本次构建生成的相关资源信息(文件名、大小等等)格式化地输出在指定的文件之中,方便对构建结果进行追溯、统计。
# 搭建插件骨架
plugin
都需要一个标识,所以这里定义了一个 pluginName
,以及必备的 apply
方法。
我们的需求是在构建结束之后生成资源信息文件,在翻阅 compiler-hooks (opens new window) 发现了 done (opens new window) 这个钩子,done
会在此次构建成功后执行,符合我们的需求,加之插件没有异步操作,所以我们通过同步 tap
方法将插件逻辑插入到 done
钩子中。
const pluginName = 'AssetsReportPlugin'
class AssetsReportPlugin {
apply(compiler) {
compiler.hooks.done.tap(pluginName, (compilation) => {
console.log(pluginName, 'running.')
})
}
}
module.exports = { AssetsReportPlugin }
在构建引入插件 AssetsReportPlugin
,执行构建发现控制台输出了 AssetsReportPlugin running.
说明 plugin
调用成功。
...,
const { AssetsReportPlugin } = require('../src/index')
module.exports = {
...,
plugins: [new AssetsReportPlugin()]
}
# 完善插件逻辑
获取本次构建输出相关信息。
关于这点
webpack
官方没有太详细的介绍,因为参数、属性实在太多了。webpack
也在官网中指出写plugin
就要做好阅读源码的准备。这里推荐通过打断点,或者查阅一些功能相似的成熟的插件源码比如 html-webpack-plugin (opens new window) 来了解相关参数。通过断点查看对象可以发现通过stats.compilation.assets
对象可以获取本次构建的相关信息。参数设计。
这里也尽量保持与
webpack output
的参数一致,在实例化插件的时候可以传入path
、filename
参数,来决定文件输出目录以及文件名。如果没有传入就会使用默认参数。..., const resolve = (...args) => path.resolve(process.cwd(), ...args) const { AssetsReportPlugin } = require('../src/index') module.exports = { ..., plugins: [ ..., new AssetsReportPlugin({ path: resolve('example/dist/report'), filename: 'files.md' }) ] }
完善后的代码如下。
const fs = require('fs-extra')
const path = require('path')
// 插件名
const pluginName = 'AssetsReportPlugin'
// 默认参数
const defaultOptions = {
filename: 'assets.md'
}
// 文本头部
const tableHeader = `
# assets table
| file | size |
| ------ | ------ |
`
class AssetsReportPlugin {
constructor(options) {
// 合并参数
this.options = { ...defaultOptions, ...options }
}
apply(compiler) {
compiler.hooks.done.tap(pluginName, (stats) => {
const { assets, outputOptions } = stats.compilation
// 拼接文本
const content = Object.keys(assets).reduce((acc, key) => {
acc += `| ${key} | ${assets[key]._size} |\n`
return acc
}, tableHeader)
// 获取输出路径,如没传入,则使用 webpack 的 output.path
const outputPath = this.options.path || outputOptions.path
// 输出文件
fs.outputFile(path.resolve(outputPath, this.options.filename), content)
})
}
}
module.exports = { AssetsReportPlugin }
执行构建,可以看见在目标目录成功输出了包含构建信息的 md
文件。
# 参数校验
为了能够让使用者在参数传入错误时给出准确的错误提示,这里使用官方推荐的 schema-utils (opens new window) 来对传入的 options
进行检验。
安装依赖。
npm install schema-utils --save
添加检验规则描述文件
/src/schema.json
。{ "type": "object", "properties": { "path": { "type": "string", "absolutePath": true }, "filename": { "type": "string" } }, "additionalProperties": false }
schema-utils
的用法具体可查看 schema-utils 文档 (opens new window) 说明,官方README
没有并没有太详细的说明,那么也可以从其 单元测试用例 schema.json (opens new window) 中查看、参考更多不同的用法。调用校验方法。
修改插件。
..., const { validate } = require('schema-utils') const schema = require('./schema.json') // 插件名 const pluginName = 'AssetsReportPlugin' ..., class AssetsReportPlugin { constructor(options) { // 校验参数 validate(schema, options, { name: pluginName }) // 合并参数 this.options = { ...defaultOptions, ...options } } ..., } module.exports = { AssetsReportPlugin }
配置完成之后,尝试传入不合法的 options
,再次执行构建,可以看见控制台报错,并且输出了可读性较高的错误提示。
本文相关实践代码可从 https://github.com/xuwenchao66/webpack-plugin-practice (opens new window) 中查阅。