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

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

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

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

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

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

การทำให้แอปของคุณคาดเดาได้

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

ข้อมูลบางอย่างอาจเก็บไว้ใน IndexedDB บางแพลตฟอร์มเท่านั้น

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

โชคดีที่การแปลง Blob เป็น ArrayBuffer ก็ไม่ยากเกินไป และในทางกลับกันด้วย การจัดเก็บ ArrayBuffer ใน 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() อย่างไรก็ตาม คุณยังคงต้องเขียนโค้ดอัปเกรดให้สามารถรองรับผู้ใช้ที่มาจากเวอร์ชันก่อนหน้า (รวมถึงเวอร์ชันที่มีข้อบกพร่อง)

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

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

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

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

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

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

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

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

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

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

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

บทสรุป

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

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

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