前言
做过浏览器扩展的小伙伴肯定知道,浏览器扩展是没有开发运行环境的,我们只能通过 开发者人员模式 → 加载解压缩的扩展 来运行项目,这时加载的其实就是最后需要发布的资源。每次改动源码,必须手动刷新浏览器。如果想使用 Webpack/Vite 等需要打包的方式开发项目,则更麻烦,还要额外每次手动编译。
那么 Vite 自带的 HMR 能否实现热更新呢?显然不能,本地启动的 devServer 浏览器压根用不了,所以本文主要解决以下两个问题:
- 代码修改,自动重编译。
- 重编译完成,自动刷新浏览器。
自动重编译
这里纯粹水字数👻,实现起来非常简单,使用 vite build --watch 代替 vite build 即可。
自动刷新浏览器
思路也不复杂:编译结束 → 通知浏览器里的页面 → 页面调用刷新方法
1. 如何获知编译结束?
翻遍 Vite 文档也没有相关的配置项,只能自定义插件,而 Vite 特有的插件钩子同样无法实现,虽然标题写的 Vite,最终还得仰仗大哥 Rollup。代码见下文 ↓
2. 如何通知浏览器里的页面?
没啥说的,Websocket 永远滴神!
3. 页面如何刷新?
- 仅刷新当前页面:
window.location.reload() - 刷新整个插件:
chrome.runtime.reload()
4. 代码实现
安装依赖(强力推荐 PNPM!)
pnpm add -D ws
Rollup 插件:为什么通知加了延迟?因为当我们快速多次修改保存源码时,第一次编译完成触发了 closeBundle,紧接着又会重新编译,如果立刻通知刷新,浏览器扩展页面会加载失败,因为第二次编译还未完成,当真正完成编译已无法通知到页面。
所以我们在 closeBundle 钩子中延迟,在 watchChange 钩子中重新计时。
import { ConfigEnv, UserConfig } from "vite"
export default function() {
let wss: WebSocketServer
let ws:WebSocket
let timer
// 发送通知
const send = (msg) => {
if (!ws) return
msg = JSON.stringify(msg)
ws.send(msg)
}
// 清理资源
// 如果不清空变量的引用,插件将不会自动退出
const close = () => {
ws && ws.close()
wss && wss.close()
clearTimeout(timer)
ws = null
wss = null
timer = null
}
return {
name: 'build-notifier',
apply(config: UserConfig, { command }: ConfigEnv) {
// 我们只在 build 且 watch 的情况下使用插件
const canUse = command === 'build' && Boolean(config.build.watch)
if (canUse) {
// 创建 websocket server
wss = new WebSocketServer({ port: 2333 })
wss.on('connection', (client) => {
ws = client
})
}
return canUse
},
closeBundle() {
timer = setTimeout(() => send('watch-build-ok'), 500)
},
watchChange() {
clearTimeout(timer)
},
closeWatcher() {
close()
}
}
}
在 vite.config.ts 中引入插件:
import buildNotifier from './.vite/plugins/rollup-plugin-notifier'
export default defineConfig({
plugins: [
buildNotifier()
]
})
页面监听通知,并在需要热更新的页面中,通过 script 标签引入该脚本即可。
// 加上开发环境的判断
// 最终打包时 tree shaking 会移除这段用不到的代码
if (import.meta.env.DEV) {
const ws = new WebSocket('ws://localhost:2333')
ws.onmessage = (event) => {
let msg = JSON.parse(event.data)
if (msg === 'watch-build-ok') {
window.location.reload()
}
}
}
<body>
<div id="app" v-cloak></div>
<script type="module" src="../reload/home.ts"></script>
</body>