UNPKG

vite-plugin-browserext-hmr

Version:

基于 Vite 构建的浏览器扩展开发环境,提供热更新功能,让扩展开发更加高效。

213 lines (202 loc) 7.01 kB
import { MatchPattern } from "@webext-core/match-patterns"; // Chrome扩展热更新客户端代码 const browser = globalThis.browser?.runtime?.id ? globalThis.browser : globalThis.chrome; const socketProtocol = __socketProtocol__; const socketHost = __socketHost__; const overlay = __overlay__; const wsToken = __wsToken__; let heartbeatTimer = null; function connectDevServer() { try { const ws = new WebSocket( `${socketProtocol}://${socketHost}?token=${wsToken}`, "vite-hmr" ); ws.addEventListener("open", () => { console.debug("Connected to dev server"); startHeartbeat(); }); ws.addEventListener("close", () => { console.debug( "Disconnected from dev server, attempting to reconnect in 1s..." ); if (heartbeatTimer) clearInterval(heartbeatTimer); setTimeout(connectDevServer, 5000); }); ws.addEventListener("error", (event) => { console.error("Failed to connect to dev server", event); }); // 工具函数:生成content script的唯一ID const getScriptId = (item) => `crx:${item.js?.join?.("-")}`; // 工具函数:批量操作content scripts const batchOperateContentScripts = async (data, handler) => { if (!Array.isArray(data)) return; for (const item of data) { const id = getScriptId(item); await handler(item, id); } }; /** * 优化后的消息处理函数,提升可读性、健壮性和性能 */ const messageHandler = async (e) => { let message; try { message = JSON.parse(e.data); } catch (err) { console.error("Message parsing failed:", err, e.data); return; } try { if ( message.type === "custom" && message.event === "crx-reload-content_scripts-register" ) { // 处理 content_scripts 注册 const ids = message?.data?.map(getScriptId); let registered = []; try { registered = (await browser?.scripting?.getRegisteredContentScripts?.({ ids, })) || []; } catch (err) { // 某些浏览器可能不支持该API,忽略 registered = []; } // 如果全部已注册,则只需更新 if (registered.length === message?.data?.length) { await batchOperateContentScripts( message.data, async (_item, id) => { await browser?.scripting?.updateContentScripts?.([ { allFrames: true, id }, ]); } ); return; } // 否则先全部注销再注册 try { await browser?.scripting?.unregisterContentScripts?.({ ids }); } catch (err) {} await batchOperateContentScripts(message.data, async (item, id) => { await browser?.scripting?.registerContentScripts?.([ { ...item, id }, ]); }); } else if ( message.type === "custom" && message.event === "crx-reload-content_scripts" ) { // 处理 content_scripts 热重载 let registered = []; try { registered = (await browser?.scripting?.getRegisteredContentScripts?.()) || []; } catch (err) { registered = []; } await batchOperateContentScripts(message.data, async (item, id) => { const existing = registered.find?.((cs) => cs.id === id); if (existing) { await browser?.scripting?.updateContentScripts?.([ { allFrames: true, id }, ]); } else { await browser?.scripting?.registerContentScripts?.([ { ...item, id }, ]); } }); // 只刷新用到这个插件的窗口 let allTabs = []; try { allTabs = (await browser?.tabs?.query({})) || []; } catch (err) { console.warn("Failed to retrieve tab:", err); return; } if (!allTabs.length) return; const matchPatterns = message?.data ?.map((i) => i.matches) ?.flat() ?.map((match) => new MatchPattern(match)); const matchingTabs = allTabs?.filter((tab) => { const url = tab.url; if (!url) return false; return !!matchPatterns?.find((pattern) => pattern.includes(url)); }); await Promise.all( matchingTabs?.map(async (tab) => { try { await browser.tabs.reload(tab.id); } catch (err) { console.warn("Failed to reload tab:", err); } }) ); } else if ( message.type === "custom" && (message.event === "crx-reload" || message.event === "vite:ws:disconnect") ) { // 处理扩展重载 browser.runtime?.reload?.(); } else if (message.type === "update") { // 处理错误覆盖层 try { if (overlay && typeof clearErrorOverlay === "function") { clearErrorOverlay(); } } catch (e) {} } else if ( (message.type === "error" || message.type === "crx-error") && message.err ) { // 处理构建错误 if (overlay && typeof createErrorOverlay === "function") { createErrorOverlay(message.err); } else { console.warn( `[vite-plugin-browserext-hmr] Internal Server Error\n${message.err.message}\n${message.err.stack}` ); } } } catch (err) { console.error("Message processing exception:", err, message); } }; ws.addEventListener("message", (e) => messageHandler(e)); return ws; } catch (error) { console.error("Error setting up WebSocket connection:", error); setTimeout(connectDevServer, 5000); return null; } } // 初始化热更新 const ws = connectDevServer(); function startHeartbeat() { heartbeatTimer = setInterval(() => { if (ws?.readyState === WebSocket.OPEN) { // 发送心跳并启动超时检测 ws.send(JSON.stringify({ type: "ping" })); } }, 30000); } // 添加卸载清理 globalThis?.addEventListener?.("beforeunload", () => { if (ws?.readyState === WebSocket.OPEN) { ws.close(); } if (heartbeatTimer) clearInterval(heartbeatTimer); }); function keepServiceWorkerAlive() { setInterval(async () => { await browser.runtime?.getPlatformInfo?.(); }, 5e3); } keepServiceWorkerAlive();