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

Workbox Service Worker导致SPA路由失效问题求助

问题

通过InjectManifest模式配置Workbox(仅设置swSrc和swDest属性),并创建了对应的Service Worker(代码如下)。从站点根目录启动时一切正常,但从React Router管理的其他页面(比如刷新页面)启动时,Service Worker报错:Cannot get /page(其中page为尝试加载的URL路由)。

观察发现Service Worker中未配置这些路由的处理逻辑,但它似乎阻止了请求任意URL时加载index.html文件。尝试使用registerRoute(new NavigationRoute(createHandlerBoundToURL('/index.html'))),但出现index.html未预缓存的错误。不想缓存index.html,避免因始终提供旧缓存页面导致无法下载新Service Worker,请问还有什么解决办法?

import { CacheableResponsePlugin } from 'workbox-cacheable-response/CacheableResponsePlugin';
import { CacheFirst } from 'workbox-strategies/CacheFirst';
import { StaleWhileRevalidate } from 'workbox-strategies/StaleWhileRevalidate';
import { ExpirationPlugin } from 'workbox-expiration/ExpirationPlugin';
import { precacheAndRoute } from 'workbox-precaching/precacheAndRoute';
import { registerRoute } from 'workbox-routing/registerRoute';
import { setCatchHandler } from 'workbox-routing/setCatchHandler';
import { setDefaultHandler } from 'workbox-routing/setDefaultHandler';
import { clientsClaim } from 'workbox-core';
import { NetworkOnly } from 'workbox-strategies';

const imageFallback = `/media/catalog/product/p/l/placeholder.jpg`;

precacheAndRoute(self.__WB_MANIFEST);
clientsClaim();
setDefaultHandler(new NetworkOnly());

registerRoute(
    new RegExp('(robots.txt|favicon.ico|manifest.json)'),
    new StaleWhileRevalidate()
);

// Handle images hosted with the PWA, that means they'll be served from the same origin
registerRoute(
    ({ url, sameOrigin }) => {
        // Only cache images from the same origin
        if (!sameOrigin) return false;

        return url.pathname.match(/\.(?:png|gif|jpg|jpeg|svg|pjpg|webp)$/);
    },

    new StaleWhileRevalidate({
        cacheName: 'local-images',
        matchOptions: {
            ignoreVary: true
        },
        plugins: [
            new ExpirationPlugin({
                // Keep at most 100 entries
                maxEntries: 100,
                // Don't keep any entries for more than 7 days
                maxAgeSeconds: 7 * 24 * 60 * 60,
                // Automatically cleanup if quota is exceeded
                purgeOnQuotaError: true
            }),
            new CacheableResponsePlugin({
                statuses: [0, 200]
            })
        ]
    })
);

// Handle images loaded from the BE URL. This likely means product images
registerRoute(
    ({ url }) => {
        if (url.origin !== __MAGENTO_BE_URL__) return false;

        return url.pathname.match(/\.(?:png|gif|jpg|jpeg|svg|pjpg|webp)$/);
    },

    new StaleWhileRevalidate({
        cacheName: 'remote-images',
        matchOptions: {
            ignoreVary: true
        },
        plugins: [
            new ExpirationPlugin({
                // Keep at most 100 entries
                maxEntries: 100,
                // Don't keep any entries for more than 7 days
                maxAgeSeconds: 7 * 24 * 60 * 60,
                // Automatically cleanup if quota is exceeded
                purgeOnQuotaError: true
            }),
            new CacheableResponsePlugin({
                statuses: [0, 200]
            })
        ]
    })
);

registerRoute(new RegExp(/\.js$/), new CacheFirst());

self.addEventListener('fetch', event => {
    const { request } = event;
    const responsePromise = router.handleRequest({
        event,
        request
    });
    if (responsePromise) {
        // Router found a route to handle the request.
        event.respondWith(responsePromise);
    } else {
        // No route was found to handle the request.
        // Fallback to network
    }
});

const catchHandler = async options => {
    const dest = options.request.destination;
    const cache = await self.caches.open('workbox-offline-fallbacks');

    if (dest === 'image' && imageFallback !== false) {
        return (await cache.match(imageFallback)) || Response.error();
    }

    return Response.error();
};

setCatchHandler(catchHandler);

self.addEventListener('install', event => {
    const files = [];
    if (imageFallback) {
        files.push(imageFallback);
    }

    event.waitUntil(
        self.caches
            .open('workbox-offline-fallbacks')
            .then(cache => cache.addAll(files))
    );
});

self.addEventListener('message', event => {
    if (event.data && event.data.type === 'SKIP_WAITING') {
        self.skipWaiting();
    }
});
解决办法

核心思路是针对导航请求直接走网络请求/index.html,不缓存它,同时确保Workbox的路由能正确匹配这些单页路由。具体调整如下:

1. 补充导入导航路由模块

在代码开头的导入部分添加:

import { NavigationRoute } from 'workbox-routing/NavigationRoute';

2. 注册导航路由规则

在setDefaultHandler(new NetworkOnly());之后添加以下代码:

// 处理所有导航请求,直接走网络拉取最新的index.html
registerRoute(new NavigationRoute(new NetworkOnly({
  fetchOptions: {
    credentials: 'same-origin', // 保持同域凭证,按需调整
  },
}), {
  // 排除不需要重定向到index.html的路径,比如API、静态资源
  denylist: [/^\/api/, /\.(js|css|png|jpg|svg)$/],
}));

3. 移除冲突的手动fetch监听

删除这段手动的fetch事件监听代码(Workbox路由系统已自动处理fetch逻辑,手动监听会引发冲突):

self.addEventListener('fetch', event => {
    const { request } = event;
    const responsePromise = router.handleRequest({
        event,
        request
    });
    if (responsePromise) {
        // Router found a route to handle the request.
        event.respondWith(responsePromise);
    } else {
        // No route was found to handle the request.
        // Fallback to network
    }
});

调整说明

  • 导航请求(比如刷新/page)会被NavigationRoute捕获,直接发起网络请求获取最新的index.html,完全不缓存,确保每次都能拿到最新页面,也不会干扰Service Worker的更新流程
  • denylist参数可以过滤掉不需要重定向的路径,避免API接口、静态资源等被错误导向index.html
  • 移除手动fetch监听是因为Workbox内部已经封装了路由处理逻辑,重复监听会导致请求处理混乱,引发Cannot get /page这类错误

内容的提问来源于stack exchange,提问作者Denno

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.24 21:18:17