mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-07 00:36:41 +08:00
fix(pwa): recover CORS image cache reads (#627)
This commit is contained in:
@@ -4,6 +4,7 @@ import { CacheFirst, NetworkFirst, StaleWhileRevalidate } from 'workbox-strategi
|
|||||||
import { ExpirationPlugin } from 'workbox-expiration'
|
import { ExpirationPlugin } from 'workbox-expiration'
|
||||||
import { CacheableResponsePlugin } from 'workbox-cacheable-response'
|
import { CacheableResponsePlugin } from 'workbox-cacheable-response'
|
||||||
import * as navigationPreload from 'workbox-navigation-preload'
|
import * as navigationPreload from 'workbox-navigation-preload'
|
||||||
|
import { corsSafeCachePlugin } from '@/utils/serviceWorkerCache'
|
||||||
|
|
||||||
// Service Worker 类型声明
|
// Service Worker 类型声明
|
||||||
declare let self: ServiceWorkerGlobalScope & {
|
declare let self: ServiceWorkerGlobalScope & {
|
||||||
@@ -97,6 +98,7 @@ registerRoute(
|
|||||||
new CacheFirst({
|
new CacheFirst({
|
||||||
cacheName: `image-cache-${RESOURCE_VERSION}`,
|
cacheName: `image-cache-${RESOURCE_VERSION}`,
|
||||||
plugins: [
|
plugins: [
|
||||||
|
corsSafeCachePlugin,
|
||||||
new CacheableResponsePlugin({
|
new CacheableResponsePlugin({
|
||||||
statuses: [0, 200],
|
statuses: [0, 200],
|
||||||
}),
|
}),
|
||||||
@@ -131,6 +133,7 @@ registerRoute(
|
|||||||
new CacheFirst({
|
new CacheFirst({
|
||||||
cacheName: `tmdb-image-cache-${RESOURCE_VERSION}`,
|
cacheName: `tmdb-image-cache-${RESOURCE_VERSION}`,
|
||||||
plugins: [
|
plugins: [
|
||||||
|
corsSafeCachePlugin,
|
||||||
new CacheableResponsePlugin({
|
new CacheableResponsePlugin({
|
||||||
statuses: [0, 200],
|
statuses: [0, 200],
|
||||||
}),
|
}),
|
||||||
@@ -157,7 +160,7 @@ registerRoute(
|
|||||||
!url.pathname.includes('/api/v1/mfa/') && // 多因素认证接口
|
!url.pathname.includes('/api/v1/mfa/') && // 多因素认证接口
|
||||||
!url.pathname.includes('/api/v1/auth/') && // 登录认证入口与票据交换
|
!url.pathname.includes('/api/v1/auth/') && // 登录认证入口与票据交换
|
||||||
!url.pathname.includes('/api/v1/dashboard/') && // Dashboard实时监控数据
|
!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/'), // 订阅接口
|
!url.pathname.includes('/api/v1/subscribe/'), // 订阅接口
|
||||||
new NetworkFirst({
|
new NetworkFirst({
|
||||||
cacheName: `api-cache-${CACHE_VERSION}`,
|
cacheName: `api-cache-${CACHE_VERSION}`,
|
||||||
|
|||||||
@@ -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()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -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)
|
||||||
|
},
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user