乐闻世界logo
搜索文章和话题

How does Service Worker cache cross-origin resources?

3月7日 12:05

Service Worker Cross-Origin Resource Caching Explained

Cross-origin resource caching is a common issue in Service Worker. Due to the browser's Same-Origin Policy, cross-origin request caching requires special handling.

Cross-Origin Request Basic Concepts

What is Cross-Origin Request

javascript
// Same-Origin requests // Current page: https://example.com/page.html fetch('/api/data'); // ✅ Same origin fetch('https://example.com/api'); // ✅ Same origin // Cross-Origin requests fetch('https://api.other.com/data'); // ❌ Cross-origin (different domain) fetch('https://cdn.example.com/file'); // ❌ Cross-origin (different subdomain)

CORS and Opaque Response

javascript
// CORS request - server allows cross-origin fetch('https://api.example.com/data', { mode: 'cors' // Default mode }); // Response contains full headers and body // No-CORS request - server doesn't allow cross-origin fetch('https://cdn.example.com/image.jpg', { mode: 'no-cors' }); // Response is "opaque", cannot read content, but can be cached

Cross-Origin Resource Caching Solutions

Solution 1: Use CORS Mode (Recommended)

javascript
// sw.js // Server needs to set CORS header: Access-Control-Allow-Origin: * self.addEventListener('fetch', event => { const url = new URL(event.request.url); // Handle cross-origin API requests if (url.hostname === 'api.example.com') { event.respondWith( caches.match(event.request).then(response => { if (response) { return response; } // Use cors mode request return fetch(event.request, { mode: 'cors' }) .then(networkResponse => { // Check if response is successful if (!networkResponse || networkResponse.status !== 200) { return networkResponse; } // Cache response const responseToCache = networkResponse.clone(); caches.open('api-cache').then(cache => { cache.put(event.request, responseToCache); }); return networkResponse; }) .catch(error => { console.error('Cross-origin request failed:', error); // Return offline fallback content return new Response(JSON.stringify({ error: 'Network error', offline: true }), { headers: { 'Content-Type': 'application/json' } }); }); }) ); } });

Solution 2: Use No-CORS Mode to Cache Opaque Responses

javascript
// sw.js // Suitable for CDN resources, images that don't need content reading self.addEventListener('fetch', event => { const url = new URL(event.request.url); // Handle CDN image resources if (url.hostname === 'cdn.example.com') { event.respondWith( caches.match(event.request).then(response => { if (response) { return response; } // Use no-cors mode return fetch(event.request, { mode: 'no-cors' }) .then(networkResponse => { // Opaque responses can be cached but cannot read status and content const responseToCache = networkResponse.clone(); caches.open('cdn-cache').then(cache => { cache.put(event.request, responseToCache); }); return networkResponse; }); }) ); } });

Characteristics of Opaque Responses:

  • Status code is always 0
  • Cannot read response headers
  • Cannot read response body
  • Occupies more cache quota (about 7x actual size)

Solution 3: Use CORS Proxy

javascript
// If third-party API doesn't support CORS, can set up proxy server // sw.js self.addEventListener('fetch', event => { const url = new URL(event.request.url); // Proxy cross-origin requests if (url.pathname.startsWith('/proxy/')) { const targetUrl = url.pathname.replace('/proxy/', ''); event.respondWith( caches.match(event.request).then(response => { if (response) { return response; } // Forward request through same-origin proxy return fetch(`/api/proxy?url=${encodeURIComponent(targetUrl)}`) .then(networkResponse => { const responseToCache = networkResponse.clone(); caches.open('proxy-cache').then(cache => { cache.put(event.request, responseToCache); }); return networkResponse; }); }) ); } });

Solution 4: Precache Cross-Origin Resources

javascript
// sw.js const CROSS_ORIGIN_CACHE = 'cross-origin-v1'; // Cross-origin resources to precache const crossOriginResources = [ 'https://fonts.googleapis.com/css?family=Roboto', 'https://fonts.gstatic.com/s/roboto/v20/font.woff2', 'https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js' ]; self.addEventListener('install', event => { event.waitUntil( caches.open(CROSS_ORIGIN_CACHE).then(cache => { // Use addAll to precache (automatically handles CORS) return cache.addAll(crossOriginResources); }) ); }); self.addEventListener('fetch', event => { const url = new URL(event.request.url); // Match cross-origin resources if (crossOriginResources.some(resource => event.request.url.includes(resource))) { event.respondWith( caches.match(event.request).then(response => { return response || fetch(event.request); }) ); } });

Practical Application Examples

Example 1: Cache Google Fonts

javascript
// sw.js const FONT_CACHE = 'fonts-v1'; self.addEventListener('fetch', event => { const url = new URL(event.request.url); // Google Fonts CSS (CORS supported) if (url.hostname === 'fonts.googleapis.com') { event.respondWith( caches.match(event.request).then(response => { return response || fetch(event.request).then(fetchResponse => { return caches.open(FONT_CACHE).then(cache => { cache.put(event.request, fetchResponse.clone()); return fetchResponse; }); }); }) ); } // Google Fonts font files (Opaque responses) if (url.hostname === 'fonts.gstatic.com') { event.respondWith( caches.match(event.request).then(response => { return response || fetch(event.request, { mode: 'no-cors' }) .then(fetchResponse => { return caches.open(FONT_CACHE).then(cache => { cache.put(event.request, fetchResponse.clone()); return fetchResponse; }); }); }) ); } });

Example 2: Cache CDN Resources

javascript
// sw.js const CDN_CACHE = 'cdn-v1'; const CDN_HOSTS = [ 'cdn.jsdelivr.net', 'unpkg.com', 'cdnjs.cloudflare.com' ]; self.addEventListener('fetch', event => { const url = new URL(event.request.url); // Check if it's a CDN resource if (CDN_HOSTS.includes(url.hostname)) { event.respondWith( caches.match(event.request).then(response => { if (response) { return response; } // CDN resources use no-cors mode return fetch(event.request, { mode: 'no-cors' }) .then(fetchResponse => { // Only cache successful responses if (fetchResponse.type === 'opaque' || fetchResponse.ok) { const responseToCache = fetchResponse.clone(); caches.open(CDN_CACHE).then(cache => { cache.put(event.request, responseToCache); }); } return fetchResponse; }) .catch(() => { // Return fallback when offline return new Response('CDN resource unavailable offline'); }); }) ); } });

Example 3: Handle Cross-Origin Images

javascript
// sw.js const IMAGE_CACHE = 'images-v1'; self.addEventListener('fetch', event => { const request = event.request; // Handle image requests if (request.destination === 'image') { event.respondWith( caches.match(request).then(response => { if (response) { return response; } // Determine if it's a cross-origin request const url = new URL(request.url); const isCrossOrigin = url.origin !== location.origin; const fetchOptions = isCrossOrigin ? { mode: 'no-cors' } : {}; return fetch(request, fetchOptions).then(fetchResponse => { // Check if response is valid if (!fetchResponse || fetchResponse.status === 0) { // Opaque response or failure return fetchResponse; } const responseToCache = fetchResponse.clone(); caches.open(IMAGE_CACHE).then(cache => { cache.put(request, responseToCache); }); return fetchResponse; }).catch(() => { // Return placeholder image return caches.match('/images/placeholder.png'); }); }) ); } });

Cross-Origin Caching Notes

1. Opaque Response Storage Cost

javascript
// Opaque response storage cost is about 7x actual size // Need to use carefully to avoid exceeding storage quota async function checkOpaqueResponseSize() { const cache = await caches.open('cdn-cache'); const requests = await cache.keys(); let totalSize = 0; for (const request of requests) { const response = await cache.match(request); if (response.type === 'opaque') { // Opaque responses cannot get actual size // Need to estimate or limit quantity totalSize += 1024 * 1024; // Assume 1MB each } } console.log(`Estimated opaque response total size: ${totalSize / 1024 / 1024} MB`); }

2. Cache Cleanup Strategy

javascript
// Periodically clean cross-origin cache async function cleanCrossOriginCache() { const cache = await caches.open('cdn-cache'); const requests = await cache.keys(); // Only keep last 50 accessed resources if (requests.length > 50) { const toDelete = requests.slice(0, requests.length - 50); await Promise.all( toDelete.map(request => cache.delete(request)) ); } } // Execute cleanup in activate event self.addEventListener('activate', event => { event.waitUntil(cleanCrossOriginCache()); });

3. Error Handling

javascript
// Cross-origin request error handling async function fetchWithFallback(request, isCrossOrigin) { try { const fetchOptions = isCrossOrigin ? { mode: 'no-cors' } : {}; const response = await fetch(request, fetchOptions); // Opaque responses have status 0, need special handling if (response.type === 'opaque' || response.ok) { return response; } throw new Error(`HTTP ${response.status}`); } catch (error) { console.error('Fetch failed:', error); // Return cache or fallback response const cachedResponse = await caches.match(request); if (cachedResponse) { return cachedResponse; } // Return default response return new Response('Resource unavailable', { status: 503 }); } }

Best Practices

  1. Prioritize CORS: If server supports CORS, prefer cors mode
  2. Limit Opaque Responses: Opaque responses occupy more storage space
  3. Set Reasonable Cache Strategy: Cross-origin resources that change frequently need expiration time
  4. Provide Fallback Solution: Provide alternative content when cross-origin resources fail to load
  5. Monitor Storage Usage: Regularly check cache size to avoid exceeding quota
标签:Service Worker