การเก็บข้อมูล Offline ด้วย IndexedDB
การเก็บข้อมูล offline ด้วย IndexedDB
หัวข้อที่มีชื่อว่า “การเก็บข้อมูล offline ด้วย IndexedDB”browser มีกลไก client-side storage หลักสามแบบ แต่ละแบบออกแบบสำหรับงานที่แตกต่างกัน การเลือกแบบที่ถูกต้องจะป้องกัน bugs ที่ลึกซึ้งและทำให้ประสบการณ์ offline ของคุณเชื่อถือได้
การเปรียบเทียบ storage
หัวข้อที่มีชื่อว่า “การเปรียบเทียบ storage”| localStorage | Cache Storage | IndexedDB | |
|---|---|---|---|
| เก็บ | Key/value strings | HTTP request/response pairs | Structured objects (ค่า JS ใดก็ได้) |
| Async? | ไม่ (synchronous, บล็อก main thread) | ใช่ (Promise-based) | ใช่ (event-based / Promise wrappers) |
| Query ได้? | ไม่ | ตาม URL เท่านั้น | ใช่ — indexes, key ranges, cursors |
| ขีดจำกัดขนาด | ~5 MB | Quota-based (~หลายร้อย MB) | Quota-based (~หลายร้อย MB) |
| ใช้ใน SW ได้? | ไม่ | ใช่ | ใช่ |
| เหมาะสำหรับ | การตั้งค่าขนาดเล็ก / flags | Cache network resources | ข้อมูลแอป: tasks, messages, user records |
หลักการง่าย ๆ: ใช้ Cache Storage สำหรับ network assets ที่ service worker cache (HTML, CSS, JS, รูปภาพ) และใช้ IndexedDB สำหรับข้อมูลจริงที่แอปของคุณทำงานด้วย
Raw IndexedDB API
หัวข้อที่มีชื่อว่า “Raw IndexedDB API”IndexedDB เป็น transactional, object-oriented database ที่ built-in ในทุก browser สมัยใหม่ โดยเก็บ JavaScript objects ใน named object stores ที่ระบุด้วย key path ที่คุณเลือก
การเปิด database
หัวข้อที่มีชื่อว่า “การเปิด database”const request = indexedDB.open('my-app-db', 1);
request.onupgradeneeded = (event) => { const db = event.target.result; // Create an object store keyed on the 'id' property if (!db.objectStoreNames.contains('tasks')) { db.createObjectStore('tasks', { keyPath: 'id' }); }};
request.onsuccess = (event) => { const db = event.target.result; console.log('Database opened:', db.name);};
request.onerror = (event) => { console.error('Failed to open database:', event.target.error);};indexedDB.open(name, version) คืนค่า IDBOpenDBRequest callback onupgradeneeded จะ fire เมื่อใดก็ตามที่ version number เพิ่มขึ้น — นี่เป็นที่เดียวที่คุณสามารถสร้างหรือแก้ไข object stores และ indexes ได้
การเขียน record
หัวข้อที่มีชื่อว่า “การเขียน record”เมื่อมี db reference แล้ว ให้ wrap ทุก operation ในการ transaction ส่ง store name และ mode — 'readwrite' เพื่อเขียน, 'readonly' เพื่ออ่าน
function saveTask(db, task) { return new Promise((resolve, reject) => { const tx = db.transaction('tasks', 'readwrite'); const store = tx.objectStore('tasks'); const putRequest = store.put(task); // insert or replace by keyPath
putRequest.onsuccess = () => resolve(putRequest.result); putRequest.onerror = () => reject(putRequest.error); });}
// UsagesaveTask(db, { id: 'task-1', title: 'Buy groceries', done: false });store.put(record) แทรก object ถ้า key ไม่มีอยู่ หรือแทนที่ถ้ามีอยู่แล้ว ใช้ store.add(record) ถ้าต้องการ error เมื่อ key ซ้ำกันแทน
การอ่าน record
หัวข้อที่มีชื่อว่า “การอ่าน record”function getTask(db, id) { return new Promise((resolve, reject) => { const tx = db.transaction('tasks', 'readonly'); const store = tx.objectStore('tasks'); const getRequest = store.get(id);
getRequest.onsuccess = () => resolve(getRequest.result); // undefined if not found getRequest.onerror = () => reject(getRequest.error); });}
// UsagegetTask(db, 'task-1').then((task) => console.log(task));การวนผ่าน records ทั้งหมด
หัวข้อที่มีชื่อว่า “การวนผ่าน records ทั้งหมด”เพื่ออ่านทุก record ให้เปิด cursor บน store
function getAllTasks(db) { return new Promise((resolve, reject) => { const tx = db.transaction('tasks', 'readonly'); const store = tx.objectStore('tasks'); const results = []; const cursorRequest = store.openCursor();
cursorRequest.onsuccess = (event) => { const cursor = event.target.result; if (cursor) { results.push(cursor.value); cursor.continue(); } else { resolve(results); } };
cursorRequest.onerror = () => reject(cursorRequest.error); });}Versioning และ migrations
หัวข้อที่มีชื่อว่า “Versioning และ migrations”argument ที่สองของ indexedDB.open() คือ schema version ทุกครั้งที่คุณเพิ่มเลขเวอร์ชัน onupgradeneeded จะ fire โดยมี event.oldVersion และ event.newVersion พร้อมใช้งาน ทำให้คุณสามารถ migrate schema แบบ incremental ได้
request.onupgradeneeded = (event) => { const db = event.target.result; const oldVersion = event.oldVersion;
if (oldVersion < 1) { db.createObjectStore('tasks', { keyPath: 'id' }); } if (oldVersion < 2) { // Version 2 adds a 'notes' store db.createObjectStore('notes', { keyPath: 'id' }); }};อย่าลบ store หรือ index โดยไม่ตรวจสอบ oldVersion — ผู้ใช้อาจ upgrade จาก version ก่อนหน้าใด ๆ ไม่ใช่เฉพาะ version ก่อนหน้าเท่านั้น
Practical wrappers
หัวข้อที่มีชื่อว่า “Practical wrappers”Raw event-based API มีความยืดยาว ใน production ทีมส่วนใหญ่ใช้ thin wrapper เช่น library idb (ขนาดไม่กี่ KB) ที่นำเสนอ API เดียวกันในรูปแบบ Promise chains แนวคิดเหมือนกัน — open, transaction, store, put/get — wrapper เพียงแค่ลด boilerplate event handlers
Data flow diagram
หัวข้อที่มีชื่อว่า “Data flow diagram”sequenceDiagram
participant App as Page / SW
participant IDB as IndexedDB
App->>IDB: indexedDB.open('my-app-db', 1)
IDB-->>App: onupgradeneeded (create stores)
IDB-->>App: onsuccess (db handle)
App->>IDB: db.transaction('tasks', 'readwrite')
App->>IDB: store.put(task)
IDB-->>App: onsuccess (key)
App->>IDB: store.get('task-1')
IDB-->>App: onsuccess (record) ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ตัวเลือก | Benefit | Cost |
|---|---|---|
| เก็บ mutation/data offline ใน IndexedDB | ทนทาน รอด service worker terminate และ browser restart ได้ | API ใช้งานซับซ้อนกว่า localStorage มาก ต้อง handle transaction/schema เอง |
| เก็บชั่วคราวใน memory ของ service worker | เขียนโค้ดง่ายกว่า เร็วกว่าในระยะสั้น | หายทันทีที่ service worker ถูก browser terminate (เกิดบ่อยเมื่อ idle) |
ข้อผิดพลาดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ข้อผิดพลาดที่พบบ่อย”- ทำ mutation ระหว่าง offline ไว้ใน variable ธรรมดาแทน IndexedDB ทำให้ข้อมูลหายเมื่อ service worker ถูก kill
- ไม่ handle onupgradeneeded ให้ครบ ทำให้ schema migration พังตอนอัปเดต version ของ database
- ไม่มี error handler บน transaction ทำให้ error เงียบหายไปโดยไม่มีใครรู้ว่า write ล้มเหลว
💡 ตัวอย่างจากของจริง
Google Docs offline — เก็บการแก้ไขเอกสารระหว่าง offline ไว้ใน IndexedDB แล้ว sync กลับ server ทันทีที่กลับมามีเน็ต
Gmail — cache อีเมลและ draft ไว้ใน IndexedDB ทำให้เปิดอ่าน/แก้ draft ได้แม้ไม่มีสัญญาณอินเทอร์เน็ต