Library idb: IndexedDB แบบ Promise
ทำไมต้องใช้ library wrapper?
หัวข้อที่มีชื่อว่า “ทำไมต้องใช้ library wrapper?”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 สมัยใหม่อื่น ๆ
การเปิด database ด้วย openDB
หัวข้อที่มีชื่อว่า “การเปิด database ด้วย openDB”import { openDB } from 'https://esm.sh/idb@8';
const db = await openDB('my-database', 1, { upgrade(db) { db.createObjectStore('tasks', { keyPath: 'id' }); },});openDB รับอาร์กิวเมนต์สามตัว:
| อาร์กิวเมนต์ | ประเภท | จุดประสงค์ |
|---|---|---|
name | string | ชื่อ database (กำหนดขอบเขตตาม origin) |
version | number | เวอร์ชันของ schema — เพิ่มค่าเพื่อกระตุ้นการอัปเกรด |
{ upgrade(db) } | object | Callback ที่รันเมื่อ DB ถูกสร้างขึ้นหรือเวอร์ชันเพิ่มขึ้น |
Callback upgrade เป็นสถานที่เดียวที่คุณสามารถสร้างหรือลบ object store ได้ จะถูกข้ามไปทั้งหมดในการเปิดครั้งถัดไปเมื่อเวอร์ชันไม่มีการเปลี่ยนแปลง
การเขียน record ด้วย db.put
หัวข้อที่มีชื่อว่า “การเขียน record ด้วย db.put”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')
การอ่าน record ด้วย db.get และ db.getAll
หัวข้อที่มีชื่อว่า “การอ่าน record ด้วย db.get และ db.getAll”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
การลบ record ด้วย db.delete
หัวข้อที่มีชื่อว่า “การลบ record ด้วย db.delete”await db.delete('tasks', 1);const remaining = await db.getAll('tasks');console.log(remaining.length); // 1db.delete(storeName, key) ลบ record สำหรับ key นั้น การเรียกใช้กับ key ที่ไม่มีอยู่จะไม่มีผลใด ๆ
รันได้เลย: idb ในการใช้งานจริง
หัวข้อที่มีชื่อว่า “รันได้เลย: idb ในการใช้งานจริง”เปรียบเทียบ IndexedDB ดิบกับ idb
หัวข้อที่มีชื่อว่า “เปรียบเทียบ IndexedDB ดิบกับ idb”operation “เปิด database และเขียน record” เดียวกันมีลักษณะดังนี้เมื่อใช้ IndexedDB ดิบ:
// Raw IndexedDB — callback styleconst 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 styleconst 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 มาตรฐาน
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ตัวเลือก | Benefit | Cost |
|---|---|---|
| IndexedDB ดิบ | ควบคุมได้เต็มที่ ไม่มี dependency เพิ่ม ทำงานตรงกับ spec ของ browser | โค้ดยุ่งยาก ซ้อน callback หลายชั้น debug ยาก |
idb library | Developer experience (DX) ดีกว่ามาก ใช้ async/await เขียนสั้น อ่านง่าย จับ error ด้วย try/catch ได้ตรงไปตรงมา | เพิ่ม dependency ขนาดเล็ก (~1 KB) ต้องโหลดผ่าน import และพึ่งพา library ภายนอก |
ข้อผิดพลาดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ข้อผิดพลาดที่พบบ่อย”- สร้างหรือแก้ไข object store นอก callback
upgrade— จะไม่มีผลใดๆ เลย object store และ index เปลี่ยนแปลงได้เฉพาะภายในupgradeเมื่อหมายเลขเวอร์ชันเพิ่มขึ้นเท่านั้น - คิดว่า
idbเปลี่ยนพฤติกรรมของ IndexedDB —idbเป็นเพียง 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 เอง