שיטות מומלצות לשימוש ב-IndexedDB

שיטות מומלצות לסנכרון מצב האפליקציה בין IndexedDB ספריות ניהול פופולריות של מצבים.

כשהמשתמש טוען בפעם הראשונה אתר או אפליקציה, בדרך כלל צריך להשקיע לא מעט מאמץ כדי לבנות את המצב הראשוני של האפליקציה שמשמש לעיבוד ממשק המשתמש. לדוגמה, לפעמים האפליקציה צריכה לאמת את המשתמש בצד הלקוח ואז לשלוח כמה בקשות API כדי שיהיו לה את כל הנתונים שהיא צריכה להציג בדף.

אחסון מצב האפליקציה ב-IndexedDB יכול להיות דרך מצוינת להאצת זמן הטעינה של ביקורים חוזרים. לאחר מכן האפליקציה יכולה להסתנכרן עם כל שירותי API ברקע ולעדכן את ממשק המשתמש בנתונים חדשים באופן מדורג, תוך שימוש באסטרטגיה של תהליך אימות מחדש לא פעיל.

שימוש טוב נוסף ב-IndexedDB הוא לאחסן תוכן שנוצר על ידי משתמשים, כאחסון זמני לפני ההעלאה לשרת או כמטמון בצד הלקוח של הנתונים המרוחקים, או כשניהם.

עם זאת, כשמשתמשים ב-IndexedDB, יש הרבה דברים חשובים שצריך לקחת בחשבון, שאולי לא יהיו ברורים למפתחים חדשים בממשקי ה-API. המאמר הזה עונה על שאלות נפוצות ונדון בכמה מהדברים שהכי חשוב לזכור כששומרים נתונים ב-IndexedDB.

שמירה על יכולת החיזוי של האפליקציה

חלק גדול מהמורכבות ב-IndexedDB נובעות מהעובדה שיש כל כך הרבה גורמים שאין לך (המפתח) שליטה עליהם. בקטע הזה מפורטות הרבה מהבעיות שצריך לזכור כשעובדים עם IndexedDB.

לא כל דבר אפשר לאחסן ב-IndexedDB בכל הפלטפורמות

אם מאחסנים קבצים גדולים שנוצרו על ידי משתמשים, כמו תמונות או סרטונים, אפשר לנסות לאחסן אותם כאובייקטים File או Blob. זה יעבוד בפלטפורמות מסוימות אבל ייכשל בפלטפורמות אחרות. באופן ספציפי, אי אפשר לשמור קובצי Blob ב-IndexedDB ב-Safari ב-iOS.

למרבה המזל, לא קשה מדי להמיר Blob ל-ArrayBuffer, ולהיפך. יש תמיכה טובה מאוד באחסון של ArrayBuffer ב-IndexedDB.

עם זאת, חשוב לזכור של-Blob יש סוג MIME ול-ArrayBuffer אין. כדי לבצע את ההמרה בצורה נכונה, צריך לשמור את הסוג לצד מאגר הנתונים הזמני.

כדי להמיר ArrayBuffer ל-Blob, צריך להשתמש ב-constructor של 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. כלומר, בדרך כלל כדאי לשמור את מצב האפליקציה בזיכרון (בנוסף לאחסון), כדי שממשק המשתמש לא יתקלקל בהפעלה במצב גלישה פרטית או כשאין מקום פנוי באחסון (גם אם חלק מהתכונות האחרות של האפליקציה שדורשות אחסון לא יפעלו).

כדי לזהות שגיאות בפעולות של IndexedDB, אפשר להוסיף handler של אירועים לאירוע error בכל פעם שיוצרים אובייקט IDBDatabase, IDBTransaction או IDBRequest.

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

יכול להיות שהמשתמש שינה או מחק נתונים מאוחסנים

בניגוד למסדי נתונים בצד השרת שבהם אפשר להגביל גישה לא מורשית, מסדי נתונים בצד הלקוח נגישים לתוספי דפדפן ולכלים למפתחים, והמשתמשים יכולים למחוק אותם.

למרות שיכול להיות שהמשתמשים לא משנים את הנתונים שלהם שמאוחסנים באופן מקומי, המשתמשים נוהגים למחוק אותם. חשוב שהאפליקציה תוכל לטפל בשני המקרים האלה ללא שגיאות.

יכול להיות שהנתונים המאוחסנים לא עדכניים

בדומה לסעיף הקודם, גם אם המשתמש לא שינה את הנתונים בעצמו, ייתכן גם שהנתונים שיש לו באחסון נכתבו על ידי גרסה ישנה של הקוד, אולי גרסה שיש בה באגים.

ב-IndexedDB יש תמיכה מובנית בגרסאות סכימה ובשדרוג באמצעות השיטה IDBOpenDBRequest.onupgradeneeded(). אבל עדיין צריך לכתוב את קוד השדרוג כך שיוכל לטפל במשתמש שמגיע מגרסה קודמת (כולל גרסה עם באג).

בדיקות יחידה (unit testing) יכולות להיות שימושיות מאוד, כי לעיתים קרובות אי אפשר לבדוק באופן ידני את כל הנתיבים והמקרים האפשריים לשדרוג.

שמירה על ביצועי האפליקציה

אחת מהתכונות העיקריות של IndexedDB היא ה-API האסינכרוני שלו, אבל אל תתנו לזה להטעות אתכם ולגרום לכם לחשוב שאין לכם צורך לדאוג לגבי הביצועים כשאתם משתמשים בו. יש כמה מקרים שבהם שימוש לא נכון עדיין יכול לחסום את ה-thread הראשי, וזה עלול לגרום לבעיות בממשק (jank) ולחוסר תגובה.

באופן כללי, פעולות קריאה וכתיבה ב-IndexedDB לא יכולות להיות גדולות יותר מהנדרש לנתונים שניגשים אליהם.

למרות שאפשר לאחסן אובייקטים גדולים שמעוננים ב-IndexedDB כרשומה אחת (וזה אמנם נוח למדי מבחינת המפתח, אבל יש להימנע מכך. הסיבה לכך היא שכשאובייקט IndexedDB מאחסן אותו, קודם הוא צריך ליצור שכפול מובנה של האובייקט, ותהליך השכפול המובנה מתרחש ב-thread הראשי. ככל שהאובייקט גדול יותר, זמן החסימה יהיה ארוך יותר.

יש כאן כמה אתגרים כשרוצים לשמור על מצב האפליקציה ב-IndexedDB, כי רוב ספריות ניהול המצב הפופולריות (כמו Redux) פועלות על ידי ניהול עץ המצב כולו כאובייקט JavaScript יחיד.

לניהול מצב בצורה הזו יש יתרונות רבים (למשל, הוא הופך את הקוד בקלות לנימוקים ולניפוי באגים). לעומת זאת, אחסון של עץ המצב כולו כרשומה אחת ב-IndexedDB עשוי להיות מפתה ונוח, אבל אם תבצעו את הפעולה הזו אחרי כל שינוי (גם אם מתבצעת ויסות נתונים (throttle)/יצאו משימוש), הדבר יוביל לחסימה מיותרת של ה-thread הראשי, והוא יגרום לחסימה מיותרת של ה-thread הראשי, ובחלק מהמקרים הוא יגרום לשגיאות כתיבה בדפדפן, ובחלק מהמקרים הוא יגרום לשגיאות כתיבה.

במקום לאחסן את כל עץ המצב ברשומה אחת, צריך לפצל אותו לרשומות נפרדות ולעדכן רק את הרשומות שמשתנה בפועל.

אותו עיקרון חל על אם אתם מאחסנים ב-IndexedDB פריטים גדולים כמו תמונות, מוזיקה או סרטונים. מאחסנים כל פריט עם מפתח משלו במקום בתוך אובייקט גדול יותר, כדי לאחזר את הנתונים המובְנים בלי לשלם את העלות של אחזור הקובץ הבינארי.

כמו ברוב השיטות המומלצות, זהו לא כלל של "הכול או כלום". במקרים שבהם לא ניתן לפרק אובייקט מצב ורק לכתוב את השינוי המינימלי, מפצלים את הנתונים לעצי משנה ולכתב אותם בלבד, עדיין עדיף לכתוב תמיד את עץ המצב כולו. עדיף לבצע שיפורים קלים מאשר לא לשפר כלל.

לסיום, תמיד צריך למדוד את ההשפעה על הביצועים של הקוד שכתבתם. נכון שכתיבה קטנה ל-IndexedDB מניבה ביצועים טובים יותר מכתיבה גדולה, אבל חשוב רק אם הכתיבה ל-IndexedDB שהאפליקציה מבצעת בפועל מובילה למשימות ארוכות שחוסמות את ה-thread הראשי ופוגעות בחוויית המשתמש. חשוב למדוד כדי שתבינו בשביל מה אתם מבצעים אופטימיזציה.

מסקנות

המפתחים יכולים למנף מנגנוני אחסון של לקוחות כמו IndexedDB כדי לשפר את חוויית המשתמש באפליקציה, על ידי שמירה על המצב של האפליקציה בסשנים שונים, אלא גם על ידי צמצום הזמן שנדרש לטעינת המצב הראשוני בביקורים חוזרים.

שימוש נכון ב-IndexedDB יכול לשפר משמעותית את חוויית המשתמש, אבל טיפול שגוי בשגיאות או כשל בטיפול במקרים של שגיאות עלול להוביל לאפליקציות לא תקינות ולמשתמשים לא מרוצים.

מכיוון שאחסון הלקוח כולל גורמים רבים שאינם בשליטתכם, חשוב מאוד שהקוד נבדק היטב ומטפל בשגיאות כראוי, גם אם נראה בהתחלה שלא סביר שיתרחשו שגיאות.