Oyun varlıklarını önbelleğe almak için service worker'lar
Service worker'lar, varlıkların nasıl önbelleğe alınacağını ve sunulacağını kontrol etmenizi sağlar. Oyunlar için bu, tekrar ziyaretlerde anında yükleme ve çevrimdışı destek anlamına gelir.
1) Service worker temelleri
Kök dizininizde sw.js dosyasını oluşturun:
// sw.js
self.addEventListener('install', (event) => {
console.log('Service worker installing')
})
self.addEventListener('activate', (event) => {
console.log('Service worker activated')
})
self.addEventListener('fetch', (event) => {
// Intercept all network requests
})Kaydedin:
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then(reg => console.log('SW registered'))
.catch(err => console.error('SW registration failed:', err))
}2) Önce önbellek stratejisi (oyun varlıkları için en iyisi)
Önce önbellekten yükleyin, yalnızca önbellekte yoksa getirin:
const CACHE_NAME = 'game-assets-v1'
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((cached) => {
if (cached) {
return cached
}
return fetch(event.request).then((response) => {
// Don't cache non-ok responses or non-GET requests
if (!response.ok || event.request.method !== 'GET') {
return response
}
// Clone response for caching
const clone = response.clone()
caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, clone)
})
return response
})
})
)
})3) Kritik varlıkları önceden önbelleğe alma
Install olayı, önceden önbelleğe alma için idealdir:
const CACHE_NAME = 'game-v1'
const PRECACHE = [
'/',
'/index.html',
'/game.js',
'/style.css',
'/assets/sprites.png',
'/assets/tileset.png',
'/assets/player.png',
]
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => cache.addAll(PRECACHE))
.then(() => self.skipWaiting())
)
})4) Dinamik içerik için önce ağ stratejisi
Değişen içerikler (liderlik tabloları, oyuncu verileri) için önce ağ stratejisini kullanın:
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url)
// API calls: network first
if (url.pathname.startsWith('/api/')) {
event.respondWith(networkFirst(event.request))
return
}
// Assets: cache first
event.respondWith(cacheFirst(event.request))
})
async function networkFirst(request) {
try {
const response = await fetch(request)
const cache = await caches.open('api-cache')
cache.put(request, response.clone())
return response
} catch {
return caches.match(request)
}
}
async function cacheFirst(request) {
const cached = await caches.match(request)
if (cached) return cached
const response = await fetch(request)
const cache = await caches.open(CACHE_NAME)
cache.put(request, response.clone())
return response
}5) Eski içeriği sunup arka planda yenileme
Önbellekteki içeriği hemen döndürün, arka planda güncelleyin:
async function staleWhileRevalidate(request) {
const cache = await caches.open(CACHE_NAME)
const cached = await cache.match(request)
const fetchPromise = fetch(request).then((response) => {
if (response.ok) {
cache.put(request, response.clone())
}
return response
})
return cached || fetchPromise
}6) Önbellek sürümleme ve temizleme
Activate sırasında eski önbellekleri temizleyin:
const CACHE_VERSION = 'v2'
const CACHE_NAME = `game-${CACHE_VERSION}`
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((keys) => {
return Promise.all(
keys
.filter(key => key.startsWith('game-') && key !== CACHE_NAME)
.map(key => {
console.log('Deleting old cache:', key)
return caches.delete(key)
})
)
}).then(() => self.clients.claim())
)
})7) Önbellekleri türe göre ayırma
Daha iyi yönetim için önbellekleri düzenleyin:
const CACHES = {
static: 'static-v1', // HTML, JS, CSS
images: 'images-v1', // Sprites, textures
audio: 'audio-v1', // Music, SFX
api: 'api-v1', // API responses
}
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url)
if (url.pathname.match(/\.(png|jpg|webp)$/)) {
event.respondWith(cacheFirst(event.request, CACHES.images))
} else if (url.pathname.match(/\.(mp3|ogg|wav)$/)) {
event.respondWith(cacheFirst(event.request, CACHES.audio))
} else if (url.pathname.startsWith('/api/')) {
event.respondWith(networkFirst(event.request, CACHES.api))
} else {
event.respondWith(cacheFirst(event.request, CACHES.static))
}
})8) Ses/video için aralık istekleri
Baştan bilmeniz gereken bir nokta var: Cache API, kısmi yanıtları saklamaz. Bir 206 Partial Content yanıtı cache.put() işlevine ulaşırsa TypeError: Failed to execute 'put' on 'Cache': Partial response (status code 206) is unsupported hatasını verir. Bu nedenle önbelleğe alınmış medya için kalıp her zaman aynıdır: dosyanın tamamını normal bir 200 yanıtı olarak önceden önbelleğe alın (veya getirin), ardından Range başlığı içeren bir istek geldiğinde önbellekteki baytları dilimleyerek 206 yanıtını kendiniz oluşturun. Bunu kendiniz uygulamak istemiyorsanız Workbox'ın range-requests eklentisi, CacheFirst stratejisiyle eşleştirildiğinde bu dilimleme işlemini sizin için yapar.
Aralık isteklerini doğru şekilde işleyin (ses içinde ileri veya geri sarma için gereklidir):
self.addEventListener('fetch', (event) => {
if (event.request.headers.has('range')) {
event.respondWith(handleRangeRequest(event.request))
return
}
// ... normal handling
})
async function handleRangeRequest(request) {
const cache = await caches.open(CACHES.audio)
// Match the full resource, ignoring the Range header (which would never match a cached full file)
let cached = await cache.match(request.url)
if (!cached) {
// Fetch the FULL file (string URL drops the Range header) and cache it as a 200
cached = await fetch(request.url)
if (cached.ok) cache.put(request.url, cached.clone()) // cache.put rejects 206, so only store full 200s
}
// Build a 206 Partial Content response from the cached full body
const rangeHeader = request.headers.get('range') // e.g. "bytes=200-1000"
const buffer = await cached.arrayBuffer()
const total = buffer.byteLength
const [startStr, endStr] = rangeHeader.replace(/bytes=/, '').split('-')
const start = Number(startStr)
const end = endStr ? Number(endStr) : total - 1
const slice = buffer.slice(start, end + 1)
return new Response(slice, {
status: 206,
statusText: 'Partial Content',
headers: {
'Content-Type': cached.headers.get('Content-Type') || 'application/octet-stream',
'Content-Range': `bytes ${start}-${end}/${total}`,
'Content-Length': String(slice.byteLength),
'Accept-Ranges': 'bytes',
},
})
}9) Güncelleme bildirimi
Yeni bir sürüm hazır olduğunda oyunculara bildirin:
// In main app
let refreshing = false
navigator.serviceWorker.addEventListener('controllerchange', () => {
if (refreshing) return
refreshing = true
showUpdateBanner()
})
function showUpdateBanner() {
const banner = document.createElement('div')
banner.className = 'update-banner'
banner.innerHTML = `
<span>Yeni sürüm hazır!</span>
<button onclick="location.reload()">Güncelle</button>
`
document.body.appendChild(banner)
}10) Eksiksiz oyun service worker'ı
const VERSION = 'v1'
const STATIC_CACHE = `static-${VERSION}`
const ASSET_CACHE = `assets-${VERSION}`
const PRECACHE_URLS = [
'/',
'/index.html',
'/game.js',
'/style.css',
]
// Install: precache critical resources
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(STATIC_CACHE)
.then(cache => cache.addAll(PRECACHE_URLS))
.then(() => self.skipWaiting())
)
})
// Activate: clean old caches
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then(keys => Promise.all(
keys
.filter(key => !key.endsWith(VERSION))
.map(key => caches.delete(key))
)).then(() => self.clients.claim())
)
})
// Fetch: route requests to appropriate strategy
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url)
// Same-origin only
if (url.origin !== location.origin) {
return
}
// API: network first with cache fallback
if (url.pathname.startsWith('/api/')) {
event.respondWith(networkFirst(event.request, 'api-cache'))
return
}
// Assets: cache first
if (url.pathname.startsWith('/assets/')) {
event.respondWith(cacheFirst(event.request, ASSET_CACHE))
return
}
// HTML/JS/CSS: stale while revalidate
event.respondWith(staleWhileRevalidate(event.request, STATIC_CACHE))
})
async function cacheFirst(request, cacheName) {
const cached = await caches.match(request)
if (cached) return cached
try {
const response = await fetch(request)
if (response.ok) {
const cache = await caches.open(cacheName)
cache.put(request, response.clone())
}
return response
} catch {
return new Response('Çevrimdışı', { status: 503 })
}
}
async function networkFirst(request, cacheName) {
try {
const response = await fetch(request)
if (response.ok) {
const cache = await caches.open(cacheName)
cache.put(request, response.clone())
}
return response
} catch {
return caches.match(request) || new Response('Çevrimdışı', { status: 503 })
}
}
async function staleWhileRevalidate(request, cacheName) {
const cache = await caches.open(cacheName)
const cached = await cache.match(request)
const fetchPromise = fetch(request).then(response => {
if (response.ok) {
cache.put(request, response.clone())
}
return response
}).catch(() => cached)
return cached || fetchPromise
}İlgili içerikler
- Çevrimdışı oyunlar için PWA
- Varlıkları akışla yükleme
- Hızlı yüklenen bir web oyunu yayınlama
- Oyunlar için Web Audio API — çevrimdışı oyun için ses varlıklarını önbelleğe alma
- IndexedDB ile oyun kayıtları — oyun durumunu önbelleğe alınmış varlıklarla birlikte kalıcı olarak saklama
Harici Kaynaklar
- MDN: Service Worker API — eksiksiz API referansı
- MDN: Cache API — Cache arayüzü referansı
- web.dev: Service worker'lar — önbelleğe alma stratejileri ve kalıpları
- Workbox — Google'ın service worker araçları için geliştirdiği kütüphane