浏览器扩展:如何利用 Vite 实现开发热更新?

2,770 阅读2分钟

前言

做过浏览器扩展的小伙伴肯定知道,浏览器扩展是没有开发运行环境的,我们只能通过 开发者人员模式加载解压缩的扩展 来运行项目,这时加载的其实就是最后需要发布的资源。每次改动源码,必须手动刷新浏览器。如果想使用 Webpack/Vite 等需要打包的方式开发项目,则更麻烦,还要额外每次手动编译。

那么 Vite 自带的 HMR 能否实现热更新呢?显然不能,本地启动的 devServer 浏览器压根用不了,所以本文主要解决以下两个问题:

  1. 代码修改,自动重编译。
  2. 重编译完成,自动刷新浏览器。

自动重编译

这里纯粹水字数👻,实现起来非常简单,使用 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>

完整示例项目:bookmark-cleaner:自动检测失效书签链接,一键清理 🚀