Migrations และ Blobs
การกำหนดเวอร์ชัน Schema ใน IndexedDB
หัวข้อที่มีชื่อว่า “การกำหนดเวอร์ชัน Schema ใน IndexedDB”database IndexedDB ทุกตัวมี หมายเลขเวอร์ชัน เป็นจำนวนเต็ม เมื่อคุณเรียก indexedDB.open(name, version) ด้วยเวอร์ชันที่สูงกว่าที่จัดเก็บอยู่ในปัจจุบัน browser จะยิงเหตุการณ์ upgradeneeded ก่อน success นี่คือ สถานที่เดียว ที่คุณสามารถสร้างหรือลบ object store และ index ได้
const request = indexedDB.open('my-db', 2);
request.onupgradeneeded = (event) => { const db = event.target.result; const oldVersion = event.oldVersion; // 0 if brand-new, 1 if upgrading from v1 // ...create stores based on oldVersion};คุณสมบัติสำคัญคือ event.oldVersion: ค่านี้บอกให้คุณรู้ว่า database อยู่ที่เวอร์ชันใด ก่อน การเปิดครั้งนี้ database ที่สร้างใหม่จะรายงาน oldVersion === 0
รูปแบบ migration: การตรวจสอบ if (oldVersion < N) แบบต่อเนื่อง
หัวข้อที่มีชื่อว่า “รูปแบบ migration: การตรวจสอบ if (oldVersion < N) แบบต่อเนื่อง”แทนที่จะใช้ if/else เพียงอันเดียว ให้ใช้การตรวจสอบแบบน้อยกว่าต่อเนื่องกัน เพื่อให้ database ที่อยู่ในเวอร์ชันก่อนหน้าใด ๆ รัน upgrade ทั้งหมดที่ขาดหายไป:
request.onupgradeneeded = (event) => { const db = event.target.result; const { oldVersion } = event;
if (oldVersion < 1) { // v0 → v1: create the initial 'users' store db.createObjectStore('users', { keyPath: 'id' }); }
if (oldVersion < 2) { // v1 → v2: add a 'files' store for binary data db.createObjectStore('files', { keyPath: 'name' }); }
// if oldVersion < 3: future migration goes here};ผู้ใช้ที่อัปเกรดจาก v0 จะรัน ทั้งสอง บล็อก ผู้ใช้ที่อัปเกรดจาก v1 จะรันเฉพาะบล็อกที่สอง ผู้ใช้ที่อยู่บน v2 อยู่แล้วจะไม่รันบล็อกใดเลย
การจัดเก็บ object Blob และ File
หัวข้อที่มีชื่อว่า “การจัดเก็บ object Blob และ File”หนึ่งในข้อได้เปรียบที่ยิ่งใหญ่ที่สุดของ IndexedDB เหนือ Web Storage คือความสามารถในการจัดเก็บค่า JavaScript ที่ structured-cloneable — รวมถึง object Blob และ File — โดยไม่ต้องทำ serialisation ใด ๆ
// Create a Blob in memoryconst blob = new Blob(['Hello, binary world!'], { type: 'text/plain' });
// Store it directly — no JSON.stringify, no base64 encodingconst tx = db.transaction('files', 'readwrite');tx.objectStore('files').put({ name: 'greeting.txt', data: blob });เมื่อคุณอ่าน record กลับมา คุณจะได้รับ Blob จริง ๆ พร้อมกับ method ทั้งหมดที่ยังคงสมบูรณ์:
const getReq = db.transaction('files').objectStore('files').get('greeting.txt');getReq.onsuccess = async () => { const record = getReq.result; const text = await record.data.text(); // Blob.prototype.text() returns a Promise console.log(text); // 'Hello, binary world!'};เทคนิคเดียวกันนี้ใช้ได้กับ object File (ที่เป็น subclass ของ Blob), ArrayBuffer, ImageBitmap และประเภทอื่น ๆ ที่ structured-cloneable
ทดลองรัน: v2 migration และ Blob round-trip
หัวข้อที่มีชื่อว่า “ทดลองรัน: v2 migration และ Blob round-trip”ตัวอย่างด้านล่างเปิด demo-advanced-migrations ที่ เวอร์ชัน 2 หาก database ยังไม่มีอยู่ onupgradeneeded จะรันทั้งสองบล็อก migration (oldVersion เป็น 0) จากนั้นจะจัดเก็บ record ผู้ใช้และ Blob ดึง Blob กลับมา และอ่านเนื้อหาข้อความก่อนล้างข้อมูล
| แนวคิด | รายละเอียด |
|---|---|
event.oldVersion | หมายเลขเวอร์ชันก่อนการเปิดครั้งนี้ 0 สำหรับ database ที่สร้างใหม่ |
การตรวจสอบ if (oldVersion < N) แบบต่อเนื่อง | รับประกันว่า migration แบบเพิ่มทีละขั้นจะไม่ถูกข้ามไป |
Blob / File ใน IDB | จัดเก็บและดึงกลับโดยไม่ต้อง serialisation ใด ๆ |
Blob.prototype.text() | คืนค่า Promise<string> — ใช้ await |
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ตัวเลือก | Benefit | Cost |
|---|---|---|
| จัดเก็บ Blob โดยตรงใน IndexedDB | สะดวก ไม่ต้อง serialise เป็น base64 หรือ JSON ประหยัด CPU และขนาดข้อมูล | ไฟล์ขนาดใหญ่กินโควต้าของ origin อย่างรวดเร็ว อาจเผลอเกิน storage budget โดยไม่รู้ตัว |
| อัปโหลดไฟล์ขึ้น server แล้วเก็บแค่ URL ใน IndexedDB | ควบคุมขนาด storage ฝั่ง client ได้ดีกว่า ไม่พึ่งพาโควต้าของ browser | ต้องพึ่ง network ตอนอ่านไฟล์ ใช้งาน offline ไม่ได้เต็มรูปแบบ |
การตรวจสอบ if (oldVersion < N) แบบต่อเนื่อง | รองรับผู้ใช้ที่อัปเกรดจากเวอร์ชันไหนก็ได้อย่างถูกต้อง | ต้องดูแล migration path เพิ่มขึ้นเรื่อยๆ ตามจำนวนเวอร์ชันที่ผ่านมา โค้ดยาวขึ้นตามเวลา |
ข้อผิดพลาดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ข้อผิดพลาดที่พบบ่อย”- จัดเก็บ Blob ขนาดใหญ่โดยไม่ตรวจสอบโควต้าก่อน — อาจทำให้ origin เกิน storage budget แบบเงียบๆ ควรเช็ค
navigator.storage.estimate()ก่อนเขียนไฟล์ขนาดใหญ่ - เขียน migration ด้วย
if/elseเดี่ยวๆ แทนการตรวจสอบแบบต่อเนื่อง — ทำให้ผู้ใช้ที่อัปเกรดข้ามหลายเวอร์ชันพลาด migration บางขั้นตอนไป ควรใช้if (oldVersion < N)เรียงต่อกันเสมอ - แปลง Blob เป็น base64 string ก่อนเก็บโดยไม่จำเป็น — IndexedDB รองรับ structured clone ของ
Blob/Fileอยู่แล้ว การแปลงเป็น base64 เพิ่ม overhead ทั้ง CPU และขนาดข้อมูลโดยไม่มีประโยชน์
💡 ตัวอย่างจากของจริง
Figma — เก็บ asset และ thumbnail ของไฟล์ design เป็น Blob ใน local cache พร้อม migration schema แบบมีเวอร์ชันเมื่อโครงสร้างข้อมูล cache เปลี่ยนแปลงในแต่ละ release
Excalidraw — จัดเก็บรูปภาพที่ผู้ใช้ paste เข้ามาบน canvas เป็น Blob โดยตรงใน IndexedDB ทำให้ภาพยังอยู่ครบแม้ปิด browser แล้วเปิดใหม่ โดยไม่ต้อง encode เป็น base64