监听后台事件
本教程共 56 篇 · 第 13 篇 · 更新于 2026-08-13 · 约 6 分钟阅读
本节目标:学完你能列出后台最常用的一批事件,并照着写出 onInstalled、alarms、消息监听等的正确注册代码,知道为什么事件要写在脚本顶层。
服务工作者是”事件驱动”的,所以整个后台脚本几乎就是由一堆事件监听器组成的。你要让扩展在某个时刻做某事,不是写一个 while 循环去等,而是告诉浏览器:“这个事件来了,请调用我的函数。“浏览器负责在事件发生时唤醒服务工作者并执行它。
这一节把最常用、最容易上手的后台事件逐个讲清楚,并给出可直接照抄的写法。
13-1 初始化:runtime.onInstalled
onInstalled 在扩展第一次安装、版本更新、或浏览器自身更新时触发。它是做”初始化”的最佳位置,比如写入默认设置、设置徽章初始文字。
chrome.runtime.onInstalled.addListener(({ reason }) => {
if (reason === 'install') {
chrome.storage.local.set({
apiSuggestions: ['tabs', 'storage', 'scripting']
});
}
});
回调里收到的对象带一个 reason 字段,常见值有 install(首次安装)、update(扩展更新)、chrome_update(浏览器更新)。你可以根据它决定只在安装时初始化,避免每次更新都覆盖用户数据。
Tip把”首次安装才该做的事”放在
reason === 'install'判断里,别无条件执行,否则用户升级扩展后之前的设置可能被你清掉。
13-2 定时任务:chrome.alarms
服务工作者不能靠 setInterval 长周期保活,需要定时就交给 chrome.alarms。它能在后台休眠时也按时唤醒扩展。先在清单申请 alarms 权限:
{
"permissions": ["alarms"]
}
然后创建闹钟并监听它:
const ALARM_NAME = 'tip';
async function createAlarm() {
const alarm = await chrome.alarms.get(ALARM_NAME);
if (typeof alarm === 'undefined') {
chrome.alarms.create(ALARM_NAME, {
delayInMinutes: 1,
periodInMinutes: 1440
});
}
}
createAlarm();
chrome.alarms.onAlarm.addListener(async () => {
const response = await fetch('https://example.com/api/tip');
const data = await response.json();
await chrome.storage.local.set({ tip: data });
});
注意一个细节:创建前先 get 一下判断闹钟是否存在,这是官方推荐的兜底写法,能避免重复 create 把还没到点的闹钟误重置。闹钟的持久性在 Chrome 150+ 可以由 persistAcrossSessions 控制(true 表示持续到扩展更新;false 表示扩展重载或浏览器重启时清除),但官方仍建议每次启动时先 get 再 create。
13-3 消息监听:runtime.onMessage / onConnect
后台最常接的活儿,是别的组件(弹出页、内容脚本、选项页)发来的消息。一次性请求用 runtime.onMessage:
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.greeting === 'tip') {
chrome.storage.local.get('tip').then(sendResponse);
return true;
}
});
这里有个坑必须记住:如果你的处理函数里有异步操作(比如 fetch、读存储),必须返回 true,告诉浏览器”我还要用 sendResponse 回消息,别急着把服务工作者停掉”。不返回 true,异步还没跑完,服务工作者就被停止了,回复永远发不出去。
长连接用 runtime.onConnect,适合需要反复来回通信的场景:
chrome.runtime.onConnect.addListener((port) => {
port.onMessage.addListener((msg) => {
if (msg.joke === 'Knock knock') {
port.postMessage({ question: "Who's there?" });
}
});
});
13-4 工具栏点击:action.onClicked
如果你的扩展没有 default_popup,用户点工具栏图标时,会触发 action.onClicked。这是做”点一下就执行某个动作”的入口。
chrome.action.onClicked.addListener(async (tab) => {
const prev = await chrome.action.getBadgeText({ tabId: tab.id });
const next = prev === 'ON' ? 'OFF' : 'ON';
await chrome.action.setBadgeText({ tabId: tab.id, text: next });
});
Note一旦你在
action里配了default_popup,点图标就直接打开弹出页,onClicked不会再触发。二选一,别同时指望两者。
13-5 其他常用事件
后台还能监听很多别的事件,按需要添加:
chrome.runtime.onMessageExternal:接收来自其他扩展的消息,记得先校验sender.id再处理。chrome.omnibox.onInputChanged/onInputEntered:地址栏关键字输入的响应。chrome.tabs.onUpdated/onCreated:标签页变化时的钩子。chrome.runtime.onStartup:浏览器启动且扩展启用时触发(区别于onInstalled)。
chrome.runtime.onMessageExternal.addListener((request, sender, sendResponse) => {
if (sender.id === '允许通信的扩展ID') {
doSomething();
}
});
13-6 关键规则:事件必须注册在顶层
这是最容易被忽略、也最致命的一条。服务工作者随时会停止、随时会重新启动。每次启动时,浏览器会重新执行你的后台脚本。所以所有 addListener 必须写在脚本顶层(模块顶层),不能在某个函数内部、某个回调里才去注册。
错误写法:
// ❌ 危险:只有运行时调用了 startListening 才注册,
// 但服务工作者重新启动后如果没调用它,事件就永远没人接。
function startListening() {
chrome.runtime.onMessage.addListener(handleMessage);
}
正确写法:
// ✅ 每次脚本执行(即每次唤醒)都会重新挂上监听器。
chrome.runtime.onMessage.addListener(handleMessage);
事件注册是”声明式”的:告诉浏览器”这个事件来了找我”,而不是”我现在去等”。只要顶层挂好了,无论服务工作者被停止多少次,下次唤醒它都会重新挂上,事件就永远不会漏接。
Tip把每个事件监听当作一条”永久生效的订阅”。写在顶层,服务工作者每次醒来都会重新订阅,这就是它在非持久模型下依然可靠的秘密。
13-7 异步响应的另一种写法:返回 Promise
上一节提到,处理函数里有异步操作时必须 return true。如果你用的是 async 函数,还有更干净的写法:直接 return 一个 Promise,Chrome 会等这个 Promise 完成后再把结果作为响应发回。
// ✅ async 函数默认返回 Promise,Chrome 等它 resolve 后取返回值作为响应
chrome.runtime.onMessage.addListener(async (message) => {
const response = await fetch('https://example.com/api');
if (!response.ok) {
throw new Error(`请求失败: ${response.status}`);
}
return { statusCode: response.status };
});
这种写法不用再手动 return true,也不用手动调 sendResponse,可读性更好。但要注意一个隐性坑:async 函数始终返回一个 Promise,即便你中间 await 了一段什么都没返回的逻辑,Promise 最终 resolve 成 undefined,Chrome 会把它翻译成 null 当作响应。所以确保你的 async 监听器要么有明确返回值,要么就别用它做响应。
// ❌ 看起来在等,Promise 却 resolve 成 undefined,发送方收到 null
chrome.runtime.onMessage.addListener(async (message) => {
await new Promise(resolve => setTimeout(resolve, 1));
// 没有 return,等价于 return undefined
});
// ✅ 明确 return,发送方才能收到真正的值
chrome.runtime.onMessage.addListener(async (message) => {
await new Promise(resolve => setTimeout(resolve, 1));
return { ok: true };
});
13-8 监听器要”轻”,重活往后放
事件回调本身应当尽量轻量:快速判断事件类型、把活儿分发出去,不要在前几行就塞一大段同步重逻辑。服务工作者被唤醒后有一个执行窗口,处理太久可能被浏览器提前回收。需要耗时操作(网络请求、大量计算),用 async + await 交出去,并靠 return true 或返回 Promise 把窗口撑住,让异步任务跑完。
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type !== 'fetch-user') return; // 不相关的消息直接放过
handleFetchUser(message.id).then(sendResponse); // 重活交给独立函数
return true; // 撑住通道,等异步结果
});
把”判断”和”执行”分开,不仅符合服务工作者的运行节奏,代码也更容易维护和测试。