Chrome MV3 插件架构深度解析:Service Worker 生命周期与 Token 管理的三层博弈

🧠 Chrome MV3 插件架构深度解析:Service Worker 生命周期与 Token 管理的三层博弈

副标题:当 30 秒生命周期遇上 CORS 铁墙------一个 Chrome 扩展开发者的架构生存指南


一、引言:为什么你的插件总在"莫名其妙"地断连?

如果你正在开发一个 Chrome Manifest V3 (MV3) 扩展,并且遇到了以下症状:

  • 插件运行一段时间后,Background 的 WebSocket 突然断开
  • chrome.storage 里的数据还在,但内存中的变量全部清零
  • 用户反馈"插件用着用着就不工作了,刷新页面又好了"
  • 你明明写了 setInterval 做心跳,却发现定时器"蒸发"了

那么恭喜你,你已经踩进了 MV3 Service Worker 生命周期 这个大坑。

本文将从架构层面深入剖析:

  1. Service Worker 的 30 秒死亡倒计时------闲置判断标准到底是什么?
  2. CORS 铁墙------为什么 Content Script 永远无法直接调用你的后端 API?
  3. Token 管理的三层博弈------Background、Content Script、Storage 之间的权力分配

二、MV3 架构全景:三层世界的隔离与协作

在深入生命周期之前,我们先建立 MV3 的架构认知。Chrome 扩展不是"一个网页里跑几段 JS"那么简单,它是一个多进程、多上下文、带隔离墙的分布式系统。
#mermaid-svg-XXyqMYTEfkC1aXI6{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-XXyqMYTEfkC1aXI6 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-XXyqMYTEfkC1aXI6 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-XXyqMYTEfkC1aXI6 .error-icon{fill:#552222;}#mermaid-svg-XXyqMYTEfkC1aXI6 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-XXyqMYTEfkC1aXI6 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-XXyqMYTEfkC1aXI6 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-XXyqMYTEfkC1aXI6 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-XXyqMYTEfkC1aXI6 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-XXyqMYTEfkC1aXI6 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-XXyqMYTEfkC1aXI6 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-XXyqMYTEfkC1aXI6 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-XXyqMYTEfkC1aXI6 .marker.cross{stroke:#333333;}#mermaid-svg-XXyqMYTEfkC1aXI6 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-XXyqMYTEfkC1aXI6 p{margin:0;}#mermaid-svg-XXyqMYTEfkC1aXI6 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-XXyqMYTEfkC1aXI6 .cluster-label text{fill:#333;}#mermaid-svg-XXyqMYTEfkC1aXI6 .cluster-label span{color:#333;}#mermaid-svg-XXyqMYTEfkC1aXI6 .cluster-label span p{background-color:transparent;}#mermaid-svg-XXyqMYTEfkC1aXI6 .label text,#mermaid-svg-XXyqMYTEfkC1aXI6 span{fill:#333;color:#333;}#mermaid-svg-XXyqMYTEfkC1aXI6 .node rect,#mermaid-svg-XXyqMYTEfkC1aXI6 .node circle,#mermaid-svg-XXyqMYTEfkC1aXI6 .node ellipse,#mermaid-svg-XXyqMYTEfkC1aXI6 .node polygon,#mermaid-svg-XXyqMYTEfkC1aXI6 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-XXyqMYTEfkC1aXI6 .rough-node .label text,#mermaid-svg-XXyqMYTEfkC1aXI6 .node .label text,#mermaid-svg-XXyqMYTEfkC1aXI6 .image-shape .label,#mermaid-svg-XXyqMYTEfkC1aXI6 .icon-shape .label{text-anchor:middle;}#mermaid-svg-XXyqMYTEfkC1aXI6 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-XXyqMYTEfkC1aXI6 .rough-node .label,#mermaid-svg-XXyqMYTEfkC1aXI6 .node .label,#mermaid-svg-XXyqMYTEfkC1aXI6 .image-shape .label,#mermaid-svg-XXyqMYTEfkC1aXI6 .icon-shape .label{text-align:center;}#mermaid-svg-XXyqMYTEfkC1aXI6 .node.clickable{cursor:pointer;}#mermaid-svg-XXyqMYTEfkC1aXI6 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-XXyqMYTEfkC1aXI6 .arrowheadPath{fill:#333333;}#mermaid-svg-XXyqMYTEfkC1aXI6 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-XXyqMYTEfkC1aXI6 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-XXyqMYTEfkC1aXI6 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-XXyqMYTEfkC1aXI6 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-XXyqMYTEfkC1aXI6 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-XXyqMYTEfkC1aXI6 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-XXyqMYTEfkC1aXI6 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-XXyqMYTEfkC1aXI6 .cluster text{fill:#333;}#mermaid-svg-XXyqMYTEfkC1aXI6 .cluster span{color:#333;}#mermaid-svg-XXyqMYTEfkC1aXI6 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-XXyqMYTEfkC1aXI6 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-XXyqMYTEfkC1aXI6 rect.text{fill:none;stroke-width:0;}#mermaid-svg-XXyqMYTEfkC1aXI6 .icon-shape,#mermaid-svg-XXyqMYTEfkC1aXI6 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-XXyqMYTEfkC1aXI6 .icon-shape p,#mermaid-svg-XXyqMYTEfkC1aXI6 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-XXyqMYTEfkC1aXI6 .icon-shape .label rect,#mermaid-svg-XXyqMYTEfkC1aXI6 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-XXyqMYTEfkC1aXI6 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-XXyqMYTEfkC1aXI6 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-XXyqMYTEfkC1aXI6 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 🔮 后端 API
💾 持久化层
🔌 插件层 (Extension Contexts)
🌐 宿主页面层 (Host Page)
Background Service Worker
Content Script
读取/修改 DOM
sendMessage
fetch + Token
持久化/恢复
Popup
点击图标弹出
临时 UI 层
🔒 隔离墙 (Isolated World)
Chrome 强制隔离
页面 JS 无法访问 Content Script 变量
web.whatsapp.com
页面 DOM
页面 JS 运行环境
注入在宿主页面中
可读写 DOM
❌ 不持有 Token
sendMessage → Background
⚙️ 事件驱动后台进程
✅ 持有 Token
绕过 CORS 限制
代理所有 API 请求
chrome.storage.local
chrome.storage.sync
chrome.storage.session
iris.autosafrica.com
CRM 业务接口

2.1 三层架构的核心差异

层级 生命周期 能否直接访问 DOM CORS 权限 Token 存储 进程模型
Content Script 与页面同生共死 ✅ 完全读写 ❌ 受宿主页面限制 ❌ 不应存储 渲染进程
Background SW 事件驱动,30秒闲置终止 ❌ 无 DOM ✅ 绕过 CORS ✅ 内存+Storage 独立 Service Worker 进程
Popup 点击存在,失焦销毁 ❌ 无 DOM ✅ 独立权限 ⚠️ 临时状态 渲染进程

关键洞察 :Content Script 和 Background SW 不是"同一个程序的两个文件",而是运行在不同进程、不同安全沙箱中的两个独立程序 ,它们之间的通信是跨进程消息传递(IPC),不是函数调用。


三、Service Worker 生命周期:30 秒死亡倒计时详解

这是 MV3 中最反直觉的设计。很多前端开发者(包括我)第一次听到"30 秒就杀死"时的反应是:"什么?我的后台进程只能活 30 秒?"

是的。而且这 30 秒不是"从启动开始算",而是**"从最后一个事件处理完毕开始算"**。
Chrome 浏览器 chrome.storage Service Worker 事件源 Chrome 浏览器 chrome.storage Service Worker 事件源 #mermaid-svg-JfseVZrDlCtroivN{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-JfseVZrDlCtroivN .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-JfseVZrDlCtroivN .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-JfseVZrDlCtroivN .error-icon{fill:#552222;}#mermaid-svg-JfseVZrDlCtroivN .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-JfseVZrDlCtroivN .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-JfseVZrDlCtroivN .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-JfseVZrDlCtroivN .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-JfseVZrDlCtroivN .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-JfseVZrDlCtroivN .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-JfseVZrDlCtroivN .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-JfseVZrDlCtroivN .marker{fill:#333333;stroke:#333333;}#mermaid-svg-JfseVZrDlCtroivN .marker.cross{stroke:#333333;}#mermaid-svg-JfseVZrDlCtroivN svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-JfseVZrDlCtroivN p{margin:0;}#mermaid-svg-JfseVZrDlCtroivN .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-JfseVZrDlCtroivN text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-JfseVZrDlCtroivN .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-JfseVZrDlCtroivN .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-JfseVZrDlCtroivN .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-JfseVZrDlCtroivN .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-JfseVZrDlCtroivN #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-JfseVZrDlCtroivN .sequenceNumber{fill:white;}#mermaid-svg-JfseVZrDlCtroivN #sequencenumber{fill:#333;}#mermaid-svg-JfseVZrDlCtroivN #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-JfseVZrDlCtroivN .messageText{fill:#333;stroke:none;}#mermaid-svg-JfseVZrDlCtroivN .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-JfseVZrDlCtroivN .labelText,#mermaid-svg-JfseVZrDlCtroivN .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-JfseVZrDlCtroivN .loopText,#mermaid-svg-JfseVZrDlCtroivN .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-JfseVZrDlCtroivN .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-JfseVZrDlCtroivN .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-JfseVZrDlCtroivN .noteText,#mermaid-svg-JfseVZrDlCtroivN .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-JfseVZrDlCtroivN .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-JfseVZrDlCtroivN .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-JfseVZrDlCtroivN .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-JfseVZrDlCtroivN .actorPopupMenu{position:absolute;}#mermaid-svg-JfseVZrDlCtroivN .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-JfseVZrDlCtroivN .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-JfseVZrDlCtroivN .actor-man circle,#mermaid-svg-JfseVZrDlCtroivN line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-JfseVZrDlCtroivN :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Service Worker 生命周期时间轴 ⏱️ 闲置计时器开始: 30秒 ✅ 计时器重置,继续存活 opt 20秒内收到新事件 ❌ 进程被杀死 内存变量全部清零 WebSocket 断开 setTimeout/setInterval 失效 opt 30秒内无事件 🔄 冷启动:全新进程 需要重新从 storage 恢复状态 事件触发 (onMessage / onAlarm / onFetch) 启动/唤醒进程 从 storage 恢复状态 返回 Token、配置等 执行业务逻辑 返回响应 新事件进入 重置计时器 💀 终止进程 新事件触发 再次读取持久化数据 返回数据 重新初始化

3.1 闲置判断标准:Chrome 到底怎么定义"闲置"?

Chrome 的闲置判断非常严格,以下所有条件同时满足时,才会启动 30 秒倒计时:
#mermaid-svg-Uiknk90Y8w6JYLD8{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Uiknk90Y8w6JYLD8 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Uiknk90Y8w6JYLD8 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Uiknk90Y8w6JYLD8 .error-icon{fill:#552222;}#mermaid-svg-Uiknk90Y8w6JYLD8 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Uiknk90Y8w6JYLD8 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Uiknk90Y8w6JYLD8 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Uiknk90Y8w6JYLD8 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Uiknk90Y8w6JYLD8 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Uiknk90Y8w6JYLD8 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Uiknk90Y8w6JYLD8 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Uiknk90Y8w6JYLD8 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Uiknk90Y8w6JYLD8 .marker.cross{stroke:#333333;}#mermaid-svg-Uiknk90Y8w6JYLD8 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Uiknk90Y8w6JYLD8 p{margin:0;}#mermaid-svg-Uiknk90Y8w6JYLD8 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Uiknk90Y8w6JYLD8 .cluster-label text{fill:#333;}#mermaid-svg-Uiknk90Y8w6JYLD8 .cluster-label span{color:#333;}#mermaid-svg-Uiknk90Y8w6JYLD8 .cluster-label span p{background-color:transparent;}#mermaid-svg-Uiknk90Y8w6JYLD8 .label text,#mermaid-svg-Uiknk90Y8w6JYLD8 span{fill:#333;color:#333;}#mermaid-svg-Uiknk90Y8w6JYLD8 .node rect,#mermaid-svg-Uiknk90Y8w6JYLD8 .node circle,#mermaid-svg-Uiknk90Y8w6JYLD8 .node ellipse,#mermaid-svg-Uiknk90Y8w6JYLD8 .node polygon,#mermaid-svg-Uiknk90Y8w6JYLD8 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Uiknk90Y8w6JYLD8 .rough-node .label text,#mermaid-svg-Uiknk90Y8w6JYLD8 .node .label text,#mermaid-svg-Uiknk90Y8w6JYLD8 .image-shape .label,#mermaid-svg-Uiknk90Y8w6JYLD8 .icon-shape .label{text-anchor:middle;}#mermaid-svg-Uiknk90Y8w6JYLD8 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Uiknk90Y8w6JYLD8 .rough-node .label,#mermaid-svg-Uiknk90Y8w6JYLD8 .node .label,#mermaid-svg-Uiknk90Y8w6JYLD8 .image-shape .label,#mermaid-svg-Uiknk90Y8w6JYLD8 .icon-shape .label{text-align:center;}#mermaid-svg-Uiknk90Y8w6JYLD8 .node.clickable{cursor:pointer;}#mermaid-svg-Uiknk90Y8w6JYLD8 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Uiknk90Y8w6JYLD8 .arrowheadPath{fill:#333333;}#mermaid-svg-Uiknk90Y8w6JYLD8 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Uiknk90Y8w6JYLD8 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Uiknk90Y8w6JYLD8 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Uiknk90Y8w6JYLD8 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Uiknk90Y8w6JYLD8 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Uiknk90Y8w6JYLD8 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Uiknk90Y8w6JYLD8 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Uiknk90Y8w6JYLD8 .cluster text{fill:#333;}#mermaid-svg-Uiknk90Y8w6JYLD8 .cluster span{color:#333;}#mermaid-svg-Uiknk90Y8w6JYLD8 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Uiknk90Y8w6JYLD8 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Uiknk90Y8w6JYLD8 rect.text{fill:none;stroke-width:0;}#mermaid-svg-Uiknk90Y8w6JYLD8 .icon-shape,#mermaid-svg-Uiknk90Y8w6JYLD8 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Uiknk90Y8w6JYLD8 .icon-shape p,#mermaid-svg-Uiknk90Y8w6JYLD8 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Uiknk90Y8w6JYLD8 .icon-shape .label rect,#mermaid-svg-Uiknk90Y8w6JYLD8 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Uiknk90Y8w6JYLD8 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Uiknk90Y8w6JYLD8 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Uiknk90Y8w6JYLD8 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否















Chrome 检查 SW 是否闲置
是否有未处理的事件?
是否有正在进行的网络请求?
是否有未完成的 WebSocket 连接?
是否有未到期的 setTimeout/setInterval?
是否有活跃的 chrome.debugger 会话?
是否有 Port 长连接消息?
是否有 chrome.alarms 待触发?
✅ 判定为闲置
启动 30 秒倒计时
30秒内是否有新事件?
重置计时器
💀 终止 Service Worker
❌ 不闲置,继续运行

详细判断标准:
检查项 说明 版本备注
无新事件进入 chrome.runtime.onMessage 等事件队列空 所有版本
无网络活动 fetch/XMLHttpRequest 无进行中的请求 所有版本
无 WebSocket 活动 Chrome 116+:WS 流量可重置计时器;但无流量时仍会计时 Chrome 116+
无定时器到期 setTimeout/setInterval 未触发(注意:定时器本身不会阻止闲置) 所有版本
无 debugger 会话 Chrome 118+:活跃的 chrome.debugger 可保活 Chrome 118+
无 Port 消息 chrome.runtime.connect 建立的长连接无消息交换 所有版本
无 Alarm 待触发 chrome.alarms 注册的闹钟未到期 所有版本

重要误区澄清

  • setInterval(() => {}, 1000) 不会阻止 SW 终止,因为定时器回调本身是一个事件,如果回调里不做任何 Chrome API 调用,Chrome 认为"没有有意义的事件"
  • chrome.alarms.create({periodInMinutes: 1/3}) 重置计时器,因为 chrome.alarms 是 Chrome 原生 API,浏览器会追踪
  • ✅ Content Script 每 10 秒 sendMessage 重置计时器,因为消息进入是一个事件

3.2 终止后的真实影响

#mermaid-svg-nQ86c7sgLYHtGn2S{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-nQ86c7sgLYHtGn2S .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-nQ86c7sgLYHtGn2S .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-nQ86c7sgLYHtGn2S .error-icon{fill:#552222;}#mermaid-svg-nQ86c7sgLYHtGn2S .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-nQ86c7sgLYHtGn2S .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-nQ86c7sgLYHtGn2S .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-nQ86c7sgLYHtGn2S .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-nQ86c7sgLYHtGn2S .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-nQ86c7sgLYHtGn2S .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-nQ86c7sgLYHtGn2S .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-nQ86c7sgLYHtGn2S .marker{fill:#333333;stroke:#333333;}#mermaid-svg-nQ86c7sgLYHtGn2S .marker.cross{stroke:#333333;}#mermaid-svg-nQ86c7sgLYHtGn2S svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-nQ86c7sgLYHtGn2S p{margin:0;}#mermaid-svg-nQ86c7sgLYHtGn2S .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-nQ86c7sgLYHtGn2S .cluster-label text{fill:#333;}#mermaid-svg-nQ86c7sgLYHtGn2S .cluster-label span{color:#333;}#mermaid-svg-nQ86c7sgLYHtGn2S .cluster-label span p{background-color:transparent;}#mermaid-svg-nQ86c7sgLYHtGn2S .label text,#mermaid-svg-nQ86c7sgLYHtGn2S span{fill:#333;color:#333;}#mermaid-svg-nQ86c7sgLYHtGn2S .node rect,#mermaid-svg-nQ86c7sgLYHtGn2S .node circle,#mermaid-svg-nQ86c7sgLYHtGn2S .node ellipse,#mermaid-svg-nQ86c7sgLYHtGn2S .node polygon,#mermaid-svg-nQ86c7sgLYHtGn2S .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-nQ86c7sgLYHtGn2S .rough-node .label text,#mermaid-svg-nQ86c7sgLYHtGn2S .node .label text,#mermaid-svg-nQ86c7sgLYHtGn2S .image-shape .label,#mermaid-svg-nQ86c7sgLYHtGn2S .icon-shape .label{text-anchor:middle;}#mermaid-svg-nQ86c7sgLYHtGn2S .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-nQ86c7sgLYHtGn2S .rough-node .label,#mermaid-svg-nQ86c7sgLYHtGn2S .node .label,#mermaid-svg-nQ86c7sgLYHtGn2S .image-shape .label,#mermaid-svg-nQ86c7sgLYHtGn2S .icon-shape .label{text-align:center;}#mermaid-svg-nQ86c7sgLYHtGn2S .node.clickable{cursor:pointer;}#mermaid-svg-nQ86c7sgLYHtGn2S .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-nQ86c7sgLYHtGn2S .arrowheadPath{fill:#333333;}#mermaid-svg-nQ86c7sgLYHtGn2S .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-nQ86c7sgLYHtGn2S .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-nQ86c7sgLYHtGn2S .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-nQ86c7sgLYHtGn2S .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-nQ86c7sgLYHtGn2S .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-nQ86c7sgLYHtGn2S .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-nQ86c7sgLYHtGn2S .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-nQ86c7sgLYHtGn2S .cluster text{fill:#333;}#mermaid-svg-nQ86c7sgLYHtGn2S .cluster span{color:#333;}#mermaid-svg-nQ86c7sgLYHtGn2S div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-nQ86c7sgLYHtGn2S .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-nQ86c7sgLYHtGn2S rect.text{fill:none;stroke-width:0;}#mermaid-svg-nQ86c7sgLYHtGn2S .icon-shape,#mermaid-svg-nQ86c7sgLYHtGn2S .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-nQ86c7sgLYHtGn2S .icon-shape p,#mermaid-svg-nQ86c7sgLYHtGn2S .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-nQ86c7sgLYHtGn2S .icon-shape .label rect,#mermaid-svg-nQ86c7sgLYHtGn2S .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-nQ86c7sgLYHtGn2S .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-nQ86c7sgLYHtGn2S .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-nQ86c7sgLYHtGn2S :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 持久化层 ✅
终止后状态 💀
终止前状态
终止
终止
终止
终止
冷启动时恢复
冷启动时恢复
内存变量: token='abc123'
WebSocket: 连接中
setInterval: 心跳定时器
Map对象: tabSessions
内存变量: ❌ 全部清零
WebSocket: ❌ 连接断开
setInterval: ❌ 定时器失效
Map对象: ❌ 全部丢失
chrome.storage: Token还在
chrome.storage: 配置还在

血泪教训 :很多开发者把 Token 存在 let token = 'xxx' 这样的内存变量里,结果 SW 一终止,用户就得重新登录。正确的做法是:所有需要跨会话存活的状态,必须持久化到 chrome.storage


四、CORS 铁墙:为什么 Content Script 永远无法直接调用你的 API?

这是决定 Token 管理架构的真正约束,比安全考量更根本。

4.1 MV3 的 CORS 规则

Background SW iris.autosafrica.com Chrome 浏览器 Content Script web.whatsapp.com Background SW iris.autosafrica.com Chrome 浏览器 Content Script web.whatsapp.com #mermaid-svg-dqtL2vfkciQTGvW5{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-dqtL2vfkciQTGvW5 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-dqtL2vfkciQTGvW5 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-dqtL2vfkciQTGvW5 .error-icon{fill:#552222;}#mermaid-svg-dqtL2vfkciQTGvW5 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-dqtL2vfkciQTGvW5 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-dqtL2vfkciQTGvW5 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-dqtL2vfkciQTGvW5 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-dqtL2vfkciQTGvW5 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-dqtL2vfkciQTGvW5 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-dqtL2vfkciQTGvW5 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-dqtL2vfkciQTGvW5 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-dqtL2vfkciQTGvW5 .marker.cross{stroke:#333333;}#mermaid-svg-dqtL2vfkciQTGvW5 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-dqtL2vfkciQTGvW5 p{margin:0;}#mermaid-svg-dqtL2vfkciQTGvW5 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-dqtL2vfkciQTGvW5 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-dqtL2vfkciQTGvW5 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-dqtL2vfkciQTGvW5 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-dqtL2vfkciQTGvW5 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-dqtL2vfkciQTGvW5 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-dqtL2vfkciQTGvW5 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-dqtL2vfkciQTGvW5 .sequenceNumber{fill:white;}#mermaid-svg-dqtL2vfkciQTGvW5 #sequencenumber{fill:#333;}#mermaid-svg-dqtL2vfkciQTGvW5 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-dqtL2vfkciQTGvW5 .messageText{fill:#333;stroke:none;}#mermaid-svg-dqtL2vfkciQTGvW5 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-dqtL2vfkciQTGvW5 .labelText,#mermaid-svg-dqtL2vfkciQTGvW5 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-dqtL2vfkciQTGvW5 .loopText,#mermaid-svg-dqtL2vfkciQTGvW5 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-dqtL2vfkciQTGvW5 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-dqtL2vfkciQTGvW5 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-dqtL2vfkciQTGvW5 .noteText,#mermaid-svg-dqtL2vfkciQTGvW5 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-dqtL2vfkciQTGvW5 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-dqtL2vfkciQTGvW5 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-dqtL2vfkciQTGvW5 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-dqtL2vfkciQTGvW5 .actorPopupMenu{position:absolute;}#mermaid-svg-dqtL2vfkciQTGvW5 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-dqtL2vfkciQTGvW5 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-dqtL2vfkciQTGvW5 .actor-man circle,#mermaid-svg-dqtL2vfkciQTGvW5 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-dqtL2vfkciQTGvW5 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 场景:Content Script 直接 fetch API 请求来源 = web.whatsapp.com 目标 = iris.autosafrica.com Content Script 无法直接调用 API ⚠️ 但 Token 暴露在页面上下文 alt API 未返回 whatsapp.com 的 CORS 许可 API 返回了 CORS 许可(极罕见) 场景:Background SW 代理请求 Background SW 拥有 host_permissions 声明了 iris.autosafrica.com ✅ 绕过 CORS 限制 ✅ 请求成功,Token 安全隔离 fetch('https://iris.autosafrica.com/...') 检查 CORS 策略 发送预检请求 OPTIONS 返回 CORS 头 Access-Control-Allow-Origin: ? ❌ CORS 错误 blocked by CORS policy 实际请求 响应数据 返回数据 sendMessage({type: 'API_CALL', data: {...}}) fetch('https://iris.autosafrica.com/...') 带有 host_permissions 直接请求(无需 CORS 预检) 响应数据 返回数据 sendResponse(data)

4.2 核心规则

Content Script 的 fetch() 遵循宿主页面的 CORS 策略

  • Content Script 注入在 web.whatsapp.com
  • 它发起的任何网络请求,浏览器都会视为 "whatsapp.com 这个网页在请求"
  • 如果目标 API(如 iris.autosafrica.com)没有返回 Access-Control-Allow-Origin: https://web.whatsapp.com,请求就会被浏览器拦截

Background SW 不一样

  • manifest.json 中声明了 host_permissions: ["https://iris.autosafrica.com/*"]
  • Chrome 赋予 Background SW 特权:可以绕过 CORS 直接请求这些域名
  • 这是插件架构的设计,不是漏洞

结论 :在 MV3 中,所有跨域 API 调用都必须经过 Background SW 代理------这不是"最佳实践"的选择题,而是"能不能工作"的是非题。


五、Token 管理的三层博弈:三种架构方案对比

既然 CORS 强制要求 API 请求走 Background,那 Token 放在哪里?这引出了三种方案:
#mermaid-svg-IyQpbgUi3JJneHob{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-IyQpbgUi3JJneHob .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-IyQpbgUi3JJneHob .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-IyQpbgUi3JJneHob .error-icon{fill:#552222;}#mermaid-svg-IyQpbgUi3JJneHob .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-IyQpbgUi3JJneHob .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-IyQpbgUi3JJneHob .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-IyQpbgUi3JJneHob .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-IyQpbgUi3JJneHob .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-IyQpbgUi3JJneHob .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-IyQpbgUi3JJneHob .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-IyQpbgUi3JJneHob .marker{fill:#333333;stroke:#333333;}#mermaid-svg-IyQpbgUi3JJneHob .marker.cross{stroke:#333333;}#mermaid-svg-IyQpbgUi3JJneHob svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-IyQpbgUi3JJneHob p{margin:0;}#mermaid-svg-IyQpbgUi3JJneHob .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-IyQpbgUi3JJneHob .cluster-label text{fill:#333;}#mermaid-svg-IyQpbgUi3JJneHob .cluster-label span{color:#333;}#mermaid-svg-IyQpbgUi3JJneHob .cluster-label span p{background-color:transparent;}#mermaid-svg-IyQpbgUi3JJneHob .label text,#mermaid-svg-IyQpbgUi3JJneHob span{fill:#333;color:#333;}#mermaid-svg-IyQpbgUi3JJneHob .node rect,#mermaid-svg-IyQpbgUi3JJneHob .node circle,#mermaid-svg-IyQpbgUi3JJneHob .node ellipse,#mermaid-svg-IyQpbgUi3JJneHob .node polygon,#mermaid-svg-IyQpbgUi3JJneHob .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-IyQpbgUi3JJneHob .rough-node .label text,#mermaid-svg-IyQpbgUi3JJneHob .node .label text,#mermaid-svg-IyQpbgUi3JJneHob .image-shape .label,#mermaid-svg-IyQpbgUi3JJneHob .icon-shape .label{text-anchor:middle;}#mermaid-svg-IyQpbgUi3JJneHob .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-IyQpbgUi3JJneHob .rough-node .label,#mermaid-svg-IyQpbgUi3JJneHob .node .label,#mermaid-svg-IyQpbgUi3JJneHob .image-shape .label,#mermaid-svg-IyQpbgUi3JJneHob .icon-shape .label{text-align:center;}#mermaid-svg-IyQpbgUi3JJneHob .node.clickable{cursor:pointer;}#mermaid-svg-IyQpbgUi3JJneHob .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-IyQpbgUi3JJneHob .arrowheadPath{fill:#333333;}#mermaid-svg-IyQpbgUi3JJneHob .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-IyQpbgUi3JJneHob .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-IyQpbgUi3JJneHob .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-IyQpbgUi3JJneHob .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-IyQpbgUi3JJneHob .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-IyQpbgUi3JJneHob .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-IyQpbgUi3JJneHob .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-IyQpbgUi3JJneHob .cluster text{fill:#333;}#mermaid-svg-IyQpbgUi3JJneHob .cluster span{color:#333;}#mermaid-svg-IyQpbgUi3JJneHob div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-IyQpbgUi3JJneHob .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-IyQpbgUi3JJneHob rect.text{fill:none;stroke-width:0;}#mermaid-svg-IyQpbgUi3JJneHob .icon-shape,#mermaid-svg-IyQpbgUi3JJneHob .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-IyQpbgUi3JJneHob .icon-shape p,#mermaid-svg-IyQpbgUi3JJneHob .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-IyQpbgUi3JJneHob .icon-shape .label rect,#mermaid-svg-IyQpbgUi3JJneHob .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-IyQpbgUi3JJneHob .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-IyQpbgUi3JJneHob .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-IyQpbgUi3JJneHob :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 方案 C: 混合方案 (冗余)
携带 Token
透传
Content Script
从 storage 读取 Token
sendMessage + Token
Background SW
透传请求
API Server
方案 B: Content Script 直接持有 (❌ 不可行)
❌ CORS 错误
Content Script
持有 Token
直接 fetch API
CORS 拦截
方案 A: Background 隔离 (推荐)
无 Token
PROXY_REQUEST
读取
恢复 Token
fetch + Token
Content Script
sendMessage
Background SW
chrome.storage.sync
API Server
持有 Token

5.1 方案对比矩阵

维度 方案 A (Background 隔离) 方案 B (CS 直接) 方案 C (混合)
CORS 可行性 ✅ 完全可行 ❌ 被浏览器拦截 ✅ 可行但冗余
Token 安全性 ✅ 隔离在 Background ❌ 暴露在页面上下文 ⚠️ 传输过程中暴露
SW 终止影响 ✅ Token 在 storage 持久化 ❌ Token 随 CS 销毁 ⚠️ 需额外处理
代码复杂度 ✅ 简洁 ❌ 不可行 ⚠️ 增加消息体积
性能开销 ✅ 正常 ❌ 无 ⚠️ 每次传 Token

5.2 安全性分析:Token 隔离到底防住了什么?

很多文章把"Token 放在 Background"包装成"安全最佳实践",但实际上:
#mermaid-svg-M9f0aVh0VCseuTT4{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-M9f0aVh0VCseuTT4 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-M9f0aVh0VCseuTT4 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-M9f0aVh0VCseuTT4 .error-icon{fill:#552222;}#mermaid-svg-M9f0aVh0VCseuTT4 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-M9f0aVh0VCseuTT4 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-M9f0aVh0VCseuTT4 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-M9f0aVh0VCseuTT4 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-M9f0aVh0VCseuTT4 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-M9f0aVh0VCseuTT4 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-M9f0aVh0VCseuTT4 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-M9f0aVh0VCseuTT4 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-M9f0aVh0VCseuTT4 .marker.cross{stroke:#333333;}#mermaid-svg-M9f0aVh0VCseuTT4 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-M9f0aVh0VCseuTT4 p{margin:0;}#mermaid-svg-M9f0aVh0VCseuTT4 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-M9f0aVh0VCseuTT4 .cluster-label text{fill:#333;}#mermaid-svg-M9f0aVh0VCseuTT4 .cluster-label span{color:#333;}#mermaid-svg-M9f0aVh0VCseuTT4 .cluster-label span p{background-color:transparent;}#mermaid-svg-M9f0aVh0VCseuTT4 .label text,#mermaid-svg-M9f0aVh0VCseuTT4 span{fill:#333;color:#333;}#mermaid-svg-M9f0aVh0VCseuTT4 .node rect,#mermaid-svg-M9f0aVh0VCseuTT4 .node circle,#mermaid-svg-M9f0aVh0VCseuTT4 .node ellipse,#mermaid-svg-M9f0aVh0VCseuTT4 .node polygon,#mermaid-svg-M9f0aVh0VCseuTT4 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-M9f0aVh0VCseuTT4 .rough-node .label text,#mermaid-svg-M9f0aVh0VCseuTT4 .node .label text,#mermaid-svg-M9f0aVh0VCseuTT4 .image-shape .label,#mermaid-svg-M9f0aVh0VCseuTT4 .icon-shape .label{text-anchor:middle;}#mermaid-svg-M9f0aVh0VCseuTT4 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-M9f0aVh0VCseuTT4 .rough-node .label,#mermaid-svg-M9f0aVh0VCseuTT4 .node .label,#mermaid-svg-M9f0aVh0VCseuTT4 .image-shape .label,#mermaid-svg-M9f0aVh0VCseuTT4 .icon-shape .label{text-align:center;}#mermaid-svg-M9f0aVh0VCseuTT4 .node.clickable{cursor:pointer;}#mermaid-svg-M9f0aVh0VCseuTT4 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-M9f0aVh0VCseuTT4 .arrowheadPath{fill:#333333;}#mermaid-svg-M9f0aVh0VCseuTT4 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-M9f0aVh0VCseuTT4 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-M9f0aVh0VCseuTT4 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-M9f0aVh0VCseuTT4 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-M9f0aVh0VCseuTT4 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-M9f0aVh0VCseuTT4 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-M9f0aVh0VCseuTT4 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-M9f0aVh0VCseuTT4 .cluster text{fill:#333;}#mermaid-svg-M9f0aVh0VCseuTT4 .cluster span{color:#333;}#mermaid-svg-M9f0aVh0VCseuTT4 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-M9f0aVh0VCseuTT4 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-M9f0aVh0VCseuTT4 rect.text{fill:none;stroke-width:0;}#mermaid-svg-M9f0aVh0VCseuTT4 .icon-shape,#mermaid-svg-M9f0aVh0VCseuTT4 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-M9f0aVh0VCseuTT4 .icon-shape p,#mermaid-svg-M9f0aVh0VCseuTT4 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-M9f0aVh0VCseuTT4 .icon-shape .label rect,#mermaid-svg-M9f0aVh0VCseuTT4 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-M9f0aVh0VCseuTT4 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-M9f0aVh0VCseuTT4 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-M9f0aVh0VCseuTT4 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Token 隔离的防护效果
威胁模型分析
WhatsApp 页面恶意 JS

窃取 Token
其他扩展上下文读取

chrome.storage
Content Script 被 XSS

注入恶意代码
❌ 无需担心

MV3 Isolated World

页面 JS 无法访问 CS 变量
❌ 无效

chrome.storage 对扩展内

所有上下文共享
✅ 有效

CS 被注入时

Background Token 不泄露

威胁场景 Token 隔离是否有效 原因
WhatsApp 页面 JS 窃取 无需担心 MV3 的 Isolated World 机制,页面 JS 根本访问不到 Content Script 的变量和闭包
其他扩展上下文读取 storage 无效 chrome.storage 对扩展内所有上下文(popup/content/background)都是共享可读的
Content Script 自身被 XSS 有效 如果 Content Script 代码有漏洞被注入,Background 隔离能阻止 Token 泄露

结论 :Token 隔离的安全收益是有限的 ,主要防御的是"Content Script 自身代码漏洞"这种相对罕见的场景。真正驱动方案 A 的不是安全,而是 CORS 约束


六、保活策略:如何让 Service Worker 活过 30 秒?

如果你的插件需要维持长连接(如 WebSocket)或定期任务,必须实现保活机制。
#mermaid-svg-JjI6NqtmuBt39e9N{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-JjI6NqtmuBt39e9N .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-JjI6NqtmuBt39e9N .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-JjI6NqtmuBt39e9N .error-icon{fill:#552222;}#mermaid-svg-JjI6NqtmuBt39e9N .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-JjI6NqtmuBt39e9N .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-JjI6NqtmuBt39e9N .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-JjI6NqtmuBt39e9N .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-JjI6NqtmuBt39e9N .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-JjI6NqtmuBt39e9N .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-JjI6NqtmuBt39e9N .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-JjI6NqtmuBt39e9N .marker{fill:#333333;stroke:#333333;}#mermaid-svg-JjI6NqtmuBt39e9N .marker.cross{stroke:#333333;}#mermaid-svg-JjI6NqtmuBt39e9N svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-JjI6NqtmuBt39e9N p{margin:0;}#mermaid-svg-JjI6NqtmuBt39e9N .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-JjI6NqtmuBt39e9N .cluster-label text{fill:#333;}#mermaid-svg-JjI6NqtmuBt39e9N .cluster-label span{color:#333;}#mermaid-svg-JjI6NqtmuBt39e9N .cluster-label span p{background-color:transparent;}#mermaid-svg-JjI6NqtmuBt39e9N .label text,#mermaid-svg-JjI6NqtmuBt39e9N span{fill:#333;color:#333;}#mermaid-svg-JjI6NqtmuBt39e9N .node rect,#mermaid-svg-JjI6NqtmuBt39e9N .node circle,#mermaid-svg-JjI6NqtmuBt39e9N .node ellipse,#mermaid-svg-JjI6NqtmuBt39e9N .node polygon,#mermaid-svg-JjI6NqtmuBt39e9N .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-JjI6NqtmuBt39e9N .rough-node .label text,#mermaid-svg-JjI6NqtmuBt39e9N .node .label text,#mermaid-svg-JjI6NqtmuBt39e9N .image-shape .label,#mermaid-svg-JjI6NqtmuBt39e9N .icon-shape .label{text-anchor:middle;}#mermaid-svg-JjI6NqtmuBt39e9N .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-JjI6NqtmuBt39e9N .rough-node .label,#mermaid-svg-JjI6NqtmuBt39e9N .node .label,#mermaid-svg-JjI6NqtmuBt39e9N .image-shape .label,#mermaid-svg-JjI6NqtmuBt39e9N .icon-shape .label{text-align:center;}#mermaid-svg-JjI6NqtmuBt39e9N .node.clickable{cursor:pointer;}#mermaid-svg-JjI6NqtmuBt39e9N .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-JjI6NqtmuBt39e9N .arrowheadPath{fill:#333333;}#mermaid-svg-JjI6NqtmuBt39e9N .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-JjI6NqtmuBt39e9N .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-JjI6NqtmuBt39e9N .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-JjI6NqtmuBt39e9N .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-JjI6NqtmuBt39e9N .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-JjI6NqtmuBt39e9N .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-JjI6NqtmuBt39e9N .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-JjI6NqtmuBt39e9N .cluster text{fill:#333;}#mermaid-svg-JjI6NqtmuBt39e9N .cluster span{color:#333;}#mermaid-svg-JjI6NqtmuBt39e9N div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-JjI6NqtmuBt39e9N .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-JjI6NqtmuBt39e9N rect.text{fill:none;stroke-width:0;}#mermaid-svg-JjI6NqtmuBt39e9N .icon-shape,#mermaid-svg-JjI6NqtmuBt39e9N .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-JjI6NqtmuBt39e9N .icon-shape p,#mermaid-svg-JjI6NqtmuBt39e9N .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-JjI6NqtmuBt39e9N .icon-shape .label rect,#mermaid-svg-JjI6NqtmuBt39e9N .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-JjI6NqtmuBt39e9N .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-JjI6NqtmuBt39e9N .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-JjI6NqtmuBt39e9N :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 保活策略对比
策略1: chrome.alarms API

⏰ 每 20-25秒触发

✅ 最推荐,专为 MV3 设计
策略2: 长连接 Port

🔌 Content Script 每 10秒 sendMessage

⚠️ 仅 CS 存活时有效
策略3: WebSocket 心跳

🌐 Chrome 116+ WS 活动可重置计时器

⚠️ 需服务端配合
策略4: Offscreen Document

📄 Chrome 109+ 创建隐藏页面保活

⚠️ 适合复杂场景
策略5: 定期 API 调用

🔄 每 20秒调用 chrome API

⚠️ 简单但不够优雅

6.1 推荐方案:chrome.alarms 保活

javascript 复制代码
// manifest.json
{
  "permissions": ["alarms"],
  "background": {
    "service_worker": "background.js"
  }
}

// background.js
chrome.alarms.onAlarm.addListener((alarm) => {
  if (alarm.name === 'keepalive') {
    console.log('💓 心跳保活');
    // 可以在这里做状态检查、Token 刷新等
  }
});

// 启动时注册保活闹钟
chrome.runtime.onStartup.addListener(() => {
  chrome.alarms.create('keepalive', {
    periodInMinutes: 1 / 3  // 每 20 秒
  });
});

chrome.runtime.onInstalled.addListener(() => {
  chrome.alarms.create('keepalive', {
    periodInMinutes: 1 / 3
  });
});

为什么推荐 20 秒而不是 30 秒?

  • 30 秒是 Chrome 的终止阈值,如果闹钟正好在 30 秒边界触发,可能因为调度延迟导致 race condition
  • 业界实践(如 Claude in Chrome、OpenClaw 等)都采用 20 秒 作为安全余量

6.2 状态恢复:冷启动时的初始化流程

后端 API chrome.storage Service Worker Chrome 浏览器 后端 API chrome.storage Service Worker Chrome 浏览器 #mermaid-svg-VZaOdZmCCaGWGfbG{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-VZaOdZmCCaGWGfbG .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-VZaOdZmCCaGWGfbG .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-VZaOdZmCCaGWGfbG .error-icon{fill:#552222;}#mermaid-svg-VZaOdZmCCaGWGfbG .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-VZaOdZmCCaGWGfbG .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-VZaOdZmCCaGWGfbG .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-VZaOdZmCCaGWGfbG .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-VZaOdZmCCaGWGfbG .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-VZaOdZmCCaGWGfbG .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-VZaOdZmCCaGWGfbG .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-VZaOdZmCCaGWGfbG .marker{fill:#333333;stroke:#333333;}#mermaid-svg-VZaOdZmCCaGWGfbG .marker.cross{stroke:#333333;}#mermaid-svg-VZaOdZmCCaGWGfbG svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-VZaOdZmCCaGWGfbG p{margin:0;}#mermaid-svg-VZaOdZmCCaGWGfbG .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-VZaOdZmCCaGWGfbG text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-VZaOdZmCCaGWGfbG .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-VZaOdZmCCaGWGfbG .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-VZaOdZmCCaGWGfbG .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-VZaOdZmCCaGWGfbG .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-VZaOdZmCCaGWGfbG #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-VZaOdZmCCaGWGfbG .sequenceNumber{fill:white;}#mermaid-svg-VZaOdZmCCaGWGfbG #sequencenumber{fill:#333;}#mermaid-svg-VZaOdZmCCaGWGfbG #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-VZaOdZmCCaGWGfbG .messageText{fill:#333;stroke:none;}#mermaid-svg-VZaOdZmCCaGWGfbG .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-VZaOdZmCCaGWGfbG .labelText,#mermaid-svg-VZaOdZmCCaGWGfbG .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-VZaOdZmCCaGWGfbG .loopText,#mermaid-svg-VZaOdZmCCaGWGfbG .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-VZaOdZmCCaGWGfbG .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-VZaOdZmCCaGWGfbG .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-VZaOdZmCCaGWGfbG .noteText,#mermaid-svg-VZaOdZmCCaGWGfbG .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-VZaOdZmCCaGWGfbG .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-VZaOdZmCCaGWGfbG .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-VZaOdZmCCaGWGfbG .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-VZaOdZmCCaGWGfbG .actorPopupMenu{position:absolute;}#mermaid-svg-VZaOdZmCCaGWGfbG .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-VZaOdZmCCaGWGfbG .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-VZaOdZmCCaGWGfbG .actor-man circle,#mermaid-svg-VZaOdZmCCaGWGfbG line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-VZaOdZmCCaGWGfbG :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Service Worker 冷启动流程 token = 'abc123' config = {...} par 并行初始化 事件监听注册 ⏱️ 30秒倒计时再次开始... 新事件触发 执行顶层脚本 chrome.storage.sync.get('token', 'config') 返回持久化状态 恢复内存变量 chrome.runtime.onMessage.addListener(...) chrome.alarms.onAlarm.addListener(...) chrome.tabs.onRemoved.addListener(...) 使用恢复的 Token 发起请求 响应数据 处理完成

关键代码模式

javascript 复制代码
// background.js - 顶层执行,每次 SW 启动都会运行
const STATE = {
  token: null,
  config: null,
  isInitialized: false
};

async function init() {
  if (STATE.isInitialized) return;

  // 从 storage 恢复状态
  const data = await chrome.storage.sync.get(['token', 'config']);
  STATE.token = data.token;
  STATE.config = data.config;
  STATE.isInitialized = true;

  console.log('🔄 Service Worker 冷启动,状态已恢复');
}

// 所有事件监听器都必须在顶层注册
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  init().then(() => {
    // 处理消息
    handleRequest(request, sender, sendResponse);
  });
  return true; // 保持消息通道开放
});

// 保活闹钟
chrome.alarms.onAlarm.addListener((alarm) => {
  if (alarm.name === 'keepalive') {
    init().then(() => {
      console.log('💓 心跳');
    });
  }
});

// 启动时注册
chrome.runtime.onStartup.addListener(() => {
  chrome.alarms.create('keepalive', { periodInMinutes: 1/3 });
});

chrome.runtime.onInstalled.addListener(() => {
  chrome.alarms.create('keepalive', { periodInMinutes: 1/3 });
});

七、架构决策树:如何选择你的方案?

#mermaid-svg-t87wMuMZznPtbGZ8{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-t87wMuMZznPtbGZ8 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-t87wMuMZznPtbGZ8 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-t87wMuMZznPtbGZ8 .error-icon{fill:#552222;}#mermaid-svg-t87wMuMZznPtbGZ8 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-t87wMuMZznPtbGZ8 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-t87wMuMZznPtbGZ8 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-t87wMuMZznPtbGZ8 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-t87wMuMZznPtbGZ8 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-t87wMuMZznPtbGZ8 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-t87wMuMZznPtbGZ8 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-t87wMuMZznPtbGZ8 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-t87wMuMZznPtbGZ8 .marker.cross{stroke:#333333;}#mermaid-svg-t87wMuMZznPtbGZ8 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-t87wMuMZznPtbGZ8 p{margin:0;}#mermaid-svg-t87wMuMZznPtbGZ8 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-t87wMuMZznPtbGZ8 .cluster-label text{fill:#333;}#mermaid-svg-t87wMuMZznPtbGZ8 .cluster-label span{color:#333;}#mermaid-svg-t87wMuMZznPtbGZ8 .cluster-label span p{background-color:transparent;}#mermaid-svg-t87wMuMZznPtbGZ8 .label text,#mermaid-svg-t87wMuMZznPtbGZ8 span{fill:#333;color:#333;}#mermaid-svg-t87wMuMZznPtbGZ8 .node rect,#mermaid-svg-t87wMuMZznPtbGZ8 .node circle,#mermaid-svg-t87wMuMZznPtbGZ8 .node ellipse,#mermaid-svg-t87wMuMZznPtbGZ8 .node polygon,#mermaid-svg-t87wMuMZznPtbGZ8 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-t87wMuMZznPtbGZ8 .rough-node .label text,#mermaid-svg-t87wMuMZznPtbGZ8 .node .label text,#mermaid-svg-t87wMuMZznPtbGZ8 .image-shape .label,#mermaid-svg-t87wMuMZznPtbGZ8 .icon-shape .label{text-anchor:middle;}#mermaid-svg-t87wMuMZznPtbGZ8 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-t87wMuMZznPtbGZ8 .rough-node .label,#mermaid-svg-t87wMuMZznPtbGZ8 .node .label,#mermaid-svg-t87wMuMZznPtbGZ8 .image-shape .label,#mermaid-svg-t87wMuMZznPtbGZ8 .icon-shape .label{text-align:center;}#mermaid-svg-t87wMuMZznPtbGZ8 .node.clickable{cursor:pointer;}#mermaid-svg-t87wMuMZznPtbGZ8 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-t87wMuMZznPtbGZ8 .arrowheadPath{fill:#333333;}#mermaid-svg-t87wMuMZznPtbGZ8 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-t87wMuMZznPtbGZ8 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-t87wMuMZznPtbGZ8 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-t87wMuMZznPtbGZ8 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-t87wMuMZznPtbGZ8 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-t87wMuMZznPtbGZ8 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-t87wMuMZznPtbGZ8 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-t87wMuMZznPtbGZ8 .cluster text{fill:#333;}#mermaid-svg-t87wMuMZznPtbGZ8 .cluster span{color:#333;}#mermaid-svg-t87wMuMZznPtbGZ8 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-t87wMuMZznPtbGZ8 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-t87wMuMZznPtbGZ8 rect.text{fill:none;stroke-width:0;}#mermaid-svg-t87wMuMZznPtbGZ8 .icon-shape,#mermaid-svg-t87wMuMZznPtbGZ8 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-t87wMuMZznPtbGZ8 .icon-shape p,#mermaid-svg-t87wMuMZznPtbGZ8 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-t87wMuMZznPtbGZ8 .icon-shape .label rect,#mermaid-svg-t87wMuMZznPtbGZ8 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-t87wMuMZznPtbGZ8 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-t87wMuMZznPtbGZ8 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-t87wMuMZznPtbGZ8 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是

是(极罕见)
否(99%场景)




简单场景
复杂场景
WebSocket
开始设计 Token 管理方案
是否需要调用跨域 API?
API 是否支持宿主页面的 CORS?
简单场景

Token 放 storage 即可
安全性要求是否极高?
方案 A: Background 隔离

✅ 唯一可行路径
方案 B: Content Script 直接

⚠️ 不推荐,Token 暴露
是否需要长连接/定期任务?
使用哪种保活机制?
完成

按需唤醒模式
chrome.alarms

每20秒心跳
Offscreen Document

Chrome 109+
WS 心跳 + 重连机制
完成

保活模式


八、实战 Checklist:上线前的自检清单

8.1 Service Worker 生命周期检查

  • 所有事件监听器是否在 background.js 顶层注册?(不能在异步函数内部注册)
  • 是否有 chrome.runtime.onStartupchrome.runtime.onInstalled 处理初始化?
  • 是否使用 chrome.alarms 替代了 setInterval/setTimeout
  • 保活间隔是否 ≤ 20 秒
  • 内存状态是否在 chrome.storage 中有持久化备份?
  • 冷启动时是否有状态恢复逻辑?

8.2 Token 管理检查

  • Content Script 是否不持有 Token?
  • API 请求是否全部通过 Background SW 代理?
  • Token 是否存储在 chrome.storage.syncchrome.storage.session
  • 是否有 Token 过期自动刷新机制?
  • 是否有 Token 泄露的降级处理(如强制重新登录)?

8.3 CORS 与权限检查

  • manifest.json 是否声明了所有需要访问的 API 域名?
  • host_permissions 是否包含了后端 API 的完整域名?
  • 是否测试过 Content Script 直接 fetch 会被 CORS 拦截?

九、总结:架构设计的核心认知

#mermaid-svg-YHESGRTXbyfn3aWZ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-YHESGRTXbyfn3aWZ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-YHESGRTXbyfn3aWZ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-YHESGRTXbyfn3aWZ .error-icon{fill:#552222;}#mermaid-svg-YHESGRTXbyfn3aWZ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-YHESGRTXbyfn3aWZ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-YHESGRTXbyfn3aWZ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-YHESGRTXbyfn3aWZ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-YHESGRTXbyfn3aWZ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-YHESGRTXbyfn3aWZ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-YHESGRTXbyfn3aWZ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-YHESGRTXbyfn3aWZ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-YHESGRTXbyfn3aWZ .marker.cross{stroke:#333333;}#mermaid-svg-YHESGRTXbyfn3aWZ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-YHESGRTXbyfn3aWZ p{margin:0;}#mermaid-svg-YHESGRTXbyfn3aWZ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-YHESGRTXbyfn3aWZ .cluster-label text{fill:#333;}#mermaid-svg-YHESGRTXbyfn3aWZ .cluster-label span{color:#333;}#mermaid-svg-YHESGRTXbyfn3aWZ .cluster-label span p{background-color:transparent;}#mermaid-svg-YHESGRTXbyfn3aWZ .label text,#mermaid-svg-YHESGRTXbyfn3aWZ span{fill:#333;color:#333;}#mermaid-svg-YHESGRTXbyfn3aWZ .node rect,#mermaid-svg-YHESGRTXbyfn3aWZ .node circle,#mermaid-svg-YHESGRTXbyfn3aWZ .node ellipse,#mermaid-svg-YHESGRTXbyfn3aWZ .node polygon,#mermaid-svg-YHESGRTXbyfn3aWZ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-YHESGRTXbyfn3aWZ .rough-node .label text,#mermaid-svg-YHESGRTXbyfn3aWZ .node .label text,#mermaid-svg-YHESGRTXbyfn3aWZ .image-shape .label,#mermaid-svg-YHESGRTXbyfn3aWZ .icon-shape .label{text-anchor:middle;}#mermaid-svg-YHESGRTXbyfn3aWZ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-YHESGRTXbyfn3aWZ .rough-node .label,#mermaid-svg-YHESGRTXbyfn3aWZ .node .label,#mermaid-svg-YHESGRTXbyfn3aWZ .image-shape .label,#mermaid-svg-YHESGRTXbyfn3aWZ .icon-shape .label{text-align:center;}#mermaid-svg-YHESGRTXbyfn3aWZ .node.clickable{cursor:pointer;}#mermaid-svg-YHESGRTXbyfn3aWZ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-YHESGRTXbyfn3aWZ .arrowheadPath{fill:#333333;}#mermaid-svg-YHESGRTXbyfn3aWZ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-YHESGRTXbyfn3aWZ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-YHESGRTXbyfn3aWZ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-YHESGRTXbyfn3aWZ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-YHESGRTXbyfn3aWZ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-YHESGRTXbyfn3aWZ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-YHESGRTXbyfn3aWZ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-YHESGRTXbyfn3aWZ .cluster text{fill:#333;}#mermaid-svg-YHESGRTXbyfn3aWZ .cluster span{color:#333;}#mermaid-svg-YHESGRTXbyfn3aWZ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-YHESGRTXbyfn3aWZ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-YHESGRTXbyfn3aWZ rect.text{fill:none;stroke-width:0;}#mermaid-svg-YHESGRTXbyfn3aWZ .icon-shape,#mermaid-svg-YHESGRTXbyfn3aWZ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-YHESGRTXbyfn3aWZ .icon-shape p,#mermaid-svg-YHESGRTXbyfn3aWZ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-YHESGRTXbyfn3aWZ .icon-shape .label rect,#mermaid-svg-YHESGRTXbyfn3aWZ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-YHESGRTXbyfn3aWZ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-YHESGRTXbyfn3aWZ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-YHESGRTXbyfn3aWZ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} MV3 架构设计的核心认知
CORS 约束 > 安全考量

方案 A 是'唯一可行'而非'最佳实践'
SW 是 Serverless 函数

不是常驻进程,每次事件都是冷启动
Storage 是数据库

内存变量只是缓存,随时会丢失
30秒是设计特性

不是 Bug,架构必须围绕它设计

  1. 方案 A(Background 隔离)是 MV3 架构约束下的"唯一可行路径"------不是因为它"更安全",而是因为 CORS 强制要求所有 API 请求走 Background 代理。既然请求都要过 Background,Token 自然留在 Background 管理最简洁。安全是额外收益,不是主要驱动力。

  2. Service Worker 是 Serverless 函数,不是常驻进程 ------每次事件触发都是一次"冷启动",内存状态从零开始。你必须把 chrome.storage 当作数据库,内存变量只是临时缓存。

  3. 30 秒生命周期是设计特性,不是 Bug------Google 故意这样设计来节省内存和电量。你的架构必须围绕它设计,而不是试图"绕过"它。

  4. 保活不是"让它一直活着",而是"在需要时确保它活着"------过度保活会被 Chrome 商店审核拒绝,合理的设计是:关键操作时保活,空闲时允许终止。


十、参考与延伸阅读


作者注:本文基于实际项目踩坑经验 + 多个开源项目的 issue 分析整理而成。MV3 的架构设计确实比 MV2 复杂很多,但一旦理解了"事件驱动 + 状态持久化 + CORS 代理"这三根支柱,就能避免 90% 的坑。


本文发表于 2026 年 6 月,基于 Chrome 125+ 版本的 MV3 实现。

相关推荐
AsiaLYF1 小时前
Kotlin MutableSharedFlow: emit vs tryEmit 详解
开发语言·前端·kotlin
小李云雾1 小时前
Pinia:Vue3 全局状态管理从入门到精通
前端·javascript·vue.js
Upsy-Daisy1 小时前
Hermes Agent 学习笔记 03:CLI 与 TUI 使用体验,让 Agent 真正进入终端工作流
服务器·前端·数据库
KaMeidebaby2 小时前
卡梅德生物技术快报|噬菌体筛选:技术实操:宽谱大肠杆菌噬菌体筛选全流程与性能验证方案
前端·人工智能·算法·数据挖掘·数据分析
风吹夏回2 小时前
Vue3 + Element Plus 完整使用指南
前端·javascript·vue.js·element
影寂ldy2 小时前
C# 泛型方法
java·前端·c#
依托偶尔宁2 小时前
element-plus:el-table设置展开图标所在列的位置
前端·elementui
小小龙学IT2 小时前
Go语言后端开发实战指南:构建高性能云原生服务
前端·云原生·golang
sbjdhjd10 小时前
Redis 主从复制、哨兵高可用与 Cluster 集群部署实验手册
运维·前端·redis·云原生·开源·bootstrap·html