本文使用的 Webpack-Quickly-Starter 快速搭建 Webpack4 本地学习环境。
建议多阅读 Webpack 文档《Writing a Plugin》章节,学习开发简单插件。
本文将带你一起开发你的第一个 Webpack 插件,从 Webpack 配置工程师,迈向 Webpack 开发工程师!
做自己的轮子,让别人用去吧。
完整代码存放在:https://github.com/pingan8787/script-timestamp-webpack-plugin
本文灵感源自业务中的经验总结,不怕神一样的产品,只怕一根筋的开发。
在项目打包遇到问题:“当项目托管到 CDN 平台,希望实现项目中的 index.js 不被缓存”。因为我们需要修改 index.js
中的内容,不想用户被缓存。
思考一阵,有这么几种思路:
script
标签的 src
值,并添加时时间戳;script
标签引入文件,并添加时时间戳。(聪明的你还有其他方法,欢迎讨论)
思路分析:
script
标签,执行获取 Webpack 自动添加的 script
标签并为其 src
值添加时间戳。但事实是还没等你修改完, js 文件已经加载完毕,所以放弃index.html
生成之前,修改 js 文件的路径,并添加时间戳。于是我准备使用第三种方式,在 index.html
生成之前完成下面修改:
问题简单,实际还是想试试开发 Webpack Plugin。
Webpack 使用阶段式的构建回调,开发者可以引入它们自己的行为到 Webpack 构建流程中。
在开发之前,需要了解以下 Webpack 相关概念:
在自定义插件之前,我们先了解一个 Webpack 插件由哪些构成,下面摘抄文档:
插件由一个构造函数实例化出来。构造函数定义 apply
方法,在安装插件时,apply
方法会被 Webpack compiler
调用一次。apply
方法可以接收一个 Webpack compiler
对象的引用,从而可以在回调函数中访问到 compiler
对象。
官方文档提供一个简单的插件结构:
class HelloWorldPlugin {
apply(compiler) {
compiler.hooks.done.tap('Hello World Plugin', (
stats /* 在 hook 被触及时,会将 stats 作为参数传入。 */
) => {
console.log('Hello World!');
});
}
}
module.exports = HelloWorldPlugin;
使用插件:
// webpack.config.js
var HelloWorldPlugin = require('hello-world');
module.exports = {
// ... 这里是其他配置 ...
plugins: [new HelloWorldPlugin({ options: true })]
};
HtmlWebpackPlugin 简化了 HTML 文件的创建,以便为你的 Webpack 包提供服务。这对于在文件名中包含每次会随着编译而发生变化哈希的 webpack bundle 尤其有用。
插件的基本作用概括:生成 HTML 文件。
html-webapck-plugin
插件两个主要作用:
script
/ link
)动态添加每次编译后的 hash,防止引用文件的缓存问题;index.html
文件。html-webapck-plugin
插件原理:
entry
配置的相关入口 chunk
和 extract-text-webpack-plugin
插件抽取的 CSS 样式;template
或 templateContent
配置指定的模版文件中;link
标签引入样式,通过 script
标签引入脚本文件;本文介绍的 动态添加时间戳引用脚本文件(SetScriptTimestampPlugin) 插件实现的原理:通过 HtmlWebpackPlugin 生成 HTML 文件前,将模版文件预留位置替换成动态脚本,当访问 HTML 时,动态脚本中执行动态添加时间戳来引用 JS 本文件。
新建 SetScriptTimestampPlugin.js
文件,并参考官方文档中插件的基本结构,初始化插件代码:
// SetScriptTimestampPlugin.js
class SetScriptTimestampPlugin {
apply(compiler) {
compiler.hooks.done.tap('SetScriptTimestampPlugin',
(compilation, callback) => {
console.log('SetScriptTimestampPlugin!');
});
}
}
module.exports = SetScriptTimestampPlugin;
apply
方法为插件原型方法,接收 compiler
作为参数。
选择插件触发时机,其实是选择插件触发的 compiler 钩子(即何时触发插件)。
Webpack 提供钩子有很多,这里简单介绍几个,完整具体可参考文档《Compiler Hooks》:
entryOption
: 在 webpack 选项中的 entry
配置项处理过之后,执行插件。afterPlugins
: 设置完初始插件之后,执行插件。compilation
: 编译创建之后,生成文件之前,执行插件。emit
: 生成资源到 output
目录之前。done
: 编译完成。我们插件应该需要在 HTML 输出之前,动态添加 script
标签,所以我们选择钩入 compilation
阶段,代码修改:
// SetScriptTimestampPlugin.js
class SetScriptTimestampPlugin {
apply(compiler) {
- compiler.hooks.done.tap('SetScriptTimestampPlugin',
+ compiler.hooks.compilation.tap('SetScriptTimestampPlugin',
(compilation, callback) => {
console.log('SetScriptTimestampPlugin!');
});
}
}
module.exports = SetScriptTimestampPlugin;
在 compiler.hooks
下指定事件钩子函数,便会触发钩子时,执行回调函数。
Webpack 提供三种触发钩子的方法:
tap
:以同步方式触发钩子;tapAsync
:以异步方式触发钩子;tapPromise
:以异步方式触发钩子,返回 Promise;这三种方式对应能选择的钩子方法也不同,由于 compilation
是 SyncHook
同步钩子,所以采用 tap
触发方式。
tap
方法接收两个参数:插件名称和回调函数。
我们原理上是将模版文件中,指定替换入口,再替换成需要执行的脚本。
所以我们在模版文件 template.html
中添加 <!--SetScriptTimestampPlugin inset script-->
作为标识替换入口:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Webpack 插件开发入门</title>
</head>
<body>
<!-- other code -->
<!--SetScriptTimestampPlugin inset script-->
</body>
</html>
到这一步,才开始编写插件的逻辑。
从上一步的值,我们在 tap
第二个参数是个回调函数,且有两个参数: compilation
和 callback
。
compilation
继承于compiler
,包含 compiler
所有内容(也有 Webpack 的 options
),而且也有 plugin
函数接入任务点。
// SetScriptTimestampPlugin.js
class SetScriptTimestampPlugin {
apply(compiler) {
compiler.hooks.compilation.tap('SetScriptTimestampPlugin',
(compilation, callback) => {
// 插件逻辑 调用compilation提供的plugin方法
compilation.plugin(
"html-webpack-plugin-before-html-processing",
function(htmlPluginData, callback) {
// 读取并修改 script 上 src 列表
let jsScr = htmlPluginData.assets.js[0];
htmlPluginData.assets.js = [];
let result = `
<script>
let scriptDOM = document.createElement("script");
let jsScr = "./${jsScr}";
scriptDOM.src = jsScr + "?" + new Date().getTime();
document.body.appendChild(scriptDOM)
</script>
`;
let resultHTML = htmlPluginData.html.replace(
"<!--SetScriptTimestampPlugin inset script-->", result
);
// 返回修改后的结果
htmlPluginData.html = resultHTML;
}
);
}
);
}
}
module.exports = SetScriptTimestampPlugin;
在上面插件逻辑中,具体做了这些事:
compilation.plugin
方法,并传入两个参数:插件事件和回调方法。所谓“插件事件”即插件所提供的一些事件,用于监听插件状态,这里列举几个 html-webpack-plugin
提供的事件(完整可查看《html-webpack-plugin》):
Async:
html-webpack-plugin-before-html-generation
html-webpack-plugin-before-html-processing
html-webpack-plugin-alter-asset-tags
Sync:
html-webpack-plugin-alter-chunks
在回调方法中,通过 htmlPluginData.assets.js
获取需要通过 script
引入的脚本文件名称列表,拷贝一份,并清空原有列表。
替换逻辑即:动态创建一个 script
标签,将其 src
值设置为上一步读取到的脚本文件名,并在后面拼接 时间戳 作为参数。
通过 htmlPluginData.html
可以获取到模版文件的字符串输出,我们只需要将模版字符串中替换入口 <!--SetScriptTimestampPlugin inset script-->
替换成我们上一步编写的替换逻辑即可。
最后将修改后的 HTML 字符串,赋值给原来的 htmlPluginData.html
达到修改效果。
自定义插件使用方式,与其他插件一致,在 plugins
数组中实例化:
// webpack.config.js
const SetScriptTimestampPlugin = require("./SetScriptTimestampPlugin.js");
module.exports = {
// ... 省略其他配置
plugins: [
// ... 省略其他插件
new SetScriptTimestampPlugin()
]
}
到这一步,我们已经实现需求“当项目托管到 CDN 平台,希望实现项目中的 index.js 不被缓存”。
这里以之前 SetScriptTimestampPlugin 插件为例子,继续拓展。
每个插件本质是一个类,跟一个类实例化相同,可以在实例化时传入配置参数,在构造函数中操作:
// SetScriptTimestampPlugin.js
class SetScriptTimestampPlugin {
constructor(options) {
this.options = options;
}
apply(compiler) {
console.log(this.options.filename); // "index.js"
// ... 省略其他代码
}
}
module.exports = SetScriptTimestampPlugin;
使用时:
// webpack.config.js
const SetScriptTimestampPlugin = require("./SetScriptTimestampPlugin.js");
module.exports = {
// ... 省略其他配置
plugins: [
// ... 省略其他插件
new SetScriptTimestampPlugin({
filename: "index.js"
})
]
}
如果我们此时需要同时修改多个脚本文件的时间戳,也只需要将参数类型和执行脚本做下调整。
具体修改脚本,这里不具体展开,篇幅有限,可以自行思考实现咯~
这里展示使用插件时的参数:
// webpack.config.js
const SetScriptTimestampPlugin = require("./SetScriptTimestampPlugin.js");
module.exports = {
// ... 省略其他配置
plugins: [
// ... 省略其他插件
new SetScriptTimestampPlugin({
filename: ["index.js", "boundle.js", "pingan.js"]
})
]
}
生成结果:
<script src="./index.js?1582425467655"></script>
<script src="./boundle.js?1582425467655"></script>
<script src="./pingan.js?1582425467655"></script>
本文通用自定义 Webpack 插件来实现日常一些比较棘手的需求。主要为大家介绍了 Webpack 插件的基本组成和简单架构,也介绍了 HtmlWebpackPlugin 插件。并通过这些基础知识,完成了一个 HTML 文本替换插件,最后通过两个场景来拓展插件使用范围。
最后,关于 Webpack 插件开发,还有更多知识可以学习,建议多看看官方文档《Writing a Plugin》进行学习。
本文属个人经验总结,如有异议,欢迎指点。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。