本文将带你一块儿开发你的第一个 Webpack 插件,从 Webpack 配置工程师,迈向 Webpack 开发工程师!作本身的轮子,让别人用去吧。html
1、背景介绍本文灵感源自业务中的经验总结,不怕神同样的产品,只怕一根筋的开发。webpack
在项目打包遇到问题:“当项目托管到 CDN 平台,但愿实现项目中的 index.js 不被缓存”。由于咱们须要修改 index.js 中的内容,不想用户被缓存。web
思考一阵,有这么几种思路:数组
(聪明的你还有其余方法,欢迎讨论)缓存
思路分析:架构
因而我准备使用第三种方式,在app
index.html
生成以前完成下面修改:异步
问题简单,实际仍是想试试开发 Webpack Plugin。ide
2、基础知识Webpack 使用阶段式的构建回调,开发者能够引入它们本身的行为到 Webpack 构建流程中。在开发以前,须要了解如下 Webpack 相关概念:svg
在自定义插件以前,咱们须要了解,一个 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.jsvar HelloWorldPlugin = require('hello-world'); module.exports = { // ... 这里是其余配置 ...plugins: [new HelloWorldPlugin({ options: true })] }; 复制代码
HtmlWebpackPlugin 简化了 HTML 文件的建立,以便为你的 Webpack 包提供服务。这对于在文件名中包含每次会随着编译而发生变化哈希的 webpack bundle 尤为有用。
插件的基本做用归纳:生成 HTML 文件。
html-webapck-plugin 插件两个主要做用:
html-webapck-plugin 插件原理介绍:
本文开发的 自动添加时间戳引用脚本文件(SetScriptTimestampPlugin) 插件实现的原理:经过 HtmlWebpackPlugin 生成 HTML 文件前,将模版文件预留位置替换成脚本,脚本中执行自动添加时间戳来引用脚本文件。
3.2 初始化插件文件
新建 SetScriptTimestampPlugin.js 文件,并参考官方文档中插件的基本结构,初始化插件代码:
// SetScriptTimestampPlugin.jsclass SetScriptTimestampPlugin { apply(compiler) { compiler.hooks.done.tap('SetScriptTimestampPlugin', (compilation, callback) => { console.log('SetScriptTimestampPlugin!'); }); } } module.exports = SetScriptTimestampPlugin; 复制代码
apply 方法为插件原型方法,接收 compiler 做为参数。
选择插件触发时机,实际上是选择插件触发的 compiler 钩子(即什么时候触发插件)。Webpack 提供钩子有不少,这里简单介绍几个,完整具体可参考文档《Compiler Hooks》:
咱们插件应该是要在 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 提供三种触发钩子的方法:
这三种方式能选择的钩子方法也不一样,因为 compilation 是 SyncHook 同步钩子,因此采用 tap 触发方式。tap 方法接收两个参数:插件名称和回调函数。
咱们原理上是将模版文件中,指定替换入口,再替换成须要执行的脚本。
[data:image/svg+xml;utf8,<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="947" height="340"></svg>](data:image/svg+xml;utf8,<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="947" height="340"></svg>)
因此咱们在模版文件 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.jsclass 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; 复制代码
在上面插件逻辑中,具体作了这些事:
所谓“插件事件”即插件所提供的一些事件,用于监听插件状态,这里列举几个 html-webpack-plugin 提供的事件(完整可查看《html-webpack-plugin》):Async:
Sync:
在回调方法中,经过 htmlPluginData.assets.js 获取须要经过 script 引入的脚本文件名称列表,拷贝一份,并清空原有列表。
[data:image/svg+xml;utf8,<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="334" height="93"></svg>](data:image/svg+xml;utf8,<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="334" height="93"></svg>)
替换逻辑即:动态建立一个 script 标签,将其 src 值设置为上一步读取到的脚本文件名,并在后面拼接 时间戳 做为参数。
经过 htmlPluginData.html 能够获取到模版文件的字符串输出,咱们只须要将模版字符串中替换入口 <!--SetScriptTimestampPlugin inset script--> 替换成咱们上一步编写的替换逻辑便可。
最后将修改后的 HTML 字符串,赋值给原来的 htmlPluginData.html 达到修改效果。
自定义插件使用方式,与其余插件一致,在 plugins 数组中实例化:
// webpack.config.jsconst SetScriptTimestampPlugin = require("./SetScriptTimestampPlugin.js"); module.exports = { // ... 省略其余配置plugins: [ // ... 省略其余插件new SetScriptTimestampPlugin() ] } 复制代码
到这一步,咱们已经实现需求“当项目托管到 CDN 平台,但愿实现项目中的 index.js 不被缓存”。
[data:image/svg+xml;utf8,<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="467" height="291"></svg>](data:image/svg+xml;utf8,<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="467" height="291"></svg>)
4、案例拓展这里以以前 SetScriptTimestampPlugin 插件为例子,继续拓展。
每一个插件本质是一个类,跟一个类实例化相同,能够在实例化时传入配置参数,在构造函数中操做:
// SetScriptTimestampPlugin.jsclass SetScriptTimestampPlugin { constructor(options) { this.options = options; } apply(compiler) { console.log(this.options.filename); // "index.js"// ... 省略其余代码 } } module.exports = SetScriptTimestampPlugin; 复制代码
使用时:
// webpack.config.jsconst SetScriptTimestampPlugin = require("./SetScriptTimestampPlugin.js"); module.exports = { // ... 省略其余配置plugins: [ // ... 省略其余插件new SetScriptTimestampPlugin({ filename: "index.js" }) ] } 复制代码
若是咱们此时须要同时修改多个脚本文件的时间戳,也只须要将参数类型和执行脚本作下调整。具体修改脚本,这里不具体展开,篇幅有限,能够自行思考实现咯~这里展现使用插件时的参数:
// webpack.config.jsconst 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> 复制代码5、总结
本文通用自定义 Webpack 插件来实现平常一些比较棘手的需求。主要为你们介绍了 Webpack 插件的基本组成和简单架构,也介绍了 HtmlWebpackPlugin 插件。并经过这些基础知识,完成了一个 HTML 文本替换插件,最后经过两个场景来拓展插件使用范围。
最后,关于 Webpack 插件开发,还有更多知识能够学习,建议多看看官方文档《Writing a Plugin》进行学习。