רכיבי ממשק משתמש לקישור נתונים עם IndexedDB

ריימונד קמדן
ריימונד קמדן

מבוא

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

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

  1. עלינו להגדיר ולהפעיל מכונה של IndexedDB. קל מאוד לבצע את הפעולה הזו גם ב-Chrome וגם ב-Firefox, אבל נראה שהיא קצת יותר מורכבת.
  2. אנחנו צריכים לבדוק אם יש לנו נתונים כלשהם, ואם לא, להוריד אותם. בדרך כלל יש לעשות זאת באמצעות קריאות AJAX. לצורך ההדגמה שלנו, יצרנו מחלקה פשוטה של כלי עזר ליצירה מהירה של נתונים מזויפים. האפליקציה תצטרך לזהות מתי היא יוצרת את הנתונים האלה ולמנוע מהמשתמש להשתמש בנתונים עד אז. זו פעולה חד-פעמית. בפעם הבאה שהמשתמש יפעיל את האפליקציה, הוא לא יצטרך לבצע את התהליך הזה. הדגמה מתקדמת יותר תטפל בפעולות סנכרון בין הלקוח לשרת, אבל ההדגמה הזו מתמקדת יותר בהיבטים של ממשק המשתמש.
  3. כשהיישום יהיה מוכן, נוכל להשתמש בבקרת ההשלמה האוטומטית של ממשק המשתמש של jQuery כדי להסתנכרן עם ה-IndexedDB. בקרת ההשלמה האוטומטית מאפשרת להוסיף רשימות ומערכים בסיסיים של נתונים, אבל יש לה API שמאפשר להוסיף כל מקור נתונים. נדגים איך נוכל להשתמש בהם כדי להתחבר לנתוני IndexedDB שלנו.

תחילת העבודה

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

<form>
  <p>
    <label for="name">Name:</label> <input id="name" disabled> <span id="status"></span>
    </p>
</form>

<div id="displayEmployee"></div>

לא הרבה, נכון? בממשק המשתמש הזה יש שלושה היבטים עיקריים שחשובים לנו. הראשון הוא השדה, "שם", שישמש להשלמה האוטומטית. הטעינה מושבתת ותופעל מאוחר יותר באמצעות JavaScript. הטווח שלידו נמצא בשימוש במהלך ה-Seed הראשוני כדי לספק עדכונים למשתמש. לסיום, ייעשה שימוש ב-div עם המזהה של displayEmployee כשבוחרים עובד מההצעה האוטומטית.

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

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

window.indexedDB = window.indexedDB || window.webkitIndexedDB || window.mozIndexedDB;
var IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction;
var IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange;

בשלב הבא נשתמש בכמה משתנים גלובליים שבהם נשתמש במהלך ההדגמה:

var db;
var template;

עכשיו נתחיל עם הבלוק המוכן למסמך jQuery:

$(document).ready(function() {
  console.log("Startup...");
  ...
});

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

<h2>, </h2>
Department: <br/>
Email: <a href='mailto:'></a>

לאחר מכן, הקוד עובר הידור מחדש ב-JavaScript באופן הבא:

//Create our template
var source = $("#employeeTemplate").html();
template = Handlebars.compile(source);

עכשיו נתחיל לעבוד עם IndexedDB. קודם כל, אנחנו פותחים אותו.

var openRequest = indexedDB.open("employees", 1);

פתיחת חיבור ל-IndexedDB נותנת לנו גישה לקריאה ולכתיבה של נתונים, אבל לפני שאנחנו עושים זאת, אנחנו צריכים לוודא שיש לנו אובייקט Store. אובייקט Store הוא כמו טבלה של מסד נתונים. לאחד מ-IndexedDB עשויים להיות הרבה ObjectStores, וכל אחד מהם מכיל אוסף של אובייקטים קשורים. ההדגמה שלנו פשוטה וצריך להצטבר רק אובייקט אחד שאנחנו קוראים לו 'עובד'. כשה-IndexDB נפתח בפעם הראשונה, או כשמשנים את הגרסה בקוד, מופעל אירוע onupgradeneeded. נוכל להשתמש בה כדי להגדיר את ה-ObjectStore שלנו.

// Handle setup.
openRequest.onupgradeneeded = function(e) {

  console.log("running onupgradeneeded");
  var thisDb = e.target.result;

  // Create Employee
  if(!thisDb.objectStoreNames.contains("employee")) {
    console.log("I need to make the employee objectstore");
    var objectStore = thisDb.createObjectStore("employee", {keyPath: "id", autoIncrement: true});
    objectStore.createIndex("searchkey", "searchkey", {unique: false});
  }

};

openRequest.onsuccess = function(e) {
  db = e.target.result;

  db.onerror = function(e) {
    alert("Sorry, an unforseen error was thrown.");
    console.log("***ERROR***");
    console.dir(e.target);
  };

  handleSeed();
};

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

האירוע onungradeneeded יפעל באופן אוטומטי בפעם הראשונה שנריץ את הסקריפט. אחרי שהוא מופעל, או כשהוא מדלג בהפעלות עתידיות, ה-handler של onsuccess פועל. הגדירנו handler של שגיאות פשוט (ומכוער) ואז אנחנו נקרא handleSeed.

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

תן לי קצת נתונים!

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

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

function handleSeed() {
  // This is how we handle the initial data seed. Normally this would be via AJAX.

  db.transaction(["employee"], "readonly").objectStore("employee").count().onsuccess = function(e) {
    var count = e.target.result;
    if (count == 0) {
      console.log("Need to generate fake data - stand by please...");
      $("#status").text("Please stand by, loading in our initial data.");
      var done = 0;
      var employees = db.transaction(["employee"], "readwrite").objectStore("employee");
      // Generate 1k people
      for (var i = 0; i < 1000; i++) {
         var person = generateFakePerson();
         // Modify our data to add a searchable field
         person.searchkey = person.lastname.toLowerCase();
         resp = employees.add(person);
         resp.onsuccess = function(e) {
           done++;
           if (done == 1000) {
             $("#name").removeAttr("disabled");
             $("#status").text("");
             setupAutoComplete();
           } else if (done % 100 == 0) {
             $("#status").text("Approximately "+Math.floor(done/10) +"% done.");
           }
         }
      }
    } else {
      $("#name").removeAttr("disabled");
      setupAutoComplete();
    }
  };
}

השורה הראשונה היא קצת מורכבת, כי יש לנו מספר פעולות מחוברות זו לזו:

db.transaction(["employee"], "readonly");

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

objectStore("employee");

קבל את אחסון האובייקטים של העובד.

count()

מריצים את ה-count API – שאפשר לנחש – מבצע ספירה.

onsuccess = function(e) {

בסיום, צריך לבצע את הקריאה החוזרת (callback). בתוך הקריאה החוזרת אנחנו יכולים לקבל את ערך התוצאה שהוא מספר האובייקטים. אם הספירה הייתה אפס, נתחיל את תהליך המקור.

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

{
  firstname: "Random Name",
  lastname: "Some Random Last Name",
  department: "One of 8 random departments",
  email: "first letter of firstname+lastname@fakecorp.com"
}

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

// Modify our data to add a searchable field
person.searchkey = person.lastname.toLowerCase();

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

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

לאחר שהמקור מסתיים, החלק הבא של האפליקציה מופעל – setupAutoComplete.

יצירת ההשלמה האוטומטית

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

function setupAutoComplete() {

  //Create the autocomplete
  $("#name").autocomplete({
    source: function(request, response) {

      console.log("Going to look for "+request.term);

      $("#displayEmployee").hide();

      var transaction = db.transaction(["employee"], "readonly");
      var result = [];

      transaction.oncomplete = function(event) {
        response(result);
      };

      // TODO: Handle the error and return to it jQuery UI
      var objectStore = transaction.objectStore("employee");

      // Credit: http://stackoverflow.com/a/8961462/52160
      var range = IDBKeyRange.bound(request.term.toLowerCase(), request.term.toLowerCase() + "z");
      var index = objectStore.index("searchkey");

      index.openCursor(range).onsuccess = function(event) {
        var cursor = event.target.result;
        if(cursor) {
          result.push({
            value: cursor.value.lastname + ", " + cursor.value.firstname,
            person: cursor.value
          });
          cursor.continue();
        }
      };
    },
    minLength: 2,
    select: function(event, ui) {
      $("#displayEmployee").show().html(template(ui.item.person));
    }
  });

}

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

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

אנחנו מתחילים ביצירת טרנזקציה לקריאה בלבד, מערך שנקרא result ו-handler מלא שמעביר את התוצאה לבקרת ההשלמה האוטומטית.

כדי למצוא פריטים שתואמים לקלט שלנו, נשתמש בטיפ מאת משתמש StackOverflow, פונג-ואן צ'או (Fong-Wan Chau): אנחנו משתמשים בטווח אינדקסים שמבוסס על הקלט כגבול תחתון, והקלט יחד עם האות z כגבול העליון של הטווח. שימו לב: אנחנו משתמשים במונח באותיות קטנות כך שיתאים לנתונים שהזנתם באותיות קטנות.

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

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