首页 / 浏览器扩展开发入门教程 / 服务工作者注意事项与最佳实践

浏览器扩展开发入门教程

服务工作者注意事项与最佳实践

本教程共 56 篇 · 第 14 篇 · 更新于 2026-08-13 · 约 7 分钟阅读

服务工作者全局状态保活误区chrome.storagealarms最佳实践

本节目标:学完你能避开”变量丢了""定时器不响""强行保活”三大坑,知道状态该存哪、定时该用什么、什么情况下才需要真正的长连接。

服务工作者这套”唤醒—停止”模型很省资源,但和旧习惯冲突很大。初学者八成会在同几个地方栽跟头。这一节把最常见的坑和对应的正确写法一次讲透,你照着改基本就能避开 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 小结:三条铁律

把这一节的要点浓缩成三条,写后台前默念一遍:

  1. 跨唤醒的数据进 chrome.storage,别信顶层变量。
  2. 周期任务用 chrome.alarms,别用 setInterval 保活。
  3. 所有逻辑挂在顶层事件监听上,让服务工作者”随时醒、随时干、干完睡”。

做到这三点,服务工作者在你的扩展里就是稳定、省资源、可预期的,而不是一个随时丢状态、莫名其妙不响的隐患。

14-8 顶层变量不是完全不能用

前面说”别信顶层变量”,但要补一句关键限定:它不适合存需要跨唤醒保留的数据,却很适合存本次运行内的瞬时中间值。比如一次事件处理中从存储读出来、后面几行还要用的状态,放在局部或顶层变量里完全没问题,反正这次运行期间它不会消失。

区别在于”生命周期预期”:

  • 只在这次唤醒里用 —— 用变量,没问题。
  • 要在下次唤醒、或另一个组件里还要 —— 必须进 chrome.storage

混淆这两者的边界,正是 bug 的根源。一个简单判断法:问自己”如果服务工作者现在被停止、十分钟后才被下一个事件唤醒,这个值还该在吗?“该在,就存 storage;不该在,就用变量。

14-9 上线前的自检清单

写完后台,对照这张清单过一遍,能挡掉绝大多数问题:

  1. 所有 addListener 是否都在脚本顶层,不在函数内部?
  2. 需要跨唤醒的数据,是否都走 chrome.storage 而非裸变量?
  3. 周期任务是否用 chrome.alarms,没有 setInterval 式保活?
  4. 异步消息处理是否 return true 或返回 Promise?
  5. 是否用 chrome.alarms.get 判断过闹钟已存在,避免重复重置?

把这五条养成习惯,服务工作者相关的返工会少一大半。