跳到主要内容

Chrome 扩展热重载后消息被处理两次?WXT HMR 多实例叠加监听器

· 阅读需 3 分钟

在为客户构建电商数据采集 Chrome 扩展时遇到此问题,记录根因与解法。

TL;DR

WXT 框架 HMR 热重载时,content script 重新执行但旧的 window.addEventListener('message', ...) 不会被清除。每热重载一次就多一个监听器实例,导致每条 postMessage 被处理 N 次。

解法:注册新监听器前,从 window 变量取出旧监听器引用并 removeEventListener

问题现象

浏览器控制台显示同一条消息被两个不同实例捕获:

content.js:114 [CCL] CCL_SHOP_REPORT_DAILY daily caught - 实例: nxctn6
content.js:2 [CCL] CCL_SHOP_REPORT_DAILY daily caught - 实例: t6jce7

每条 postMessage 被处理两次,导致后台发送重复请求。

根因

WXT (基于 Vite 的 Chrome 扩展框架) 开发模式下,修改 content script 后触发 HMR:

  1. 新的 content script 模块被加载执行
  2. 新的 window.addEventListener('message', messageListener) 被注册
  3. 旧的监听器函数仍然存在于内存中——HMR 不负责清理 DOM 事件监听器

结果:window 上挂载了多个独立的 message 监听器,每条 postMessage 触发所有实例。

解决方案

在 content script 入口处,注册新监听器前移除旧的:

const instanceId = Math.random().toString(36).slice(2, 8);

// 取出旧监听器引用
const prevListener = (window as any).__cclMessageListener;
if (prevListener) {
window.removeEventListener('message', prevListener);
}

// 定义新监听器
const messageListener = (event: MessageEvent) => {
// ... 处理逻辑
};

// 存储当前引用(供下次 HMR 取用)
(window as any).__cclMessageListener = messageListener;

// 注册
window.addEventListener('message', messageListener);

关键点removeEventListener 必须传入和 addEventListener 相同的函数引用。把函数存到 window 变量上,下次 HMR 时就能取出旧引用并正确移除。这样无论热重载多少次,始终只有一个活跃的 message 监听器。

如果你同时遇到 postMessage 的 targetOrigin 安全问题Cookie 取值导致数据全零,建议一并排查。

注意事项

  • 此问题不仅限于 WXT,所有支持 content script 热重载的框架(Plasmo、CRXJS 等)都可能遇到
  • window 上的变量在页面刷新前一直存在,HMR 只替换脚本模块不清除 window 属性
  • 生产构建不存在此问题(content script 只加载一次),但开发时会产生难以排查的重复请求