深入理解 plugins 插件
loader 被用于转换某些类型的模块,而插件则可以用来处理各种各样的任务,例如打包优化和压缩、重新定义环境中的变量等。
插件是 webpack 生态系统的重要组成部分,为社区用户提供了一种强大方式来直接触及 webpack 的编译过程(compilation process)。
插件能够钩入(hook)到每个编译(compilation)中触发的所有关键事件。在编译的每个阶段中,插件都拥有对 compiler 对象的完全访问能力,并且在合适的时机,还可以访问当前的 compilation 对象。
一、Tapable
tapable 这个小型库是 webpack 的一个核心工具,它为 webpack 插件接口提供了核心能力。
webpack 中许多对象扩展自 Tapable 类(例如 compiler 对象)。这个类暴露 tap
、tapAsync
和 tapPromise
等方法,可以使用这些方法,注入自定义的构建步骤,这些步骤将在整个编译过程中不同时机触发。
二、常见的插件
- HotModuleReplacementPlugin:模块热更新插件;
- HtmlWebpackPlugin:生成 HTML 文件,用于服务器访问;
- MiniCssExtractPlugin:将 CSS 从 JS 中提取出来;
- OptimizeCSSAssetsWebpackPlugin:压缩优化 CSS;
- UglifyJSWebpackPlugin:压缩优化 JS;
- CleanPluginForWebpack:每次打包前先删除 dist 文件夹。
- FriendlyErrorsWebpackPlugin:识别 webpack 中的类别错误,提供友好的命令行提示。
三、插件的使用
这里以 HtmlWebpackPlugin 为例,HtmlWebpackPlugin 用于生成一个 HTML 文件,把所有生产的 JS 文件都引入到该文件中,最终生成到 output 目录。
- 安装:
npm install --save-dev html-webpack-plugin
- 配置:
var HtmlWebpackPlugin = require('html-webpack-plugin');
var path = require('path');
var webpackConfig = {
entry: 'index.js',
output: {
path: path.resolve(__dirname, './dist'),
filename: 'index_bundle.js'
},
plugins: [new HtmlWebpackPlugin()]
};
输出结果:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>webpack App</title>
</head>
<body>
<script src="index_bundle.js"></script>
</body>
</html>
如果需要多个页面的配置,需要实例化多个 HtmlWebpackPlugin 对象,也可以对其进行参数配置:
const htmlWebPackConfig = {
title: 'Hello Webpack', // 配置模板 title
template: '', // 模板来源 html 文件
filename: 'index.html', // 生成的模板文件名
favicon: '', // 指定页面的图标
hash: true, // 是否生成 hash 添加在引入文件地址的末尾 默认为 true
inject: '', // 引入模板的注入位置 取值有(true/false/body/head)
minify: { // 对生成的 html 文件进行压缩,默认是 false
collapseWhitespace: true, // 是否去除空格
removeAttributeQuotes: true, // 去掉属性引用
caseSensitive: false, // 是否大小写敏感
removeComments: true // 去掉注释
},
cache: true, // 表示内容变化的时候生成一个新的文件,默认 true
showErrors: true, // 是否将错误信息写在页面,默认 true
chunks: ['index'] // 引入模块,即 entry 中设置的多个 js,这里是执行 js, 否则引入全部
}
四、loader 与 plugin 的区别
loader 是一个转换器,用于将 A 文件编译成 B 文件,对文件进行操作,在打包文件之前运行。例如将
A.scss
转换为B.css
;plugin 是一个扩展器,用于增强 webpack 的功能,它不直接操作文件,而是基于事件机制工作,监听 webpack 打包过程中的某些节点,执行相应的任务,在整个编译周期都起作用。例如打包优化、资源管理、环境变量注入等,其目的是解决 loader 无法实现的其他事。
五、如何实现一个插件
由于 webpack 基于发布订阅模式,在运行的生命周期中会广播出许多事件,插件通过监听这些事件,就可以在特定的阶段执行自己的插件任务。
1、插件的组成
webpack 插件由以下组成:
- 一个 JavaScript 命名函数。
- 在插件函数的 prototype 上定义一个
apply
方法。 - 指定一个绑定到 webpack 自身的事件钩子。
- 处理 webpack 内部实例的特定数据。
- 功能完成后调用 webpack 提供的回调。
2、创建插件
2-1、老版的写法 webpack <v5
插件是由「具有 apply
方法的 prototype 对象」所实例化出来的。这个 apply
方法在安装插件时,会被 webpack compiler 调用一次。apply 方法可以接收一个 webpack compiler 对象的引用,从而可以在回调函数中访问到 compiler 对象。一个简单的插件结构如下:
// 一个 JavaScript 命名函数
function HelloWorldPlugin() {}
// 在插件函数的 prototype 上定义一个 apply 方法
HelloWorldPlugin.prototype.apply = function (compiler) {
// 指定一个挂载到 webpack 自身的事件钩子
compiler.plugin(
'done',
function (compilation /* 当前打包构建流程的上下文 */, callback) {
console.log('Hello World!')
// 功能完成后可以调用 webpack 提供的回调
callback()
}
)
}
module.exports = HelloWorldPlugin
上面代码中,
compiler 扩展自 Tapable 类,包含了 webpack 环境所有的配置信息,在启动 webpack 时被实例化,可理解为 webpack 实例,用来注册和调用插件。
compilation 也扩展自 Tapable 类,作为 plugin 内置事件回调函数的参数,包含了当前的模块资源、编译生成资源、变化的文件等,当 webpack 以开发模式运行时,每检测到一个文件变化,一次新的 Compilation 将被创建。Compilation 对象也提供了很多事件回调供插件做扩展。通过 Compilation 也能读取到 Compiler 对象。
Compiler 和 Compilation 的区别:
Compiler 代表了整个 Webpack 从启动到关闭的生命周期,而 Compilation 只是代表了一次新的编译。
2-2、新写法 webpack >=v5
class HelloWorldPlugin {
// webpack 会调用 HelloWorldPlugin 实例的 apply 方法给插件实例传入 compiler 对象
apply(compiler) {
// 指定一个挂载到 webpack 自身的事件钩子,实现插件功能
compiler.hooks.done.tap('HelloWorldPlugin', (compilation, callback) => {
// compilation 为当前打包构建流程的上下文
console.log('Hello World!')
// 功能完成后可以调用 webpack 提供的回调
callback()
})
}
}
module.exports = HelloWorldPlugin
新的 webpack 需要使用 compiler.hooks
的写法,点击查看 compiler 钩子。
3、使用插件
var HelloWorldPlugin = require('hello-world')
var webpackConfig = {
// ... 这里是其他配置 ...
plugins: [
new HelloWorldPlugin({options: true})
]
}
4、插件实现示例
这里写一个名为 EndWebpackPlugin 的插件,作用是在 Webpack 即将退出时再附加一些额外的操作,例如 Webpack 成功编译和输出了文件后执行发布操作把输出的文件上传到服务器。同时该插件还能区分 Webpack 构建是否执行成功。
- 创建插件:
老版的写法:
function Plugin1(options) {}
// 在插件函数的 prototype 上定义一个 apply 方法
Plugin1.prototype.apply = function (compiler) {
// 所有文件资源经过不同的 loader 处理后触发这个事件
compiler.plugin('emit', function (compilation, callback) {
// 获取打包后的 JS 文件名
const filename = compiler.options.output.filename
// 生成一个 index.html 并引入打包后的 JS 文件
const html = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="${filename}"></script>
</head>
<body>
</body>
</html>`
// 所有处理后的资源都放在 compilation.assets 中
// 添加一个 index.html 文件
compilation.assets['index.html'] = {
source: function () {
return html
},
size: function () {
return html.length
}
}
// 功能完成后调用 webpack 提供的回调
callback()
})
}
module.exports = Plugin1
新版的写法:
class Plugin1 {
apply(compiler) {
compiler.hooks.emit.tap('Plugin1', (compilation) => {
// 获取打包后的 JS 文件名
const filename = compiler.options.output.filename
// 生成一个 index.html 并引入打包后的 JS 文件
const html = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="${filename}"></script>
</head>
<body>
</body>
</html>`
// 所有处理后的资源都放在 compilation.assets 中
// 添加一个 index.html 文件
compilation.assets['index.html'] = {
source: function () {
return html
},
size: function () {
return html.length
}
}
// 功能完成后调用 webpack 提供的回调
// callback()
})
}
}
module.exports = Plugin1
- 使用插件:
const Plugin1 = require('./src/plugin1')
module.exports = {
// ...
plugins: [
new Plugin1(),
// 也可以这么写 ↓
// 在初始化 EndWebpackPlugin 时传入了两个参数,分别是在成功时的回调函数和失败时的回调函数
new Plugin1(
() => {
// Webpack 构建成功,并且文件输出了后会执行到这里,在这里可以做发布文件操作
},
(err) => {
// Webpack 构建失败,err 是导致错误的原因
console.error(err)
}
)
]
}
- 运行结果:
目录文件:
|- /dist
|- bundle.js
|- index.html
生成一个 index.html 并引入打包后的 JS 文件:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="bundle.js"></script>
</head>
<body>
</body>
</html>