跳到主要内容

说说 Service Worker 是什么?怎么用?有哪些坑?

面试速答(30 秒版 TL;DR)

  • Service Worker(SW) 是运行在浏览器后台的脚本,不直接操作 DOM,但可以拦截页面的网络请求(fetch 事件),从而实现 离线缓存、弱网加速、统一资源更新 等能力。
  • 它的核心位置是:页面与网络之间的一层可编程代理(类似“可编程缓存层”)。
  • 有严格限制:通常要求 HTTPS(或 localhost),受 scope(作用域) 约束,并且有明确的 生命周期(install/activate/waiting/…),更新与生效时机是高频考点。

心智模型:它在“页面”和“网络”之间

你可以把 SW 理解为“前端自己写的 CDN/反向代理的迷你版”,但它运行在用户浏览器里,且受同源与安全策略约束。


关键概念与机制

1)scope(作用域)

  • SW 只会控制它的 scope 覆盖的 URL 范围(通常由 sw.js 所在路径决定,也可以通过注册参数指定)。
  • scope 越大能力越强,但风险也越大:一旦缓存策略写错,影响面更大。

2)生命周期(install -> activate -> 控制页面)

面试要点:

  • install:适合做“预缓存(precache)”与资源准备。
  • activate:适合做“清理旧缓存”、迁移、clients.claim()(是否立刻接管)。
  • waiting:新 SW 往往会卡在 waiting,直到旧 SW 不再控制任何页面(或你显式 skipWaiting())。

3)拦截请求:fetch 事件 + 缓存策略

常见策略(只要会说清楚取舍即可):

  • Cache First:静态资源最快,但容易“陈旧”。
  • Network First:数据更“新”,但弱网体验差。
  • Stale-While-Revalidate:先用缓存兜底,再后台更新,体验与新鲜度折中。

最小可运行示例(面试讲得清,工作也能用)

页面侧注册(例如 main.js

if ("serviceWorker" in navigator) {
window.addEventListener("load", async () => {
const reg = await navigator.serviceWorker.register("/sw.js", {
scope: "/",
});
console.log("SW registered:", reg.scope);
});
}

sw.js(预缓存 + 运行时缓存)

const CACHE_NAME = "app-cache-v1";
const PRECACHE_URLS = ["/", "/index.html", "/styles.css", "/app.js"];

self.addEventListener("install", (event) => {
event.waitUntil(
caches
.open(CACHE_NAME)
.then((cache) => cache.addAll(PRECACHE_URLS))
.then(() => self.skipWaiting())
);
});

self.addEventListener("activate", (event) => {
event.waitUntil(
(async () => {
const keys = await caches.keys();
await Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k)));
await self.clients.claim();
})()
);
});

self.addEventListener("fetch", (event) => {
const req = event.request;
if (req.method !== "GET") return;

event.respondWith(
(async () => {
const cached = await caches.match(req);
if (cached) return cached;

const res = await fetch(req);
const cache = await caches.open(CACHE_NAME);
cache.put(req, res.clone());
return res;
})()
);
});

要点:

  • cache.put(req, res.clone()) 必须 clone(),因为 Response body 只能读一次。
  • skipWaiting() + clients.claim() 会让“更新更激进”,但也更容易引入“新旧版本不一致”的线上问题(见下文坑点)。

更新机制:为什么“我已经部署了但用户还是旧版本”?

本质原因:SW 是有生命周期与接管时机的,不是“刷新就一定生效”。

你要能说明这条链路:

  • 新版本 sw.js 下载后进入 Installing,安装完成后通常进入 Waiting
  • 只有当旧 SW 不再控制任何页面时,新 SW 才会进入 Activating/Activated 并接管。
  • 常见解决方案:
    • 让页面监听 updatefound,提示用户“有新版本,点击刷新”。
    • 或在可控场景使用 skipWaiting() + clients.claim(),并配合资源版本化,避免出现“HTML 新、JS 旧”之类的割裂。

典型题与标准答法

Q1:Service Worker 和 Web Worker 有什么区别?

  • Web Worker:把计算丢到后台线程,解决主线程卡顿;不拦截网络请求,也不做离线能力的核心承载。
  • Service Worker:面向“网络与缓存”,可拦截请求、做离线与更新;同样不操作 DOM,但更像“可编程代理层”。

Q2:为什么 SW 一般要求 HTTPS?

因为它能拦截并篡改网络响应,权限很大;如果允许在不安全通道下注册,攻击者可中间人注入恶意 SW,后果严重。

Q3:skipWaiting()/clients.claim() 用不用?

结论式回答:

  • 想要“更新立刻生效”:可以用,但要承担“新旧资源混用”风险。
  • 想要“更新更稳”:不强制接管,提示用户刷新,让一次导航完成“整体切换”通常更安全。

易错点/坑(面试加分)

  • 缓存版本不管理:不清理旧 CACHE_NAME,用户磁盘越占越大,还可能一直命中旧资源。
  • 把接口响应也 Cache First:用户看到旧数据;含鉴权信息时还有安全风险(要区分 public/static 与 user-specific)。
  • 跨域请求缓存的“opaque 响应”no-cors 下的响应不可读,调试困难,缓存策略要谨慎。
  • scope 过大:一次失误影响整个站点;scope 设计要遵循最小权限原则。

速记要点(可背诵)

  • SW = 页面与网络之间的可编程代理:install/activate/fetch 三件套。
  • 关键词:HTTPS、scope、waiting、Cache Storage、更新生效时机。