使用Parcel.js构建的PWA在线正常,离线时Service Worker失效
问题描述
我的Service Worker在在线状态下能正常安装并从Cache Storage获取文件,但一旦离线就无法找到任何资源。怀疑可能是加载顺序错误、main.ts中async使用不当,或是Parcel的manifest缓存文件列表不完整。
代码示例
index.html文件
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>Journal</title> <meta name="HandheldFriendly" content="True"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="theme-color" media="(prefers-color-scheme: light)" content="hsl(45, 100%, 50%)"> <meta name="theme-color" media="(prefers-color-scheme: dark)" content="hsl(45, 100%, 50%)"> <link rel="shortcut icon" href="media/img/logo.svg"> <link rel="stylesheet" href="css/main.css"> <link rel="manifest" href="./app.webmanifest"> </head> <body onload="document.body.style.visibility=`visible`;"> <!-- avoid flash of unstyled content --> <script>document.body.style.visibility=`hidden`;</script> <div class="app"> </div> <script src="main.ts" type="module"></script> </body> </html>
main.ts文件
import { createApp } from 'vue'; import {router} from './js/4_widgets/router'; import appWidget from './js/4_widgets/app.vue'; import * as storeHelper from './js/3_io/storeHelper'; import * as globalState from './js/2_logic/state'; import * as testData from './js/1_data/testData'; import * as browserStore from './js/3_io/browserStore'; setupApp(); async function setupApp() { globalThis.__VUE_OPTIONS_API__ = (process.env.NODE_ENV === "development") as unknown as string; globalThis.__VUE_PROD_DEVTOOLS__ = (process.env.NODE_ENV === "development") as unknown as string; await registerServiceWorker(); await storeHelper.start(); createApp(appWidget) .use(router) .mount(`.app`); } async function registerServiceWorker(): Promise<void> { if ('serviceWorker' in navigator) { try { const registration = await navigator.serviceWorker.register(new URL('./serviceWorker.ts', import.meta.url), {type: 'module', scope: '/' }); console.log('Service worker registered', registration); } catch (error) { console.error('Error registering service worker:', error); } } else console.warn('Service workers are not supported.'); }
serviceWorker.ts文件
/// <reference types="serviceworker" /> import {manifest, version} from '@parcel/service-worker'; self.addEventListener('offline', (event: Event) => { console.log('Offline!'); }); self.addEventListener('install', async (event: ExtendableEvent) => { try { const cache = await caches.open(version); await cache.addAll(manifest); await self.skipWaiting(); console.log('Service worker installed'); } catch (error) { console.error('Service worker installation failed:', error); } }); self.addEventListener('activate', async (event: Event) => { const keys = await caches.keys(); await Promise.all( keys.map(key => key !== version && caches.delete(key)) ); console.log('Service worker activated'); }); self.addEventListener('fetch', async (event: FetchEvent) => { try { console.log(`${event.request.method} - ${event.request.url} - request received and we are online: ${navigator.onLine}`); if (event.request.method !== "GET") return fetch(event.request); const response: Response | undefined = await caches.match(event.request); if (!response) console.warn(`${event.request.method} - ${event.request.url} - was NOT found in the cache`); console.log(`${event.request.method} - ${event.request.url} - was found in the cache`); if (response) return response; if (navigator.onLine) { console.log(`${event.request.method} - ${event.request.url} - we are online, so trying to connect...`); return fetch(event.request); } console.error(`${event.request.method} - ${event.request.url} - is not in cache and we are offline`); } catch (error) { console.error('Service worker fetch failed:', error); } });
调试日志
在Brave浏览器中访问localhost:1234时的控制台情况:
- 首次加载,安装正常:
Service worker registered ServiceWorkerRegistration {installing: ServiceWorker, waiting: null, active: null, navigationPreload: NavigationPreloadManager, scope: 'http://localhost:1234/', …} serviceWorker.ts:32 Service worker activated serviceWorker.ts:18 Service worker installed
- 页面刷新,缓存资源被获取(但未尝试从缓存获取index.html):
GET - http://localhost:1234/index.34df27b5.css - was found in the cache serviceWorker.js?1684145072288:641 GET - http://localhost:1234/index.b7a05eb9.js - request received and we are online: true serviceWorker.js?1684145072288:645 GET - http://localhost:1234/index.b7a05eb9.js - was found in the cache serviceWorker.js?1684145072288:641 GET - http://localhost:1234/logo.92616178.svg - request received and we are online: true serviceWorker.js?1684145072288:641 GET - http://localhost:1234/app.webmanifest - request received and we are online: true serviceWorker.js?1684145072288:645 GET - http://localhost:1234/logo.92616178.svg - was found in the cache serviceWorker.js?1684145072288:645 GET - http://localhost:1234/app.webmanifest - was found in the cache main.ts:39 Service worker registered ServiceWorkerRegistration {installing: ServiceWorker, waiting: null, active: ServiceWorker, navigationPreload: NavigationPreloadManager, scope: 'http://localhost:1234/', …} :1234/serviceWorker.js?1684145072288:641 GET - http://localhost:1234/appIcon512.7b148678.png - request received and we are online: true :1234/serviceWorker.js?1684145072288:645 GET - http://localhost:1234/appIcon512.7b148678.png - was found in the cache serviceWorker.ts:32 Service worker activated serviceWorker.ts:18 Service worker installed
- 离线后刷新,缓存资源未找到,且
navigator.onLine未设为false:
bug/1173575, non-JS module files deprecated. (anonymous) @ VM11:161 serviceWorker.ts:39 GET - http://localhost:1234/ - request received and we are online: true serviceWorker.ts:44 GET - http://localhost:1234/ - was NOT found in the cache (anonymous) @ serviceWorker.ts:44 serviceWorker.ts:45 GET - http://localhost:1234/ - was found in the cache serviceWorker.ts:50 GET - http://localhost:1234/ - we are online, so trying to connect... serviceWorker.ts:51 Uncaught (in promise) TypeError: Failed to fetch at serviceWorker.ts:51:20
若访问
http://localhost:1234/index.html,Service Worker显示文件已找到,但页面无内容
- 再次刷新,页面完全失效:
bug/1173575, non-JS module files deprecated.
问题分析与修复
1. 根问题:首页路由(/)未被缓存
从调试日志可见,离线后访问/时找不到缓存,但访问/index.html能命中缓存。这说明Parcel生成的manifest里只包含了index.html,没有包含根路径/的请求记录。
修复方法:在Service Worker的install事件中,手动把根路径'/'添加到缓存列表:
self.addEventListener('install', async (event: ExtendableEvent) => { try { const cache = await caches.open(version); // 添加根路径到缓存列表 await cache.addAll([...manifest, '/']); await self.skipWaiting(); console.log('Service worker installed'); } catch (error) { console.error('Service worker installation failed:', error); } });
2. navigator.onLine判断不可靠
Service Worker中navigator.onLine的状态更新存在延迟,离线后可能仍显示true,导致代码误判网络状态去发起请求,最终报错。
修复方法:不要依赖navigator.onLine,改用fetch请求的异常捕获来处理离线情况,同时在fetch事件中使用event.respondWith()确保返回正确的响应:
self.addEventListener('fetch', (event: FetchEvent) => { event.respondWith( (async () => { try { console.log(`${event.request.method} - ${event.request.url} - 尝试从缓存获取`); if (event.request.method !== "GET") { return await fetch(event.request); } // 先尝试从缓存获取 const cachedResponse = await caches.match(event.request); if (cachedResponse) { console.log(`${event.request.method} - ${event.request.url} - 命中缓存`); return cachedResponse; } // 缓存未命中,尝试网络请求 console.log(`${event.request.method} - ${event.request.url} - 缓存未命中,尝试网络请求`); const networkResponse = await fetch(event.request); // 可选:把新的网络响应存入缓存,供下次使用 const cache = await caches.open(version); cache.put(event.request, networkResponse.clone()); return networkResponse; } catch (error) { console.error(`${event.request.method} - ${event.request.url} - 网络请求失败,离线状态`, error); // 这里可以返回自定义的离线页面或错误响应 return new Response('离线状态,无法获取资源', { status: 503 }); } })() ); });
3. 页面无内容的问题
当访问/index.html命中缓存但页面无内容,是因为原代码中fetch事件处理没有正确返回响应(原代码直接return response但没有用event.respondWith()包裹,导致Service Worker无法正确拦截并返回响应)。上面的修复已经用event.respondWith()包裹了整个逻辑,能解决这个问题。
4. 额外优化:确保Service Worker激活后立即控制页面
在activate事件中调用self.clients.claim(),让新激活的Service Worker立即控制所有打开的页面,避免需要刷新多次才能生效:
self.addEventListener('activate', async (event: ExtendableEvent) => { const keys = await caches.keys(); await Promise.all( keys.map(key => key !== version && caches.delete(key)) ); // 立即控制所有客户端页面 await self.clients.claim(); console.log('Service worker activated'); });
内容的提问来源于stack exchange,提问作者Richard

