chrome extension manifest V3 hot reload

135 阅读2分钟
  1. 背景

    chrome manifest v3 由于CSP,禁用了热更新,使得webpack + writetoDisk成为了插件开发的必选项之一,这导致了开发过程中需要不断的刷新service-workder+宿主网站,本文分享我的解决方案

  2. 实现原理

    • 启动项目的同时创建socket 服务器,等待contentjs链接
    • 监听开发环境产物文件,如果文件发生变化,发送消息给contentjs
    • contentjs 收到消息,通知service-worker更新当前tab和service-worker
  3. 具体实现

    • node ./watch.js - 随项目启动,执行当前脚本,启动socket服务器,脚本内容如下
    
    const chokidar = require('chokidar')
    const WebSocket = require('ws');
    
    const wss = new WebSocket.Server({ port: 1717 });
    wss.on('connection', (ws) => {
        const watcher = chokidar.watch(['./dist/development/js/contentScript.js', './dist/development/background.js'], {
            persistent: true
        });
        // 添加事件监听
        watcher
            .on('add', path => console.log(`文件 ${path} 已被添加`))
            .on('change', path => {
                console.log(`文件 ${path} 已被修改    `)
                ws.send("reload");
            })
            .on('unlink', path => console.log(`文件 ${path} 已被删除`))
            .on('error', error => console.log(`发生错误: ${error}`));
    });
    
    • contentjs 入口处执行以下代码
    export const AddHotReload = () => {
    if (isDev()) {
    
        const wsManager = new WebSocketManager("ws://localhost:1717");
        //页面加载完毕时,开启监听
        window.addEventListener('load', () => {
            wsManager.connect();
        });
        document.addEventListener("visibilitychange", () => {
            //如果当前页面不可见,关闭socket链接,可见的时候链接,避免刷新错误,和节能
            if (document.hidden) {
                wsManager.close();
            } else {
                wsManager.connect();
            }
        });
        // 设置收到消息时的回调函数
        wsManager.onMessage((message: string) => {
            console.log('处理接收到的消息:', message);
            if (!document.hidden) {
                if (chrome.runtime) {
                    chrome.runtime.sendMessage({ type:9999 })
                } else {
                    console.log("runtime empty")
                }
            }
        });
    }
    }
    
    
    //开发环境热更新
    export default class WebSocketManager {
        private url: string;
        private socket: WebSocket | null = null;
        private reconnectInterval: number;
        private maxReconnectAttempts: number;
        private reconnectAttempts: number;
        private messageCallback: ((message: string) => void) | null = null;
    
        constructor(url: string, reconnectInterval: number = 5000, maxReconnectAttempts: number = 10) {
            this.url = url;
            this.reconnectInterval = reconnectInterval;
            this.maxReconnectAttempts = maxReconnectAttempts;
            this.reconnectAttempts = 0;
        }
    
        // 初始化 WebSocket 连接
        public connect(): void {
            if (!this.socket || this.socket.readyState === WebSocket.CLOSED) {  
                console.log("正在连接 WebSocket...");
                this.socket = new WebSocket(this.url);
                this.socket.addEventListener('open', (event: Event) => {
                    console.log('WebSocket 连接已打开');
                    this.reconnectAttempts = 0; // 重置重新连接尝试次数
                });
    
                this.socket.addEventListener('message', (event: MessageEvent) => {
                    console.log('接收到消息:', event.data);
                    if (this.messageCallback) {
                        this.messageCallback(event.data); // 调用用户定义的回调函数
                    }
                });
    
                this.socket.addEventListener('close', (event: CloseEvent) => {
                    console.log('WebSocket 连接已关闭:', event.code, event.reason);
                    this.attemptReconnect(); // 尝试重新连接
                });
    
                this.socket.addEventListener('error', (event: Event) => {
                    console.error('WebSocket 出现错误:', event);
                });
            }
        }
    
        // 关闭 WebSocket 连接
        public close(): void {
            if (this.socket && this.socket.readyState === WebSocket.OPEN) {
                this.socket.close();
                console.log("WebSocket 连接已手动关闭");
            }
        }
    
        // 设置收到消息时的回调函数
        public onMessage(callback: (message: string) => void): void {
            this.messageCallback = callback;
        }
    
        // 尝试重新连接
        private attemptReconnect(): void {
            if (this.reconnectAttempts < this.maxReconnectAttempts) {
                this.reconnectAttempts++;
                console.log(`尝试重新连接 (${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
                setTimeout(() => {
                    this.connect();
                }, this.reconnectInterval);
            } else {
                console.log('达到最大重新连接次数,停止尝试');
            }
        }
    }
    
    
    • service-worker 入口处执行以下代码
    chrome.runtime.onMessage.addListener((request: any, sender, sendResponse) => {
        if (request.type == 9999) {
            chrome.windows.getCurrent(w => {
                chrome.tabs.query({ active: true, windowId: w.id }, (tabs: any) => {
                    chrome.tabs.reload(tabs[0].id);
                    chrome.runtime.reload()
                })
            })
        }
    })