You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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时的控制台情况:

  1. 首次加载,安装正常:
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
  1. 页面刷新,缓存资源被获取(但未尝试从缓存获取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
  1. 离线后刷新,缓存资源未找到,且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显示文件已找到,但页面无内容

  1. 再次刷新,页面完全失效:
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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.22 05:02:03