同源多应用部署踩坑记:Service Worker 把子路径「吃」掉了
一、现象
服务端一切正常:本机 curl 带鉴权返回 200,页面 15KB 完整,外网经代理访问同样 200,标题、按钮、脚本都在。但浏览器里打开 https://HOST:8443/app/,看到的却是另一个应用(一套笔记工具)的界面,控制台没有任何报错。
更奇怪的是,换浏览器无痕窗口就正常,普通窗口一直坏。
二、排查顺序
先把"服务挂了"这个假设排除掉,按链路从内到外逐段验证:
- 本机回环直连,带 Basic Auth,看状态码与响应体长度;
- 指定
--resolve强制走本机,排除 DNS 与 CDN; - 外网经代理再请求一次,确认边界一致;
- 最后在浏览器里读
document.title与首屏 DOM,而不是看截图。
前三条全绿,问题就锁定在浏览器侧。
三、根因:Service Worker 的导航兜底
同一个域名下,根路径部署了一个 PWA。它的 Service Worker 注册在根作用域,fetch 事件里对 mode === "navigate" 的请求做了离线兜底:只要网络失败或匹配到缓存策略,就返回缓存的 index.html。
问题在于缓存键匹配写得太宽——用 caches.match(event.request) 命中不到时,直接 return caches.match("/")。于是 /app/ 这种子路径导航请求,被换成了笔记应用的外壳。服务端 200 的响应根本没机会渲染。
这也解释了为什么无痕正常:无痕窗口不共享 Service Worker 注册。
四、快速确认方法
在问题页面执行:
navigator.serviceWorker.getRegistrations()
.then(rs => rs.map(r => [r.scope, r.active && r.active.scriptURL]))
如果 scope 是根路径、脚本是你另一个应用的 sw.js,基本可以定案。再配合 DevTools → Application → Service Workers 勾选 Bypass for network,页面立刻恢复,就是它。
五、三种解法
1. 改匹配逻辑(治本,但要改代码)
导航兜底只对应用自己的路径生效,其它路径直接走网络:
self.addEventListener("fetch", (e) => {
if (e.request.mode !== "navigate") return;
const url = new URL(e.request.url);
if (!url.pathname.startsWith("/notes/")) return; // 只兜自己的路径
e.respondWith(
fetch(e.request).catch(() => caches.match("/notes/index.html"))
);
});
2. 独立 origin(治本,最省事)
给第二个应用单独的子域名和证书,作用域天然隔离,互不干扰。缺点是 DNS、证书、Caddy/Nginx 站点各配一份。
3. 临时绕过(治标)
让用户清一次该站点的存储,或在部署里给 /sw.js 返回 Cache-Control: no-store 并注销旧注册。只能救急,下次发版还会复现。
六、小结
同一个 origin 下挂多个应用时,Service Worker 的作用域是整个 origin,不是路径。任何在根路径注册了 SW 的应用,都可能把兄弟路径的导航请求"吃掉"。排查时先分清"服务端是否正常"和"浏览器拿到的是不是服务端的响应",再去看 SW、缓存和重定向,能省掉大量无效改动。
- 点赞
- 收藏
- 关注作者
评论(0)