Skip to content

Commit 2d4ffdb

Browse files
sunnylqmclaude
andcommitted
fix(pwa): service worker 在 dev 环境自毁,修复缓存旧 bundle 导致的无限刷新
cache-first 的 sw.js 会把 dev server 的非哈希 JS 钉死在 CacheStorage, dev server 重启后旧 bundle 请求已不存在的 hot-update.json,404 后 HMR 客户端整页 reload,陷入无限刷新且无法自愈。 - sw.js:localhost/127.0.0.1/::1 上自毁(清缓存 + 注销),CACHE_NAME 升至 v2 - register-pwa.js:dev 环境不注册并清理残留注册 - rspress.config.ts:head 内联清理脚本(内联不经过 SW 拦截, 即使页面处于毫秒级 reload 循环也能在若干次内胜出打破循环) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0112yBeEEYT8tyywNbypwXkN
1 parent 53c88cd commit 2d4ffdb

3 files changed

Lines changed: 106 additions & 58 deletions

File tree

site/pages/public/register-pwa.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,18 @@
33
return;
44
}
55

6+
// Don't register the SW on dev origins (its cache-first strategy pins stale
7+
// dev bundles); also clean up any previously installed registration.
8+
var DEV_HOSTNAMES = ['localhost', '127.0.0.1', '::1', '[::1]'];
9+
if (DEV_HOSTNAMES.indexOf(location.hostname) !== -1) {
10+
navigator.serviceWorker.getRegistrations().then(function (registrations) {
11+
registrations.forEach(function (registration) {
12+
registration.unregister();
13+
});
14+
});
15+
return;
16+
}
17+
618
window.addEventListener('load', function () {
719
navigator.serviceWorker.register('/sw.js').catch(function (error) {
820
console.warn('PWA registration failed:', error);

site/pages/public/sw.js

Lines changed: 85 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,95 @@
1-
const CACHE_NAME = 'pushy-site-v1';
1+
const CACHE_NAME = 'pushy-site-v2';
22
const PRECACHE_URLS = ['/', '/manifest.webmanifest', '/images/logo.svg'];
33

4-
self.addEventListener('install', (event) => {
5-
event.waitUntil(
6-
caches
7-
.open(CACHE_NAME)
8-
.then((cache) => cache.addAll(PRECACHE_URLS))
9-
.then(() => self.skipWaiting()),
10-
);
11-
});
4+
// Never cache on dev origins: the dev server's JS URLs are not content-hashed,
5+
// so a cache-first SW pins a stale bundle whose HMR hash no longer exists on
6+
// the server, causing an infinite 404 → reload loop. This branch self-destructs
7+
// (clears caches + unregisters) so previously poisoned browsers heal on the
8+
// next service-worker update check.
9+
const DEV_HOSTNAMES = ['localhost', '127.0.0.1', '::1', '[::1]'];
1210

13-
self.addEventListener('activate', (event) => {
14-
event.waitUntil(
15-
caches
16-
.keys()
17-
.then((keys) =>
18-
Promise.all(
19-
keys
20-
.filter((key) => key !== CACHE_NAME)
21-
.map((key) => caches.delete(key)),
22-
),
23-
)
24-
.then(() => self.clients.claim()),
25-
);
26-
});
11+
if (DEV_HOSTNAMES.includes(self.location.hostname)) {
12+
self.addEventListener('install', () => self.skipWaiting());
13+
self.addEventListener('activate', (event) => {
14+
event.waitUntil(
15+
caches
16+
.keys()
17+
.then((keys) => Promise.all(keys.map((key) => caches.delete(key))))
18+
.then(() => self.registration.unregister())
19+
.then(() => self.clients.matchAll({ type: 'window' }))
20+
.then((clients) => {
21+
for (const client of clients) {
22+
client.navigate(client.url).catch(() => {});
23+
}
24+
}),
25+
);
26+
});
27+
} else {
28+
self.addEventListener('install', (event) => {
29+
event.waitUntil(
30+
caches
31+
.open(CACHE_NAME)
32+
.then((cache) => cache.addAll(PRECACHE_URLS))
33+
.then(() => self.skipWaiting()),
34+
);
35+
});
2736

28-
self.addEventListener('fetch', (event) => {
29-
const { request } = event;
37+
self.addEventListener('activate', (event) => {
38+
event.waitUntil(
39+
caches
40+
.keys()
41+
.then((keys) =>
42+
Promise.all(
43+
keys
44+
.filter((key) => key !== CACHE_NAME)
45+
.map((key) => caches.delete(key)),
46+
),
47+
)
48+
.then(() => self.clients.claim()),
49+
);
50+
});
3051

31-
if (request.method !== 'GET') {
32-
return;
33-
}
52+
self.addEventListener('fetch', (event) => {
53+
const { request } = event;
3454

35-
const url = new URL(request.url);
36-
if (url.origin !== self.location.origin) {
37-
return;
38-
}
55+
if (request.method !== 'GET') {
56+
return;
57+
}
3958

40-
if (request.mode === 'navigate') {
41-
event.respondWith(
42-
fetch(request)
43-
.then((response) => {
44-
const copy = response.clone();
45-
caches.open(CACHE_NAME).then((cache) => cache.put(request, copy));
46-
return response;
47-
})
48-
.catch(() => caches.match(request).then((cached) => cached || caches.match('/'))),
49-
);
50-
return;
51-
}
59+
const url = new URL(request.url);
60+
if (url.origin !== self.location.origin) {
61+
return;
62+
}
5263

53-
event.respondWith(
54-
caches.match(request).then((cached) => {
55-
if (cached) {
56-
return cached;
57-
}
64+
if (request.mode === 'navigate') {
65+
event.respondWith(
66+
fetch(request)
67+
.then((response) => {
68+
const copy = response.clone();
69+
caches.open(CACHE_NAME).then((cache) => cache.put(request, copy));
70+
return response;
71+
})
72+
.catch(() =>
73+
caches.match(request).then((cached) => cached || caches.match('/')),
74+
),
75+
);
76+
return;
77+
}
5878

59-
return fetch(request).then((response) => {
60-
if (response.ok) {
61-
const copy = response.clone();
62-
caches.open(CACHE_NAME).then((cache) => cache.put(request, copy));
79+
event.respondWith(
80+
caches.match(request).then((cached) => {
81+
if (cached) {
82+
return cached;
6383
}
64-
return response;
65-
});
66-
}),
67-
);
68-
});
84+
85+
return fetch(request).then((response) => {
86+
if (response.ok) {
87+
const copy = response.clone();
88+
caches.open(CACHE_NAME).then((cache) => cache.put(request, copy));
89+
}
90+
return response;
91+
});
92+
}),
93+
);
94+
});
95+
}

site/rspress.config.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,15 @@ export default defineConfig({
9595
content: 'Pushy',
9696
},
9797
},
98+
{
99+
// Inline (not SW-interceptable) dev-only cleanup: unregister any
100+
// previously installed service worker and drop its caches, so dev
101+
// pages can never get stuck on a stale cached bundle.
102+
tag: 'script',
103+
append: false,
104+
children:
105+
"(function(){var h=location.hostname;if(h!=='localhost'&&h!=='127.0.0.1'&&h!=='::1'&&h!=='[::1]')return;if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then(function(rs){rs.forEach(function(r){r.unregister()})})}if('caches' in window){caches.keys().then(function(ks){ks.forEach(function(k){caches.delete(k)})})}})();",
106+
},
98107
{
99108
tag: 'script',
100109
attrs: {

0 commit comments

Comments
 (0)