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

PWA离线状态下已缓存资源仍无法启动Unity WebGL如何解决

问题原因
  • 缓存精确匹配规则不兼容:caches.match()默认执行严格匹配,会同时校验请求方法、查询参数、请求模式等字段。Unity WebGL运行时请求资源时,会自动追加构建版本、时间戳类的查询参数(例如实际请求路径为Build/WebGL.wasm?build=12345),和你缓存列表中无参数的路径无法匹配,离线状态下匹配失败后直接发起网络请求,必然报错。
  • Range请求响应不符合预期:Unity WebGL加载器默认会携带Range请求头,分块读取.data、.wasm这类大体积文件。当前逻辑直接把缓存中存储的全量200响应返回给Range请求,响应状态、Content-Range头信息和Unity预期的206部分内容响应不匹配,会直接触发资源解析失败。
  • Service Worker生效逻辑缺失:当前代码没有在安装阶段调用self.skipWaiting(),也没有在激活阶段调用clients.claim(),新安装的Service Worker不会立即接管页面,用户打开页面时仍被旧版本Service Worker控制,新配置的缓存规则完全不生效。
  • 缓存路径校验缺失:如果站点部署在子路径下,你写的相对路径如果和实际资源路径不一致,会导致cache.addAll()阶段直接报错,资源根本没有写入缓存。
修复方案
  1. 调整缓存匹配逻辑,匹配资源时忽略URL查询参数,只对比路径部分
  2. 新增Range请求特殊处理逻辑:检测到Range请求时,从缓存读取全量资源后手动切割对应字节段,构造符合规范的206响应返回
  3. 补全Service Worker安装、激活阶段的接管逻辑,新增旧版本缓存清理逻辑
  4. 安装阶段可以打开Chrome DevTools的Application > Cache Storage面板,确认所有资源都已经成功写入缓存,排除路径错误导致的缓存失败问题

修正后的Service Worker完整代码如下:

const cacheName = "unity-static-v1";
// 注意:如果部署在子路径,这里的路径要和实际部署路径完全一致
const appFiles = [
    "./",
    "xmlhttprequest-length-computable.min.js?v=1.5.1.22060315",
    "images/favicon.ico",
    "images/icon.png",
    "style.css",
    "responsiveMinimalTemplateStyles.css",
    "responsiveMinimalTemplateLoader.js",
    "manifest.json",
    "Build/WebGL.loader.js",
    "Build/WebGL.framework.js",
    "Build/WebGL.data",
    "Build/WebGL.wasm"
];

// 自定义缓存匹配函数:忽略查询参数匹配
async function matchCacheIgnoreSearch(request) {
    const cache = await caches.open(cacheName);
    const cachedRequests = await cache.keys();
    const targetUrl = new URL(request.url);
    const matched = cachedRequests.find(req => {
        const reqUrl = new URL(req.url);
        return reqUrl.origin === targetUrl.origin && reqUrl.pathname === targetUrl.pathname;
    });
    return matched ? cache.match(matched) : null;
}

// 处理Range请求,构造206响应
async function handleRangeRequest(request, cachedResponse) {
    const rangeHeader = request.headers.get('Range');
    if (!rangeHeader || !cachedResponse) return cachedResponse;

    const blob = await cachedResponse.blob();
    const matches = /bytes=(\d+)-(\d*)/.exec(rangeHeader);
    const start = parseInt(matches[1], 10);
    const end = matches[2] ? parseInt(matches[2], 10) : blob.size - 1;
    const slicedBlob = blob.slice(start, end + 1, blob.type);

    return new Response(slicedBlob, {
        status: 206,
        statusText: 'Partial Content',
        headers: [
            ['Content-Type', blob.type],
            ['Content-Length', slicedBlob.size],
            ['Content-Range', `bytes ${start}-${end}/${blob.size}`],
            ['Accept-Ranges', 'bytes']
        ]
    });
}

self.addEventListener("install", e => {
    console.log('Install SW');
    e.waitUntil(
        caches.open(cacheName).then(cache => {
            return cache.addAll(appFiles);
        }).then(() => self.skipWaiting()) // 安装完成后立即进入激活阶段
    );
});

self.addEventListener("activate", e => {
    e.waitUntil(
        // 清理旧版本缓存
        caches.keys().then(keys => {
            return Promise.all(keys.filter(key => key !== cacheName).map(key => caches.delete(key)));
        }).then(() => self.clients.claim()) // 立即接管所有页面
    );
});

self.addEventListener("fetch", e => {
    // 只处理GET请求
    if (e.request.method !== 'GET') return;

    e.respondWith(
        matchCacheIgnoreSearch(e.request).then(async cachedResponse => {
            if (cachedResponse) {
                // 处理Range请求
                return handleRangeRequest(e.request, cachedResponse.clone());
            }
            // 离线时没有匹配缓存直接返回错误,避免无限等待
            return fetch(e.request).catch(() => new Response('Offline', {status: 503}));
        })
    );
});

调试提示:部署后可以打开DevTools的Network面板,勾选Offline选项模拟离线环境,查看所有资源的请求状态,如果有资源返回缓存失败,优先检查路径是否正确、资源是否真的写入了Cache Storage。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.01 12:18:24