แนวทางปฏิบัติแนะนำสำหรับการใช้ IndexedDB

ดูแนวทางปฏิบัติแนะนำสำหรับการซิงค์สถานะของแอปพลิเคชันระหว่าง IndexedDB กับไลบรารีการจัดการสถานะยอดนิยม

เมื่อผู้ใช้โหลดเว็บไซต์หรือแอปพลิเคชันเป็นครั้งแรก มักจะมีงานค่อนข้างมากเกี่ยวกับการสร้างสถานะแอปพลิเคชันเริ่มต้นที่ใช้ในการแสดง UI ตัวอย่างเช่น บางครั้งแอปต้องตรวจสอบสิทธิ์ฝั่งไคลเอ็นต์ของผู้ใช้ แล้วส่งคำขอ API หลายรายการก่อนที่แอปจะมีข้อมูลทั้งหมดที่จำเป็นต่อการแสดงในหน้าเว็บ

การจัดเก็บสถานะของแอปพลิเคชันใน IndexedDB อาจเป็นวิธีที่ดีในการเพิ่มความเร็วในการโหลดสำหรับการเข้าชมซ้ำ จากนั้นแอปจะซิงค์กับบริการ API ในเบื้องหลัง และอัปเดต UI ด้วยข้อมูลใหม่แบบ Lazy Loading โดยใช้กลยุทธ์ไม่มีอัปเดตขณะตรวจสอบซ้ำ

ประโยชน์อีกข้อหนึ่งสำหรับ IndexedDB คือการจัดเก็บเนื้อหาที่ผู้ใช้สร้างขึ้น ไม่ว่าจะเป็นการจัดเก็บเนื้อหาชั่วคราวก่อนที่จะอัปโหลดไปยังเซิร์ฟเวอร์ หรือเป็นแคชข้อมูลระยะไกลที่ฝั่งไคลเอ็นต์ หรือทั้งคู่

อย่างไรก็ตาม เมื่อใช้ IndexedDB มีสิ่งสำคัญหลายอย่างที่ต้องพิจารณา ซึ่งอาจไม่ชัดเจนนักสำหรับนักพัฒนาที่เพิ่งเริ่มใช้ API บทความนี้จะตอบคำถามที่พบบ่อยและพูดถึงสิ่งสำคัญที่สุดบางประการที่ควรคำนึงถึงเมื่อคงข้อมูลใน IndexedDB

การทำให้แอปคาดการณ์ได้อยู่เสมอ

ความซับซ้อนมากมายเกี่ยวกับ IndexedDB เกิดจากข้อเท็จจริงที่ว่า มีปัจจัยต่างๆ มากมายที่คุณ (นักพัฒนาซอฟต์แวร์) ไม่มีสิทธิ์ควบคุม ส่วนนี้จะสำรวจปัญหาหลายอย่างที่คุณต้องคำนึงถึงเมื่อทำงานกับ IndexedDB

ไม่สามารถจัดเก็บทุกอย่างใน IndexedDB บนทุกแพลตฟอร์ม

หากคุณจัดเก็บไฟล์ขนาดใหญ่ที่ผู้ใช้สร้างขึ้น เช่น รูปภาพหรือวิดีโอ คุณอาจลองเก็บไฟล์เหล่านั้นเป็นออบเจ็กต์ File หรือ Blob การทำเช่นนี้จะใช้ได้ในบางแพลตฟอร์ม แต่ล้มเหลวในบางแพลตฟอร์ม โดยเฉพาะ Safari ใน iOS จะไม่สามารถจัดเก็บ Blob ใน IndexedDB

โชคดีที่การแปลง Blob เป็น ArrayBuffer ไม่ยากเกินไป และในทางกลับกันด้วย การจัดเก็บ ArrayBuffers ใน IndexedDB ได้รับการรองรับเป็นอย่างดี

อย่างไรก็ตาม อย่าลืมว่า Blob มีประเภท MIME แต่ ArrayBuffer ไม่มี คุณจะต้องจัดเก็บประเภทไว้ข้างบัฟเฟอร์เพื่อให้การแปลงเป็นไปอย่างถูกต้อง

ในการแปลง ArrayBuffer เป็น Blob เพียงแค่ใช้ตัวสร้าง Blob

function arrayBufferToBlob(buffer, type) {
  return new Blob([buffer], { type: type });
}

อีกทิศทางหนึ่งจะเกี่ยวข้องมากกว่าเล็กน้อย และเป็นกระบวนการที่ไม่พร้อมกัน คุณสามารถใช้ออบเจ็กต์ FileReader เพื่ออ่าน Blob เป็น ArrayBuffer เมื่ออ่านค่าเสร็จแล้ว จะมีการทริกเกอร์เหตุการณ์ loadend บนเครื่องอ่าน คุณสามารถรวมกระบวนการนี้ในรูปแบบ Promise ดังนี้

function blobToArrayBuffer(blob) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.addEventListener('loadend', () => {
      resolve(reader.result);
    });
    reader.addEventListener('error', reject);
    reader.readAsArrayBuffer(blob);
  });
}

การเขียนไปยังพื้นที่เก็บข้อมูลอาจล้มเหลว

ข้อผิดพลาดเมื่อเขียนไปยัง IndexedDB อาจเกิดขึ้นได้จากหลายสาเหตุ และในบางกรณี สาเหตุเหล่านี้ก็อยู่นอกเหนือการควบคุมของคุณในฐานะนักพัฒนาซอฟต์แวร์ ตัวอย่างเช่น ปัจจุบันบางเบราว์เซอร์ไม่อนุญาตให้เขียนไปยัง IndexedDB เมื่ออยู่ในโหมดการเรียกดูแบบส่วนตัว นอกจากนี้ยังเป็นไปได้ที่ผู้ใช้จะใช้พื้นที่ในดิสก์จนเต็ม และเบราว์เซอร์จะจำกัดไม่ให้คุณจัดเก็บข้อมูลใดๆ เลย

ด้วยเหตุนี้ คุณจึงต้องใช้การจัดการข้อผิดพลาดที่เหมาะสมในโค้ด IndexedDB เสมอ นอกจากนี้ โดยทั่วไปคุณควรเก็บสถานะของแอปพลิเคชันไว้ในหน่วยความจำ (นอกเหนือจากการจัดเก็บ) เพื่อให้ UI ทำงานอย่างต่อเนื่องเมื่อทำงานในโหมดการเรียกดูแบบส่วนตัวหรือเมื่อไม่มีพื้นที่เก็บข้อมูลให้ใช้งาน (แม้ว่าฟีเจอร์อื่นๆ ของแอปที่ต้องใช้พื้นที่เก็บข้อมูลจะไม่ทำงานก็ตาม)

คุณสามารถตรวจหาข้อผิดพลาดในการดำเนินงาน IndexedDB โดยเพิ่มเครื่องจัดการเหตุการณ์สำหรับเหตุการณ์ error ทุกครั้งที่คุณสร้างออบเจ็กต์ IDBDatabase, IDBTransaction หรือ IDBRequest

const request = db.open('example-db', 1);
request.addEventListener('error', (event) => {
  console.log('Request error:', request.error);
};

ผู้ใช้อาจแก้ไขหรือลบข้อมูลที่จัดเก็บไว้ได้

ส่วนขยายเบราว์เซอร์และเครื่องมือสำหรับนักพัฒนาซอฟต์แวร์จะเข้าถึงฐานข้อมูลฝั่งไคลเอ็นต์ได้ในฐานข้อมูลฝั่งไคลเอ็นต์ และผู้ใช้สามารถล้างฐานข้อมูลเหล่านี้ได้ ซึ่งต่างจากฐานข้อมูลฝั่งเซิร์ฟเวอร์ที่จำกัดการเข้าถึงโดยไม่ได้รับอนุญาตได้

แม้ว่าผู้ใช้จะแก้ไขข้อมูลที่เก็บไว้ในเครื่องได้ยาก แต่ผู้ใช้มักจะล้างข้อมูลออกอยู่แล้ว แอปพลิเคชันของคุณจำเป็นต้องจัดการทั้ง 2 กรณีนี้ได้โดยไม่เกิดข้อผิดพลาด

ข้อมูลที่จัดเก็บไว้อาจเก่าเกินไป

คล้ายกับส่วนก่อนหน้านี้ แม้ว่าผู้ใช้จะไม่ได้แก้ไขข้อมูลเอง แต่ก็เป็นไปได้ว่าข้อมูลที่ตนอยู่ในพื้นที่เก็บข้อมูลเขียนขึ้นโดยโค้ดเวอร์ชันเก่า ซึ่งอาจจะเป็นเวอร์ชันที่มีข้อบกพร่องอยู่ในนั้น

IndexedDB มีการสนับสนุนในตัวสำหรับเวอร์ชันสคีมาและการอัปเกรดผ่านเมธอด IDBOpenDBRequest.onupgradeneeded() อย่างไรก็ตาม คุณยังคงต้องเขียนโค้ดการอัปเกรดให้รองรับผู้ใช้ที่มาจากเวอร์ชันก่อนหน้า (รวมถึงเวอร์ชันที่มีข้อบกพร่อง)

การทดสอบหน่วยจะมีประโยชน์มากสำหรับที่นี่ เนื่องจากการทดสอบเส้นทางการอัปเกรดและกรณีที่เป็นไปได้ทั้งหมดด้วยตนเองมักเป็นไปไม่ได้

ทำให้แอปมีประสิทธิภาพอยู่เสมอ

หนึ่งในฟีเจอร์หลักของ IndexedDB คือ API แบบไม่พร้อมกัน แต่อย่าปล่อยให้เรื่องนี้เข้าใจผิดว่าคุณไม่จำเป็นต้องกังวลเกี่ยวกับประสิทธิภาพเมื่อใช้ มีหลายกรณีที่การใช้งานที่ไม่เหมาะสมอาจยังบล็อกเทรดหลักได้ ซึ่งอาจทำให้เกิดความไม่เสถียรและไม่ตอบสนอง

ตามกฎทั่วไป การอ่านและเขียนไปยัง IndexedDB ไม่ควรมีขนาดใหญ่กว่าที่กำหนดสำหรับข้อมูลที่มีการเข้าถึง

แม้ว่า IndexedDB จะทำให้สามารถจัดเก็บออบเจ็กต์ขนาดใหญ่ที่ซ้อนกันเป็นระเบียนเดียวได้ (ซึ่งเป็นที่ยอมรับกันค่อนข้างง่ายในแง่ของนักพัฒนาซอฟต์แวร์) จึงควรหลีกเลี่ยงการกระทำนี้ เนื่องจากเมื่อ IndexedDB จัดเก็บออบเจ็กต์ จะต้องมีการสร้างการโคลนที่มีโครงสร้างของออบเจ็กต์นั้นก่อน และกระบวนการโคลนที่มีโครงสร้างจะเกิดขึ้นในเทรดหลัก ยิ่งออบเจ็กต์มีขนาดใหญ่เท่าใด ระยะเวลาการบล็อกก็จะยิ่งนานขึ้นเท่านั้น

ปัญหานี้ก่อให้เกิดความท้าทายในการวางแผนเกี่ยวกับวิธีคงสถานะของแอปพลิเคชันไว้ใน IndexedDB เนื่องจากไลบรารีการจัดการสถานะยอดนิยมส่วนใหญ่ (เช่น Redux) ทำงานด้วยการจัดการแผนผังสถานะทั้งหมดเป็นออบเจ็กต์ JavaScript รายการเดียว

แม้ว่าการจัดการสถานะในลักษณะนี้มีประโยชน์หลายอย่าง (เช่น ทำให้โค้ดหาเหตุผลและแก้ไขข้อบกพร่องได้ง่าย) และแม้การจัดเก็บโครงสร้างสถานะทั้งหมดเป็นระเบียนเดียวใน IndexedDB ก็อาจจะน่าดึงดูดและสะดวก แต่การทำเช่นนี้หลังการเปลี่ยนแปลงทุกครั้ง (แม้จะควบคุม/ดีเดน) จะส่งผลให้มีการบล็อกเทรดหลักโดยไม่จำเป็น แต่ก็จะเพิ่มโอกาสในการเขียนข้อผิดพลาดหรือแม้แต่ในกรณีที่เบราว์เซอร์ไม่ตอบสนอง

แทนที่จะจัดเก็บโครงสร้างสถานะทั้งหมดไว้ในระเบียนเดียว คุณควรแยกออกเป็นระเบียนย่อยๆ และอัปเดตเฉพาะระเบียนที่มีการเปลี่ยนแปลงจริงๆ เท่านั้น

และระบบจะทำเช่นเดียวกันหากคุณจัดเก็บรายการขนาดใหญ่ เช่น รูปภาพ เพลง หรือวิดีโอ ไว้ใน IndexedDB จัดเก็บแต่ละรายการด้วยคีย์ของตัวเองแทนที่จะเก็บอยู่ในออบเจ็กต์ที่ใหญ่กว่า เพื่อให้คุณดึงข้อมูลที่มีโครงสร้างได้โดยไม่ต้องเสียค่าใช้จ่ายในการเรียกไฟล์ไบนารีด้วย

เช่นเดียวกับแนวทางปฏิบัติแนะนำส่วนใหญ่ กฎนี้ไม่ใช่กฎเกณฑ์ตายตัว ในกรณีที่ไม่สามารถแยกออบเจ็กต์สถานะแล้วเขียนเพียงชุดการเปลี่ยนแปลงที่น้อยที่สุด การแบ่งข้อมูลออกเป็นโครงสร้างย่อยๆ และการเขียนเพียงแผนผังสถานะยังดีกว่าการเขียนแผนผังสถานะทั้งหมดเสมอ ปรับปรุงเล็กน้อยดีกว่าไม่ต้องปรับปรุงเลย

สุดท้ายนี้ คุณควรวัดผลกระทบด้านประสิทธิภาพของโค้ดที่คุณเขียนเสมอ แม้ว่าจะเขียนขนาดเล็กลงใน IndexedDB จะทำงานได้ดีกว่าการเขียนขนาดใหญ่ แต่ก็จำเป็นเฉพาะในกรณีที่การเขียนใน IndexedDB ที่แอปพลิเคชันกำลังทำอยู่จริงๆ นำไปสู่งานที่ใช้เวลานานซึ่งบล็อกเทรดหลักและทำให้ประสบการณ์ของผู้ใช้แย่ลง การวัดผลเป็นสิ่งสำคัญ เพื่อให้เข้าใจว่ากำลังเพิ่มประสิทธิภาพอะไร

บทสรุป

นักพัฒนาซอฟต์แวร์สามารถใช้ประโยชน์จากกลไกพื้นที่เก็บข้อมูลของไคลเอ็นต์ เช่น IndexedDB เพื่อปรับปรุงประสบการณ์ของผู้ใช้ในแอปพลิเคชันของตน โดยไม่เพียงรักษาสถานะในเซสชันต่างๆ เท่านั้น แต่ยังลดเวลาที่ใช้ในการโหลดสถานะเริ่มต้นเมื่อมีการเข้าชมซ้ำ

ขณะที่การใช้ IndexedDB อย่างเหมาะสมจะช่วยปรับปรุงประสบการณ์ของผู้ใช้ได้อย่างมาก แต่การใช้ IndexedDB อย่างไม่ถูกต้องหรือไม่จัดการกับกรณีข้อผิดพลาดอาจทำให้แอปเสียหายและผู้ใช้ไม่พอใจ

เนื่องจากพื้นที่เก็บข้อมูลของไคลเอ็นต์มีหลายปัจจัยที่อยู่เหนือการควบคุมของคุณ โค้ดจึงได้รับการทดสอบเป็นอย่างดีและจัดการกับข้อผิดพลาดอย่างเหมาะสม แม้ว่าข้อผิดพลาดที่อาจจะดูเหมือนไม่เกิดขึ้นในตอนแรกก็ตาม