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()阶段直接报错,资源根本没有写入缓存。
修复方案
- 调整缓存匹配逻辑,匹配资源时忽略URL查询参数,只对比路径部分
- 新增Range请求特殊处理逻辑:检测到Range请求时,从缓存读取全量资源后手动切割对应字节段,构造符合规范的206响应返回
- 补全Service Worker安装、激活阶段的接管逻辑,新增旧版本缓存清理逻辑
- 安装阶段可以打开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
相关产品推荐
相关产品推荐

