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

Library idb: IndexedDB แบบ Promise

IndexedDB API ดิบนั้นขับเคลื่อนด้วย callback ทั้งหมด ทุก operation — ไม่ว่าจะเป็นการเปิด database การเขียน record หรือการอ่านข้อมูลกลับมา — ล้วนต้องการให้คุณรับฟังเหตุการณ์ onsuccess และ onerror และซ้อน operation ถัดไปไว้ภายใน callback ก่อนหน้า โค้ดจริงจึงกลายเป็นพีระมิดของ handler ที่ซ้อนกัน

library idb โดย Jake Archibald เป็น wrapper ขนาดเล็ก (~1 KB เมื่อบีบอัด) ที่จำลอง IndexedDB API ทั้งหมด แต่คืนค่า Promise สำหรับทุก operation ซึ่งหมายความว่าคุณสามารถใช้ async/await ได้ตลอด เหมือนกับที่คุณใช้กับ fetch หรือ API สมัยใหม่อื่น ๆ

import { openDB } from 'https://esm.sh/idb@8';
const db = await openDB('my-database', 1, {
upgrade(db) {
db.createObjectStore('tasks', { keyPath: 'id' });
},
});

openDB รับอาร์กิวเมนต์สามตัว:

อาร์กิวเมนต์ประเภทจุดประสงค์
namestringชื่อ database (กำหนดขอบเขตตาม origin)
versionnumberเวอร์ชันของ schema — เพิ่มค่าเพื่อกระตุ้นการอัปเกรด
{ upgrade(db) }objectCallback ที่รันเมื่อ DB ถูกสร้างขึ้นหรือเวอร์ชันเพิ่มขึ้น

Callback upgrade เป็นสถานที่เดียวที่คุณสามารถสร้างหรือลบ object store ได้ จะถูกข้ามไปทั้งหมดในการเปิดครั้งถัดไปเมื่อเวอร์ชันไม่มีการเปลี่ยนแปลง

await db.put('tasks', { id: 1, title: 'Buy groceries', done: false });
await db.put('tasks', { id: 2, title: 'Write tests', done: true });

db.put(storeName, value) จะแทรกค่าหากยังไม่มี key นั้น หรือ แทนที่ ค่าเดิมหากมีอยู่แล้ว key จะถูกอนุมานจาก keyPath ที่คุณกำหนดไว้เมื่อสร้าง store (ในตัวอย่างข้างต้นคือ 'id')

const task = await db.get('tasks', 1);
// { id: 1, title: 'Buy groceries', done: false }
const allTasks = await db.getAll('tasks');
// [ { id: 1, ... }, { id: 2, ... } ]
  • db.get(storeName, key) คืนค่า record เดียว (หรือ undefined เมื่อไม่พบ key)
  • db.getAll(storeName) คืนค่าอาร์เรย์ของ record ทั้งหมดใน store
await db.delete('tasks', 1);
const remaining = await db.getAll('tasks');
console.log(remaining.length); // 1

db.delete(storeName, key) ลบ record สำหรับ key นั้น การเรียกใช้กับ key ที่ไม่มีอยู่จะไม่มีผลใด ๆ

Browser Storage

operation “เปิด database และเขียน record” เดียวกันมีลักษณะดังนี้เมื่อใช้ IndexedDB ดิบ:

// Raw IndexedDB — callback style
const request = indexedDB.open('my-db', 1);
request.onupgradeneeded = (e) => {
e.target.result.createObjectStore('tasks', { keyPath: 'id' });
};
request.onsuccess = (e) => {
const db = e.target.result;
const tx = db.transaction('tasks', 'readwrite');
const store = tx.objectStore('tasks');
const putReq = store.put({ id: 1, title: 'Buy groceries', done: false });
putReq.onsuccess = () => console.log('Saved');
};
request.onerror = (e) => console.error(e.target.error);

และเมื่อใช้ idb:

// idb — async/await style
const db = await openDB('my-db', 1, {
upgrade(db) {
db.createObjectStore('tasks', { keyPath: 'id' });
},
});
await db.put('tasks', { id: 1, title: 'Buy groceries', done: false });
console.log('Saved');

ทั้งสองทำสิ่งเดียวกันทุกประการ เวอร์ชัน idb สั้นกว่า อ่านง่ายกว่า และข้อผิดพลาดจะแสดงเป็น rejected Promise ที่คุณสามารถดักจับด้วย try/catch มาตรฐาน

ตัวเลือกBenefitCost
IndexedDB ดิบควบคุมได้เต็มที่ ไม่มี dependency เพิ่ม ทำงานตรงกับ spec ของ browserโค้ดยุ่งยาก ซ้อน callback หลายชั้น debug ยาก
idb libraryDeveloper experience (DX) ดีกว่ามาก ใช้ async/await เขียนสั้น อ่านง่าย จับ error ด้วย try/catch ได้ตรงไปตรงมาเพิ่ม dependency ขนาดเล็ก (~1 KB) ต้องโหลดผ่าน import และพึ่งพา library ภายนอก
  • สร้างหรือแก้ไข object store นอก callback upgrade — จะไม่มีผลใดๆ เลย object store และ index เปลี่ยนแปลงได้เฉพาะภายใน upgrade เมื่อหมายเลขเวอร์ชันเพิ่มขึ้นเท่านั้น
  • คิดว่า idb เปลี่ยนพฤติกรรมของ IndexedDBidb เป็นเพียง wrapper ที่ห่อ API เดิมด้วย Promise เท่านั้น ข้อจำกัดของ IndexedDB เช่น เรื่อง transaction lifetime หรือ upgrade transaction ยังคงเหมือนเดิมทุกประการ
  • ลืมว่า db.put() แทนที่ record ทั้งก้อน — หากต้องการอัปเดตแค่บาง field ต้อง get() มาก่อน แล้วค่อย merge ค่าใหม่ก่อน put() กลับไป ไม่เช่นนั้นข้อมูลเดิมจะหายไป

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

Spotify Web Player — ใช้ pattern แบบ Promise-based คล้าย idb ในการ cache ข้อมูลเพลงและ metadata ฝั่ง client เพื่อรองรับการเล่นแบบ offline-ish เมื่อ network ไม่เสถียร

Excalidraw — เลือกใช้ idb โดยตรงเพื่อลดความซับซ้อนของโค้ดที่ persist state ของ canvas ทำให้ทีมโฟกัสกับ logic ของแอปแทนที่จะจัดการ callback ของ IndexedDB เอง

อาร์กิวเมนต์ตัวที่สามของ openDB() ใช้สำหรับอะไร?
db.put(storeName, value) ทำอะไรเมื่อ key นั้นมีอยู่แล้วใน store?
เมธอด idb ใดที่คืนค่าระเบียนทั้งหมดใน object store เป็นอาร์เรย์?
ข้อได้เปรียบหลักของไลบรารี idb เมื่อเทียบกับ IndexedDB API ดิบคืออะไร?