说说 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(),因为Responsebody 只能读一次。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、更新生效时机。