想象你住在一栋老式公寓里。楼下的管家老王,是你和外界所有信件、包裹往来的唯一通道。你上班不在家时,他会把快递签收好放进储物间;小区断网时,他按经验先用囤好的日用品应对;你睡觉时,他默默把明天的补给申请发出去;遇到急事,他会主动给你打个电话汇报。 PWA(渐进式 Web 应用)里的 Service Worker,就是浏览器给你的网页请来的这样一位“数字管家”。他不是网页本身,也不是服务器,而是住在浏览器与网络之间的一个独立守门人。网页完成注册,他就开始上岗:安装、激活、拦截请求、存取缓存、推送消息、后台同步。他把网页从一个“被动等待浏览的文档”,变成了一个“主动服务用户的 App”。 理解了这位管家是怎么入职、怎么值班、怎么调度、怎么应对突发状况,你就掌握了 PWA 的全部精髓。

岗位关联

对 AI 从业者而言,Service Worker 是理解“边缘缓存”“端侧智能”的天然样板。掌握它,等于掌握了一套在任何前端工程里做离线优先、性能优化和推送触达的硬核技能。

目录
  1. 可视化演示
  2. 生命周期:管家的入职与退休
  3. 请求拦截:管家的收发室
  4. 缓存仓库:管家的储物柜
  5. 缓存策略:先查库存还是先问商家
  6. 消息推送:管家主动打电话
  7. 后台同步:断网也不误事
  8. 调试技巧:给管家体检
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示SW生命周期、缓存策略、推送通知与调试更新机制。

本节配有交互动效(进入视口后自动加载,约 3KB)。

Service Worker生命周期 install事件预缓存核心资源进入installed状态 activate事件清理旧缓存后进入activated激活态 SW = install → activate → fetch 三阶段生命周期管理SW注册与激活流程

生命周期:管家的入职与退休

大楼不是随便一个人就能当管家的。老王入职那天,先在管理处登记培训,通过考核后才能正式上岗。此后他每天在收发室值班,直到新管家来接替。Service Worker 的生命周期完全复刻了这个过程:下载脚本、安装准备、激活接管、服务、更新、退休。

通过 navigator.serviceWorker.register('/sw.js') 注册后,浏览器在后台下载脚本。脚本解析成功,SW 进入 installing 状态并触发 install 事件,这是预缓存首页骨架资源的最佳时机:把固定不变的 HTML、CSS、Logo 提前放进缓存仓库。安装完成后,如果没有其他旧 SW 控制页面,它会直接进入 activating 并触发 activate 事件;activate 里通常做旧缓存清理,给新版本腾地方。

更新流程略微讲究。浏览器每次导航回到页面,都会重新对比 sw.js 的内容——一旦发现字节有差异,新 SW 进入 waiting 状态,安静等待旧 SW 控制的页面全部关闭后才接管。如果不想等,可以调用 self.skipWaiting() 让新管家立刻上岗,再用 clients.claim() 让已经打开的页面也接受新管家管理。

就像老王退休前,必须等所有住户家的事都安顿好(页面关闭)才能交接。如果整栋楼同意(skipWaiting),老王也可以提前退居幕后,把对讲机直接交给新管家。

状态∈{installing, activated, waiting, redundant}\displaystyle \text{状态} \in \{ \text{installing},\ \text{activated},\ \text{waiting},\ \text{redundant} \}
这条公式列出了 SW 生命周期中四种核心状态。前两种管“上岗”,后两种管“交接与退休”。
  1. installing 与 activated一对关键的前后状态,install 负责准备资源,activate 负责接管控制权。
    installing→安装成功activated\displaystyle \text{installing} \xrightarrow{\text{安装成功}} \text{activated}
  2. waiting 状态新管家已就绪,但旧管家还在值班,需要等待旧 SW 控制的页面全部关闭。
    新 SW 进入 waiting,等待 旧 SW 控制的页面关闭\displaystyle \text{新 SW 进入 waiting,等待 } \text{旧 SW 控制的页面关闭}
  3. redundant 状态旧 SW 被取代后进入冗余状态,从此不再处理任何请求。
    旧 SW→被取代redundant\displaystyle \text{旧 SW} \xrightarrow{\text{被取代}} \text{redundant}

请求拦截:管家的收发室

老王最重要的岗位是收发室。所有住户的信件、包裹、外卖都先经他手,再由他决定是代收、转交、还是原路退回。SW 的 fetch 事件就是这样一个收发室:页面发出的每一个网络请求,SW 都能听到并处理。

在 SW 脚本里监听 fetch 事件:

event.respondWith() 是管家的决定权——告诉浏览器“这个请求我来处理”。可以返回缓存的副本,也可以转发给网络,甚至可以拼一个兜底页面。默认情况下,页面首次加载时不受 SW 控制,需要 clients.claim() 之后,已经打开的页面才会把请求交给管家。

SW 与页面之间还通过 message 事件双向通信。页面调用 navigator.serviceWorker.controller.postMessage(data) 发消息给管家;管家在 message 事件里接收,用 event.source.postMessage(result) 回信。这就好比住户按门口对讲机问老王:“今天的牛奶到了吗?”老王在收发室回一句:“到了,我放储物柜了。”

收发室的价值在于:页面不需要关心响应到底来自储物柜还是云端。管家把一切都包办好了,页面只负责收到一个结果。

响应={cache.match(r),若缓存命中fetch(r),否则转发网络\displaystyle \text{响应} = \begin{cases} \text{cache.match}(r), & \text{若缓存命中} \\ \text{fetch}(r), & \text{否则转发网络} \end{cases}
这是 fetch 拦截中最基础的分支逻辑:先翻缓存,翻不到就请求网络。
  1. 请求对象 rSW 通过 fetch 事件收到页面发出的请求对象。
    事件参数 r=Request(url, method)\displaystyle \text{事件参数}\ r = \text{Request}(\text{url},\ \text{method})
  2. 分支判断先用 cache.match 在仓库里找,找到就直接返回;找不到再转发给网络。
    命中⇒返回缓存;未命中⇒fetch(r)\displaystyle \text{命中} \Rightarrow \text{返回缓存};\quad \text{未命中} \Rightarrow \text{fetch}(r)
  3. respondWith 包装无论走哪条分支,最终都要用 respondWith 把响应“承诺”给页面。
    event.respondWith(Promise<Response>)\displaystyle \text{event.respondWith}(\text{Promise<Response>})
js4 行
self.addEventListener('fetch', event => {  event.respondWith(caches.match(event.request)    .then(res => res || fetch(event.request)));});

缓存仓库:管家的储物柜

老王有一个整整齐齐的储物柜,按“快递柜”的格式管理:打开柜门、放进去、取出来、清空。Cache Storage 就是给 SW 用的快递柜。

打开仓库用 caches.open('my-cache-v1'),它返回一个 Promise,resolve 出 Cache 对象。往里放东西用 cache.put(request, response),手动控制颗粒度;cache.addAll(['/index.html', '/style.css']) 则批量抓取并存储,适合 install 阶段预缓存。取东西用 cache.match(request),返回命中的 Response;caches.delete(name) 用于删除整个仓库,在版本升级时清理旧货。

需要注意:HTTP 响应是“一次性流”,读完之后不能重读。所以缓存时一般先 response.clone() 一份——一份给页面用,一份存进仓库,否则会报错。另外,缓存仓库有配额限制,超出后浏览器会整体清空仓库,所以重要资源要主动管理版本。

储物柜里的东西不是越多越好。放得太多会占满配额,放得太旧会拿到过期货物。管家需要定期整理,把不用的旧版本清掉,把重要的新货摆到最顺手的位置。

命中率=NhitNtotal=缓存直接返回的请求数总请求数\displaystyle \text{命中率} = \frac{N_{\text{hit}}}{N_{\text{total}}} = \frac{\text{缓存直接返回的请求数}}{\text{总请求数}}
命中率是衡量管家储物柜价值的关键指标。命中率越高,网络依赖越低,页面加载越快。
  1. 分子 N_hit命中的请求数,即管家直接从储物柜交货的次数。
    Nhit=cache.match 成功返回的次数\displaystyle N_{\text{hit}} = \text{cache.match 成功返回的次数}
  2. 分母 N_total总请求数,即管家经手的全部请求数量。
    Ntotal=SW 拦截到的所有 fetch 请求\displaystyle N_{\text{total}} = \text{SW 拦截到的所有 fetch 请求}
  3. 比值含义命中率越接近 1,说明多数请求绕开了网络,离线能力越强。
    命中率∈[0,1], 越接近 1 越好\displaystyle \text{命中率} \in [0, 1],\ \text{越接近 1 越好}

缓存策略:先查库存还是先问商家

对同一个请求,管家可以选择不同的处理风格:是直奔储物柜拿库存,还是先打电话问商家要最新款?这就是缓存策略。

Cache First(缓存优先):先查缓存,命中直接返回;未命中才请求网络并回填缓存。适合版本不变的静态资源——Logo、JS 文件、字体,速度快得惊人,代价是网络更新后缓存可能滞后。

Network First(网络优先):先请求网络,成功就返回并顺带更新缓存;失败(断网)再退回缓存。适合首页 HTML、文章接口这类“要新鲜但也能接受兜底”的资源。

Stale-While-Revalidate(SWR):立即返回缓存,同时在后台发网络请求,拿到新响应后替换缓存,供下次使用。体验最顺滑,只是第一次可能看到旧内容。

聪明的管家不会只用一套逻辑。首页 HTML 用 Network First 保证新鲜,静态图片用 Cache First 保证速度,API 数据用 SWR 保证均衡。策略是手段,用户体验才是目的。

Tcache-first=p⋅Tcache+(1−p)(Tmiss+Tnetwork+Tfill)\displaystyle T_{\text{cache-first}} = p \cdot T_{\text{cache}} + (1-p)(T_{\text{miss}} + T_{\text{network}} + T_{\text{fill}})
这条公式拆解了 Cache First 策略的平均响应时间:命中时只花缓存读取时间,未命中时要额外付出网络请求和回填时间。
  1. p 命中率请求命中缓存的经验概率,由资源更新频率决定。
    p=NhitNtotal\displaystyle p = \frac{N_{\text{hit}}}{N_{\text{total}}}
  2. T_cache 与 T_miss命中时只需缓存读取时间;未命中时先付出一次查找失败的开销。
    Tcache-first=p⋅Tcache+(1−p)⋅Tmiss\displaystyle T_{\text{cache-first}} = p \cdot T_{\text{cache}} + (1-p) \cdot T_{\text{miss}}
  3. 网络与回填未命中还要加上网络请求时间 T_network,以及把新响应写回缓存的时间 T_fill。
    Tmiss+Tnetwork+Tfill\displaystyle T_{\text{miss}} + T_{\text{network}} + T_{\text{fill}}

消息推送:管家主动打电话

老王不只是守门,他还会主动给你打电话:“你订的牛奶到了”“物业要停水了”。PWA 的 Push API 就是管家主动来电的能力。

Push 的关键是订阅关系。页面里调用 registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey }),浏览器会生成一个订阅对象并交给服务器。服务器以后用 Web Push Protocol 往订阅里的 endpoint 发送加密消息,消息到达后由 SW 的 push 事件接收,再调用 self.registration.showNotification(title, { body }) 弹出系统通知。用户点击通知时,notificationclick 事件负责聚焦或打开对应页面。

整个过程有三方参与:应用服务器(寄信人)、浏览器推送服务(邮局)、SW(收信管家)。用户必须显式授权通知权限——这相当于住户同意管家记录自己的联系方式。

有了这串三元组,应用服务器就可以在用户完全关闭网页的情况下,依然把消息递到用户眼前。这就是 PWA 能像原生 App 一样“唤醒”用户的技术底座。

订阅=(endpoint, p256dh 公钥, auth 密钥)\displaystyle \text{订阅} = (\text{endpoint},\ \text{p256dh 公钥},\ \text{auth 密钥})
订阅对象像一个“专属邮箱地址”:服务器把信投到这个地址,浏览器推送服务负责转交,SW 查收后弹通知。
  1. endpoint浏览器推送服务分配的唯一地址,等价于管家在邮局登记的信箱号。
    endpoint=https://fcm.googleapis.com/...\displaystyle \text{endpoint} = \text{https://fcm.googleapis.com/...}
  2. p256dh 公钥用于内容加密的公钥,保证推送内容路上不被偷看。
    p256dh=ECDH 公钥(可公开分发)\displaystyle \text{p256dh} = \text{ECDH 公钥(可公开分发)}
  3. auth 密钥鉴权密钥,证明服务器确实订阅过,防止陌生人乱投递。
    auth=共享密钥,服务器与浏览器各持一份\displaystyle \text{auth} = \text{共享密钥,服务器与浏览器各持一份}

后台同步:断网也不误事

老王有个好习惯:断网时他会把待办事项写在小本子上,等网络恢复后逐一补办。Background Sync 就是小本子的数字版。

页面里调用 navigator.serviceWorker.ready.then(reg => reg.sync.register('upload-notes')),相当于在小本子上记一笔“待办事项”。浏览器立刻或稍后(网络可用时)触发 SW 的 sync 事件,你在事件里执行上传操作。如果任务失败(比如服务器 500),抛出异常即可,浏览器会按指数退避策略自动重试,不会无限猛敲服务器。

后台同步非常适合“离线表单提交”“日志上报”“消息补发”等场景。用户在地铁上写完一条评论,网络断了,评论先进离线队列;出地铁网络恢复,SW 自动补交,整个过程用户无感知。

小本子的妙处在于“不打扰”。用户不需要知道网络断过,也不需要手动重新提交。管家默默把活儿干完,这才是好服务的最高境界。

Tretry=min⁡(Tmax⁡, 2n⋅T0)\displaystyle T_{\text{retry}} = \min(T_{\max},\ 2^{n} \cdot T_{0})
这是重试等待时间的计算公式:第 n 次重试等待时间按 2 的 n 次方增长,封顶为 T_max。
  1. 初始间隔 T0第一次重试前等待的基础时间,通常设为几秒。
    Tretry(0)=T0\displaystyle T_{\text{retry}}(0) = T_{0}
  2. 指数递增每次重试间隔翻倍,1 次失败等 2 倍,2 次失败等 4 倍。
    Tretry(n)=2n⋅T0\displaystyle T_{\text{retry}}(n) = 2^{n} \cdot T_{0}
  3. 上限 T_max防止等待时间无限拉长,超过上限就固定等 T_max 再试。
    Tretry=min⁡(Tmax⁡, 2n⋅T0)\displaystyle T_{\text{retry}} = \min(T_{\max},\ 2^{n} \cdot T_{0})

调试技巧:给管家体检

再靠谱的管家也有需要体检的时候。Chrome DevTools 的 Application 面板——准确说是 Application 标签下的 Service Workers 区域——就是给管家做全面体检的地方。

打开 DevTools → Application → Service Workers,你能看到当前注册的 SW 脚本、作用域、运行状态和更新时间。左上角有 Offline 复选框,勾选后模拟断网,立刻验证离线缓存是否生效。旁边还有 “Update on reload“ 开关,打开后每次刷新页面都会重新扫描 sw.js 是否有变化,省去手动点击更新的麻烦。还有一个 ”Bypass for network“ 选项,勾选后所有请求直接走网络、绕过 SW,用来对比”有管家“和”没管家”的差异。

排查更新问题有个常见套路:改了 sw.js 但页面还是走旧逻辑,先确认是否勾了 Update on reload;再确认新 SW 是否卡在 waiting(在面板里点 Skip Waiting 按钮);最后确认 activate 事件里有没有清理旧缓存。

体检的意义在于把看不见的问题量化。一个优秀的开发者,会习惯性打开 Application 面板,观察每一条请求是管家(缓存)交付的还是云端交付的,然后据此调整策略。

ΔT=Tnetwork−Tcache>ϵ ⇒ 缓存未生效,检查更新流程\displaystyle \Delta T = T_{\text{network}} - T_{\text{cache}} > \epsilon \ \Rightarrow\ \text{缓存未生效,检查更新流程}
这是一个“体检指标”式公式:当网络响应时间明显大于缓存响应时间时,说明请求没有走缓存,需要回头排查拦截逻辑。
  1. T_network 与 T_cache同一请求分别走纯网络和走缓存时的响应时间。
    Tnetwork≫Tcache, 通常差一个数量级\displaystyle T_{\text{network}} \gg T_{\text{cache}},\ \text{通常差一个数量级}
  2. 偏差 ΔT两者之差超过阈值 ε 时,说明本应命中缓存的请求去了网络。
    ΔT=Tnetwork−Tcache\displaystyle \Delta T = T_{\text{network}} - T_{\text{cache}}
  3. 排查方向优先检查 fetch 拦截是否注册、缓存名是否匹配、更新流程是否被 waiting 卡住。
    ΔT>ϵ⇒检查 fetch / cache / waiting 三处\displaystyle \Delta T > \epsilon \Rightarrow \text{检查 fetch / cache / waiting 三处}

AI 应用场景

离线优先的智能客服助手

阶段:数据采集与端侧缓存

解决的问题:传统 Web 客服页面每次提问都要联网,网络不稳定时用户被拒之门外。利用 SW 的 Cache First 策略,把 FAQ 知识库、常见问题模板缓存到本地;再利用 Background Sync,把用户在弱网环境下提交的咨询异步补发到服务端。

具体案例:某银行的 PWA 客服页面,用户在地铁隧道里打开页面,常见问题列表秒开;提交的投诉工单因断网先进离线队列,出隧道后自动上传,并推送一条“工单已收到”的通知。

新闻资讯 App 的推送触达

阶段:推荐结果触达与用户召回

解决的问题:算法推荐出文章后,传统 Web 站无法在用户关闭页面后触达用户。SW 的 Push API 让推荐系统可以把内容直接推送到系统通知栏,点击通知直达文章页,极大提升推荐系统的曝光和点击率。

具体案例:某资讯站点在用户授权通知后,服务端对“算法认为用户感兴趣”的突发新闻,通过 Web Push 发送标题摘要。SW 弹出通知,用户点击后打开预缓存的文章骨架,正文由 Network First 策略保证新鲜。

Python 演示

python93 行
# -*- coding: utf-8 -*-"""模拟 Service Worker 的缓存策略派发 演示 cache_first 与 network_first 在离线场景下的差别。"""import randomimport time  class MockCache:    """模拟 Cache Storage 储物柜"""     def __init__(self):        self.store = {}     def put(self, key, value, ts=None):        self.store[key] = {"value": value, "ts": ts or time.time()}     def match(self, key):        return self.store.get(key)  class ServiceWorker:    """数字管家:拦截请求并按策略分发"""     def __init__(self, strategy="cache_first"):        self.cache = MockCache()        self.strategy = strategy        self.hit_count = 0        self.total_count = 0     def network(self, url):        """模拟网络往返,10% 概率失败"""        time.sleep(random.uniform(0.05, 0.1))        if random.random() < 0.1:            raise ConnectionError("网络不可用")        return {"url": url, "from": "network", "data": "content:" + url}     def fetch(self, url):        """管家拦截请求的入口"""        self.total_count += 1        if self.strategy == "cache_first":            cached = self.cache.match(url)            if cached:                self.hit_count += 1                return cached["value"]            try:                resp = self.network(url)            except ConnectionError:                raise RuntimeError("双重失败:无缓存且断网")            self.cache.put(url, resp)            return resp        try:            resp = self.network(url)            self.cache.put(url, resp)            return resp        except ConnectionError:            cached = self.cache.match(url)            if cached:                self.hit_count += 1                return cached["value"]            raise RuntimeError("双重失败:断网且无缓存")  def run_demo():    print("=== Cache First 策略 ===")    sw_cf = ServiceWorker("cache_first")    for i in range(4):        try:            r = sw_cf.fetch("/api/item/" + str(i))            print(r["from"], "->", r["url"])        except RuntimeError as e:            print("错误:", e)    print("命中:", sw_cf.hit_count, "/", sw_cf.total_count)     print("\n=== Network First 策略(手动断网) ===")    sw_nf = ServiceWorker("network_first")    for i in range(4):        try:            r = sw_nf.fetch("/api/item/" + str(i))            print(r["from"], "->", r["url"])        except RuntimeError as e:            print("错误:", e)    sw_nf.network = lambda url: (_ for _ in ()).throw(ConnectionError("强行走断网"))    try:        r = sw_nf.fetch("/api/item/0")        print(r["from"], "->", r["url"], "(离线兜底成功)")    except RuntimeError as e:        print("错误:", e)  if __name__ == "__main__":    run_demo()
目的:用 Python 模拟 Service Worker 的缓存仓库和两种核心策略,直观对比 Cache First 与 Network First 在离线场景下的行为差异。
  1. self.store[key] = {"value": value, "ts": ts or time.time()}把响应放进模拟储物柜,带时间戳。
  2. cached = self.cache.match(url)管家先在储物柜里翻一遍,对应 caches.match。
  3. self.cache.put(url, resp)网络响应拿到后立刻回填缓存,供下次命中。
  4. raise RuntimeError("双重失败:无缓存且断网")兜底失败时的最终报错。
  5. sw_nf.network = lambda url: ...临时替换网络函数,模拟断网场景,验证 Network First 会退回缓存。

常见误区

误区很多初学者以为只要调用了 `navigator.serviceWorker.register('/sw.js')`,整个页面马上就能离线运行了。刷新一下发现断网还是打不开,就开始怀疑人生。

正解SW 安装需要时间,且首次注册后只对后续页面加载生效——当前页面还没有被 SW 控制。预缓存也必须在 install 事件里做,且 cache.addAll 里的资源必须全部成功缓存,否则整个安装会失败。正确做法是在页面加载完成后注册,并等待 controllerchange 事件确认已被控制。

为何常见:因为 register 返回的 Promise 让人误以为“注册成功 = 已经接管”,其实它只代表脚本下载成功。

误区有人以为 SW 既然叫"Service Worker",那跟 Web Worker 差不多,可以在里面 `document.getElementById` 改页面。

正解SW 运行在独立的 worker 线程中,没有 window、document API,也不能直接访问 DOM。它只能通过 fetch 事件拦截请求、通过 postMessage 与页面通信、通过 Cache API 管理存储。页面表现层的更新仍需页面 JS 配合。

为何常见:把 SW 和普通 Web Worker 混淆了,且“管家”的比喻容易让人以为他能进住户家里摆家具。

误区既然缓存能大幅提速,那所有资源都走 Cache First,岂不是最快?

正解POST 请求、带鉴权信息的 API、实时数据接口都不适合盲目缓存。Cache First 会对用户信息敏感接口返回陈旧数据,甚至把 A 用户的响应缓存后发给 B 用户。正确做法是按资源类型区分:静态资源 Cache First,主要接口 Network First,兜底用 SWR。

为何常见:因为缓存带来的速度提升太诱人,初学者忽略了“新鲜度”和“用户隔离”这两个维度。

误区开发者改了 sw.js,刷新页面,发现改动根本没生效,于是怀疑浏览器缓存。

正解浏览器会在导航时后台对比 sw.js,但新 SW 默认进入 waiting 状态,要等所有旧页面关闭后才接管。调试时需要勾选 DevTools 里的 Update on reload 并点击 Skip Waiting,生产环境则应调用 self.skipWaiting() 配合 clients.claim() 完成平滑升级。

为何常见:大家习惯了“刷新即最新”的 Web 心智模型,忽略了 SW 为了稳定而设计的双版本过渡机制。

面试题

Q1. navigator.serviceWorker.register('/sw.js') 返回的 Promise 成功时,意味着什么?

单选题 · easy

  • A.SW 已经激活并接管所有页面
  • B.SW 脚本解析并注册成功,等待安装← 正确答案
  • C.通知权限已经被用户授权
  • D.Service Worker 已经立即卸载

答案:B。register 成功只代表脚本下载、解析、注册完成,后续还有 installing→activated 的状态推进。就像管家登记完简历,还没开始上班。A 错在“已接管”,C 与注册无关,D 方向完全反了。

Q2. 以下哪个不是 Service Worker 的生命周期状态?

单选题 · easy

  • A.installing
  • B.activated
  • C.waiting
  • D.loading← 正确答案

答案:D。SW 的四种状态是 installing、activated、waiting、redundant。loading 是普通浏览器的页面加载状态,不是 SW 状态机的成员。比喻来说,管家处于“培训中、值班中、等待交接、退休”四种模式,没有“加载中”。

Q3. install 事件中预缓存多个静态资源,最合适的 API 是什么?

单选题 · mid

  • A.caches.open 后手动 put
  • B.cache.addAll(urls) 批量存入← 正确答案
  • C.fetch 预请求所有资源
  • D.localStorage.setItem 存储

答案:B。install 阶段预缓存的标准做法是用 caches.open 打开仓库后调用 cache.addAll,一次性抓取列表内资源。手动 put 虽然可行但代码冗长;fetch 只是发起请求不解决存储;localStorage 是同步 API,存不了 Response 对象。

Q4. 新 SW 进入 waiting 状态后,想让它立即接管页面,应该调用什么?

单选题 · mid

  • A.self.skipWaiting()← 正确答案
  • B.clients.claim()
  • C.location.reload()
  • D.registration.unregister()

答案:A。skipWaiting 让 waiting 的新 SW 跳过等待直接激活;clients.claim 是激活后让已打开页面受控,两者常搭配使用。location.reload 只刷新页面,unregister 是卸载注册,方向反了。

Q5. SW 的 fetch 事件中,用哪个方法决定最终返回给页面的内容?

单选题 · mid

  • A.event.respondWith(promise)← 正确答案
  • B.event.returnValue = response
  • C.event.preventDefault()
  • D.self.postMessage(response)

答案:A。respondWith 是 fetch 事件的核心方法,接收一个 Promise<Response>,让 SW 成为请求的中转人。preventDefault 只是取消默认行为;postMessage 是跨线程通信,不是返回响应;returnValue 是普通事件 API,不适用于 FetchEvent。

Q6. 关于 Cache Storage 中 Response 对象的“一次性流”,正确的处理是?

单选题 · mid

  • A.缓存前先调用 response.clone() 复制一份← 正确答案
  • B.缓存前先把响应转成字符串再存
  • C.每次读取缓存的响应不受任何限制
  • D.响应只能被读取一次,无法缓存展示并存

答案:A。Response 是流式数据,被消费(读取 body)后不能再读。缓存时若既想存仓库又想返回页面,必须 clone() 一份。字符串化会丢失响应头信息;C、D 都忽略了流的单次消费特性。

Q7. 使用 Push API 推送通知时,订阅对象包含的三项核心信息是?

单选题 · mid

  • A.endpoint、p256dh 公钥、auth 密钥← 正确答案
  • B.url、token、用户 ID
  • C.设备型号、浏览器版本、IP 地址
  • D.通知标题、正文、图标

答案:A。订阅对象本质是“邮局信箱 + 加密钥匙”——endpoint 是唯一地址,p256dh 用于内容加密,auth 用于鉴权。设备型号、IP 是隐私数据不会出现在订阅里;标题正文是发送时决定的。

Q8. 关于 Background Sync,下列说法正确的是?

多选题 · mid

  • A.可以在断网时把提交任务加入队列← 正确答案
  • B.网络恢复后 SW 会收到 sync 事件← 正确答案
  • C.同步失败后不会自动重试
  • D.适合离线表单提交和日志上报← 正确答案

答案:ABD。Background Sync 先把任务注册到队列,网络可用时触发 sync 事件,这正是“小本子补办”的机制。失败后抛出异常会按指数退避重试,所以 C 错。离线表单、日志上报都是典型场景。

Q9. 以下哪些情况会导致缓存策略失效或行为异常?

多选题 · mid

  • A.缓存了 POST 请求的响应← 正确答案
  • B.缓存同一份资源却不做用户隔离← 正确答案
  • C.install 事件里预缓存了版本文件
  • D.activate 阶段清理了旧版本缓存

答案:AB。POST 请求语义上不是幂等读取,缓存会导致数据错乱;不做用户隔离会把 A 用户的私有数据发给 B 用户。预缓存版本文件和清理旧缓存都是正确操作。缓存策略要按请求语义和用户维度精细设计。

Q10. 要让用户设备上的 SW 从旧版本升级到新版本,必须满足哪些条件?

多选题 · hard

  • A.服务器上的 sw.js 内容与本地不一致← 正确答案
  • B.旧 SW 停止控制所有页面或调用 skipWaiting← 正确答案
  • C.用户清除浏览器全部缓存数据
  • D.新 SW 的 install 和 activate 流程顺利完成← 正确答案

答案:ABD。版本升级需要三个前提:浏览器检测到 sw.js 字节变化、旧 SW 不再控制页面(或 skipWaiting 跳过等待)、新 SW 成功完成 install+activate。用户清缓存不是升级条件,反而会连 SW 注册一起清掉。