ข้ามไปยังเนื้อหา

Cache Storage API

Cache Storage API ถูกเปิดให้ใช้ผ่าน global ชื่อ caches ภายในบริบทของ service worker ในการเปิด (หรือสร้าง) cache ที่มีชื่อ ให้เรียก caches.open('cache-v1') ซึ่งจะคืนค่าเป็น Promise<Cache> cache ถูกระบุด้วยชื่อที่เป็น string ล้วน ๆ — ไม่มี schema ไม่มี expiry และไม่มีการ evict อัตโนมัติ คุณเป็นเจ้าของ lifecycle ทั้งหมด

self.addEventListener('install', (event) => {
event.waitUntil(
caches.open('cache-v1').then((cache) => {
console.log('Cache opened:', cache);
})
);
});

cache หลายตัวสามารถอยู่ร่วมกันได้ รูปแบบที่นิยมคือเก็บ cache หนึ่งตัวต่อหนึ่งเวอร์ชันของแอป เพื่อให้ SW ตัวเก่ากับ SW ตัวใหม่ไม่ใช้ cache namespace เดียวกัน

cache.addAll(urls) รับ array ของ URL string เข้ามา แล้วดึงและเก็บทั้งหมดแบบ atomic หาก request ใดเพียงตัวเดียวล้มเหลว — network error, 404, หรืออะไรก็ตาม — การเรียก addAll ทั้งหมดจะ reject และ install event จะล้มเหลว นี่เป็นพฤติกรรมที่ตั้งใจ เพื่อรับประกันว่า app shell ของคุณจะถูกแคชครบทั้งหมดหรือไม่ก็ไม่ถูกแคชเลย

const CACHE_NAME = 'cache-v1';
const APP_SHELL = [
'/',
'/index.html',
'/app.js',
'/style.css',
];
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => cache.addAll(APP_SHELL))
);
});

รักษ list ของ precache ให้สั้นเข้าไว้ ทุก URL ใน list คือ network request ที่บังคับว่าต้องสำเร็จตอน install — เพียง 404 ตัวเดียวก็จะทำให้ SW activate ไม่ได้

สำหรับ runtime caching คุณเพิ่ม response แต่ละตัวด้วย cache.put(request, response) คุณดึงออกมาได้ด้วย cache.match(request) (ค้นเฉพาะ cache ตัวนั้น ๆ) หรือ caches.match(request) (ค้นทุก cache แล้วคืนค่าตัวที่ตรงตัวแรก)

// Runtime cache: intercept a fetch, cache the response, return it
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((cached) => {
if (cached) return cached;
return fetch(event.request).then((response) => {
// Store a clone; return the original
caches.open('cache-v1').then((cache) => {
cache.put(event.request, response.clone());
});
return response;
});
})
);
});
// Searching a specific named cache vs. all caches
caches.open('cache-v1').then((cache) => {
cache.match('/app.js').then((res) => {
console.log('From named cache:', res);
});
});
caches.match('/app.js').then((res) => {
console.log('From any cache:', res);
});

ออบเจ็กต์ Response ห่อหุ้ม body ที่เป็น readable stream แบบใช้ได้ครั้งเดียว เมื่อคุณ consume body — ด้วยการเรียก .json(), .text(), หรือส่งต่อให้ cache.put() — stream นั้นจะถูกดึงจนหมดและ body ก็หายไป การอ่านครั้งถัดไปจะได้ผลลัพธ์ว่างเปล่า

เมื่อคุณต้องการทั้งแคช response และคืนค่ากลับไปให้ผู้เรียก คุณต้อง clone ก่อน response.clone() สร้าง Response ตัวที่สองที่แชร์ข้อมูล body เดียวกันแต่มี read cursor เป็นอิสระต่อกัน

fetch(event.request).then((response) => {
const clone = response.clone(); // independent copy
cache.put(event.request, clone); // cache the clone
return response; // return the original to the page
});

เมื่อคุณ deploy SW เวอร์ชันใหม่ที่ใช้ชื่อ cache ใหม่ cache เก่าจะยังคงอยู่บนดิสก์จนกว่าคุณจะลบออกอย่างชัดเจน activate event คือที่ที่เหมาะสำหรับการทำสิ่งนี้ เพราะ SW ตัวใหม่ได้เข้าควบคุมแล้ว จึงปลอดภัยที่จะลบ cache ที่ SW ตัวเก่าเคยพึ่งพา

const CACHE_VERSION = 'v1';
const CURRENT_CACHES = new Set(['cache-v1', 'images-v1']);
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames
.filter((name) => !CURRENT_CACHES.has(name))
.map((name) => {
console.log('Deleting old cache:', name);
return caches.delete(name);
})
);
})
);
});

หลังจากเคลียร์เสร็จ ให้เรียก self.clients.claim() หากคุณต้องการให้ SW ที่เพิ่ง activate เข้าควบคุมหน้าเพจที่เปิดอยู่ทันที แทนที่จะรอให้มีการ reload หน้าเต็ม ๆ

Runs a real service worker + manifest in your browser.
ตัวเลือกBenefitCost
จัดการ Cache Storage API ตรงๆ ด้วยมือควบคุม logic ได้ทุกรายละเอียด ไม่ผูกกับ libraryต้องเขียนและ maintain boilerplate เยอะ เสี่ยง bug จากการ handle edge case เอง
ใช้ library ครอบ (เช่น Workbox)โค้ดสั้นลง ลด boilerplate จัดการ cache versioning ให้เพิ่ม dependency และเรียนรู้ API ของ library เพิ่ม
  • ไม่ตั้งชื่อ cache แบบ versioned (เช่น app-v1) ทำให้ลบ cache เก่าตอน activate ไม่ได้อย่างปลอดภัย
  • ลืม cache.delete() cache เก่าตอน activate event ทำให้ storage โตขึ้นเรื่อยๆ ไม่มีที่สิ้นสุด
  • cache response ที่เป็น opaque (cross-origin แบบไม่มี CORS) โดยไม่รู้ตัวว่ากิน quota เท่ากับไฟล์เต็มแม้จะอ่าน body ไม่ได้

💡 ตัวอย่างจากของจริง

Wikipedia — ใช้ Cache Storage API เก็บบทความที่เคยอ่านไว้ ทำให้เปิดซ้ำได้แบบ offline โดยไม่ต้องโหลดใหม่

Squoosh — cache ทุก asset ของแอปไว้ตั้งแต่ install ทำให้ใช้งานได้แบบ offline-first ตั้งแต่ครั้งแรกที่เปิด

เมธอดใดของ Cache Storage ที่เก็บ array ของ URL แบบ atomic ตอน install time?
ทำไมคุณจึงต้อง clone Response ก่อนส่งให้ cache.put()?
caches.match() ทำงานต่างจาก cache.match() อย่างไร?
ควรลบ cache เก่าเมื่อใด?