fix(pwa): recover CORS image cache reads (#627)

This commit is contained in:
InfinityPacer
2026-08-03 10:07:00 +08:00
committed by GitHub
parent aa63782645
commit 5ca6c520ae
3 changed files with 78 additions and 1 deletions

View File

@@ -4,6 +4,7 @@ import { CacheFirst, NetworkFirst, StaleWhileRevalidate } from 'workbox-strategi
import { ExpirationPlugin } from 'workbox-expiration'
import { CacheableResponsePlugin } from 'workbox-cacheable-response'
import * as navigationPreload from 'workbox-navigation-preload'
import { corsSafeCachePlugin } from '@/utils/serviceWorkerCache'
// Service Worker 类型声明
declare let self: ServiceWorkerGlobalScope & {
@@ -97,6 +98,7 @@ registerRoute(
new CacheFirst({
cacheName: `image-cache-${RESOURCE_VERSION}`,
plugins: [
corsSafeCachePlugin,
new CacheableResponsePlugin({
statuses: [0, 200],
}),
@@ -131,6 +133,7 @@ registerRoute(
new CacheFirst({
cacheName: `tmdb-image-cache-${RESOURCE_VERSION}`,
plugins: [
corsSafeCachePlugin,
new CacheableResponsePlugin({
statuses: [0, 200],
}),
@@ -157,7 +160,7 @@ registerRoute(
!url.pathname.includes('/api/v1/mfa/') && // 多因素认证接口
!url.pathname.includes('/api/v1/auth/') && // 登录认证入口与票据交换
!url.pathname.includes('/api/v1/dashboard/') && // Dashboard实时监控数据
!url.pathname.includes('/api/v1/plugin/')&& // 插件接口
!url.pathname.includes('/api/v1/plugin/') && // 插件接口
!url.pathname.includes('/api/v1/subscribe/'), // 订阅接口
new NetworkFirst({
cacheName: `api-cache-${CACHE_VERSION}`,

View File

@@ -0,0 +1,53 @@
import { describe, expect, it } from 'vitest'
import { corsSafeCachePlugin, selectCorsSafeCachedResponse } from '../serviceWorkerCache'
function createResponse(type: ResponseType) {
const response = new Response('image')
Object.defineProperty(response, 'type', { value: type })
return response
}
describe('Service Worker cache CORS boundary', () => {
it('rejects an opaque cache entry for a CORS image request', () => {
const request = new Request('https://image.example/wallpaper.jpg', { mode: 'cors' })
const cachedResponse = createResponse('opaque')
expect(selectCorsSafeCachedResponse(request, cachedResponse)).toBeUndefined()
})
it('keeps a CORS-clean cache entry for a CORS image request', () => {
const request = new Request('https://image.example/wallpaper.jpg', { mode: 'cors' })
const cachedResponse = createResponse('cors')
expect(selectCorsSafeCachedResponse(request, cachedResponse)).toBe(cachedResponse)
})
it('keeps an opaque cache entry for an ordinary no-cors image request', () => {
const request = new Request('https://image.example/poster.jpg', { mode: 'no-cors' })
const cachedResponse = createResponse('opaque')
expect(selectCorsSafeCachedResponse(request, cachedResponse)).toBe(cachedResponse)
})
it('returns a cache miss unchanged so CacheFirst can use its network and offline policy', () => {
const request = new Request('https://image.example/wallpaper.jpg', { mode: 'cors' })
expect(selectCorsSafeCachedResponse(request, undefined)).toBeUndefined()
})
it('exposes the boundary through the Workbox cached response lifecycle', async () => {
const request = new Request('https://image.example/wallpaper.jpg', { mode: 'cors' })
const cachedResponse = createResponse('opaque')
await expect(
corsSafeCachePlugin.cachedResponseWillBeUsed?.({
cacheName: 'image-cache',
cachedResponse,
event: new Event('fetch') as ExtendableEvent,
matchOptions: {},
request,
}),
).resolves.toBeUndefined()
})
})

View File

@@ -0,0 +1,21 @@
import type { WorkboxPlugin } from 'workbox-core'
/**
* CORS 请求不得采用 opaque 响应,否则浏览器无法读取其内容。
* 普通 no-cors 请求仍保留 opaque 缓存能力;缓存未命中时继续交给策略决定网络或离线回退。
*/
export function selectCorsSafeCachedResponse(
request: Request,
cachedResponse: Response | undefined,
): Response | undefined {
if (request.mode === 'cors' && cachedResponse?.type === 'opaque') return undefined
return cachedResponse
}
/** 在 Workbox 采用缓存前执行响应的 CORS 可读性边界。 */
export const corsSafeCachePlugin: WorkboxPlugin = {
async cachedResponseWillBeUsed({ cachedResponse, request }) {
return selectCorsSafeCachedResponse(request, cachedResponse)
},
}