Cache Storage API
การเปิด cache
หัวข้อที่มีชื่อว่า “การเปิด cache”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 เดียวกัน
Precaching ตอน install
หัวข้อที่มีชื่อว่า “Precaching ตอน install”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 ไม่ได้
cache.put และ caches.match
หัวข้อที่มีชื่อว่า “cache.put และ caches.match”สำหรับ runtime caching คุณเพิ่ม response แต่ละตัวด้วย cache.put(request, response) คุณดึงออกมาได้ด้วย cache.match(request) (ค้นเฉพาะ cache ตัวนั้น ๆ) หรือ caches.match(request) (ค้นทุก cache แล้วคืนค่าตัวที่ตรงตัวแรก)
// Runtime cache: intercept a fetch, cache the response, return itself.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 cachescaches.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.clone()
หัวข้อที่มีชื่อว่า “response.clone()”ออบเจ็กต์ 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});การลบ cache เก่าตอน activate
หัวข้อที่มีชื่อว่า “การลบ cache เก่าตอน activate”เมื่อคุณ 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 หน้าเต็ม ๆ
เดโมแบบโต้ตอบ
หัวข้อที่มีชื่อว่า “เดโมแบบโต้ตอบ”ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ตัวเลือก | Benefit | Cost |
|---|---|---|
| จัดการ 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 ตั้งแต่ครั้งแรกที่เปิด