Skip to content

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:

js
// 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:

js
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:

js
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:

js
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:

js
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:

js
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:

js
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:

js
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):

js
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:

js
// 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'ı

js
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

Harici Kaynaklar