服务工作者注意事项与最佳实践
本教程共 56 篇 · 第 14 篇 · 更新于 2026-08-13 · 约 7 分钟阅读
本节目标:学完你能避开”变量丢了""定时器不响""强行保活”三大坑,知道状态该存哪、定时该用什么、什么情况下才需要真正的长连接。
服务工作者这套”唤醒—停止”模型很省资源,但和旧习惯冲突很大。初学者八成会在同几个地方栽跟头。这一节把最常见的坑和对应的正确写法一次讲透,你照着改基本就能避开 90% 的后台 bug。
14-1 坑一:全局变量不持久
很多人喜欢在后台脚本顶层放一个变量,当成”全局状态”用:
// ❌ 看似方便,却不可靠
let isEnabled = true;
let cache = {};
chrome.runtime.onMessage.addListener((msg) => {
if (msg.type === 'toggle') isEnabled = !isEnabled;
});
问题在哪?服务工作者被停止后,这个脚本实例连同它的所有变量一起被销毁。下次被事件唤醒时,浏览器重新执行脚本,isEnabled 又被初始化成 true,你之前翻转过的状态全没了。
这就是非持久模型最反直觉的地方:顶层变量只在”一次运行生命周期”内有效,跨唤醒不保留。靠它记登录态、开关状态、临时缓存,全都会悄悄出错。
14-2 正解:状态交给 chrome.storage
任何需要”跨唤醒保留”的数据,都放进 chrome.storage,别放变量里。
// 读状态
const { isEnabled } = await chrome.storage.local.get('isEnabled');
// 改状态并持久化
await chrome.storage.local.set({ isEnabled: !isEnabled });
// 监听变化(多组件同步时很有用)
chrome.storage.onChanged.addListener((changes, area) => {
if (area === 'local' && changes.isEnabled) {
console.log('开关变为', changes.isEnabled.newValue);
}
});
chrome.storage.local 存在磁盘上,服务工作者醒来再读依然在。它还能通过 onChanged 通知所有组件,是后台共享状态的唯一靠谱通道。需要同步读写的场景,优先选它,而不是自己维护个全局对象。
Tip一句话口诀:变量记瞬时、storage 记持久。只在这一次运行里用到的中间值放变量;要跨唤醒、跨组件的数据放 storage。
14-3 坑二:误以为后台一直在”听”
第二个常见误解是觉得”后台脚本跑起来就会一直待命”。实际上服务工作者大部分时间是停止的,只有事件触发才醒来。于是有人写出这样的代码想”持续轮询”:
// ❌ 定时器在服务工作者停止后根本不会持续跑
setInterval(() => {
fetch('https://example.com/status').then(r => r.json()).then(save);
}, 30 * 1000);
这段在普通页面里没问题,在服务工作者里却几乎无效:脚本跑完顶层代码、没有挂起任务后,服务工作者就被停止了,setInterval 跟着一起死。等它下次被别的事件唤醒,又是全新的脚本实例,旧的定时器早没了。
14-4 正解:定时任务用 chrome.alarms
需要周期性干活,正式方案是 chrome.alarms。它归浏览器调度,和后台是否醒着无关。
{
"permissions": ["alarms"]
}
// 每 60 分钟执行一次,首次延迟 1 分钟
chrome.alarms.create('sync', {
delayInMinutes: 1,
periodInMinutes: 60
});
chrome.alarms.onAlarm.addListener(async (alarm) => {
if (alarm.name === 'sync') {
await syncFromServer();
}
});
alarms 的好处是:即使服务工作者正在休眠,到点也会把它唤醒一次执行监听器,执行完再回去睡。这才是 MV3 里”定时”的正确打开方式,彻底取代 setInterval 式的保活轮询。
14-5 坑三:滥用”保活”强行不让睡
最危险的误区,是为了让后台”不睡”,人为写个永不停的定时器或长轮询去吊着服务工作者。官方也点名过这类反模式。它短期看似解决了”变量丢了”的问题,代价是:扩展永远占着资源、更耗电、更容易被浏览器判定为异常,而且浏览器未来还会加强对这类行为的限制。
真正该做的,不是”不让它睡”,而是”让它睡了也能正确醒来”。把状态持久化到 storage、把定时交给 alarms、把一次性任务放进事件监听——这套组合拳下来,睡不睡根本不影响功能。
14-6 例外:真正需要长连接时的正确写法
当然也有 legit 场景需要保持一条长连接,比如后台要和服务器维持一个 WebSocket 通道实时收消息。这种时候官方给出的正确姿势是:用 WebSocket 本身的心跳包来保活,而不是靠占着服务工作者。
let webSocket = null;
function connect() {
webSocket = new WebSocket('wss://example.com/ws');
webSocket.onopen = () => {
console.log('websocket open');
keepAlive();
};
webSocket.onmessage = (event) => {
console.log('收到消息:', event.data);
};
}
function keepAlive() {
setInterval(() => {
if (webSocket) {
webSocket.send('keepalive');
}
}, 20 * 1000); // 20 秒一次心跳,避免连接被回收
}
connect();
注意这里有两个前提:第一,这是”确有长连接需求”才用,不是用来吊后台;第二,心跳间隔要足够短(官方示例用 20 秒)才能防止连接超时。绝大多数扩展并不需要这一步,别为了”保险”而加。
Note判断你要不要 WebSocket 保活:如果你只是想要”定时同步""记住开关”——那用 alarms + storage 就够了,根本碰不到长连接。只有实时双向通信才考虑 WebSocket。
14-7 小结:三条铁律
把这一节的要点浓缩成三条,写后台前默念一遍:
- 跨唤醒的数据进
chrome.storage,别信顶层变量。 - 周期任务用
chrome.alarms,别用setInterval保活。 - 所有逻辑挂在顶层事件监听上,让服务工作者”随时醒、随时干、干完睡”。
做到这三点,服务工作者在你的扩展里就是稳定、省资源、可预期的,而不是一个随时丢状态、莫名其妙不响的隐患。
14-8 顶层变量不是完全不能用
前面说”别信顶层变量”,但要补一句关键限定:它不适合存需要跨唤醒保留的数据,却很适合存本次运行内的瞬时中间值。比如一次事件处理中从存储读出来、后面几行还要用的状态,放在局部或顶层变量里完全没问题,反正这次运行期间它不会消失。
区别在于”生命周期预期”:
- 只在这次唤醒里用 —— 用变量,没问题。
- 要在下次唤醒、或另一个组件里还要 —— 必须进
chrome.storage。
混淆这两者的边界,正是 bug 的根源。一个简单判断法:问自己”如果服务工作者现在被停止、十分钟后才被下一个事件唤醒,这个值还该在吗?“该在,就存 storage;不该在,就用变量。
14-9 上线前的自检清单
写完后台,对照这张清单过一遍,能挡掉绝大多数问题:
- 所有
addListener是否都在脚本顶层,不在函数内部? - 需要跨唤醒的数据,是否都走
chrome.storage而非裸变量? - 周期任务是否用
chrome.alarms,没有setInterval式保活? - 异步消息处理是否
return true或返回 Promise? - 是否用
chrome.alarms.get判断过闹钟已存在,避免重复重置?
把这五条养成习惯,服务工作者相关的返工会少一大半。