מבוא
IndexedDB היא דרך יעילה לאחסן נתונים בצד הלקוח. אם עדיין לא עשיתם את זה, מומלץ לקרוא את המדריכים המועילים של MDN בנושא. המאמר הזה מניח שיש לכם ידע בסיסי על ממשקי ה-API והתכונות. גם אם לא נתקלתם ב-IndexedDB בעבר, אנחנו מקווים שההדגמה במאמר הזה תיתן לכם מושג מה אפשר לעשות איתו.
ההדגמה שלנו היא הוכחת היתכנות פשוטה של אפליקציית אינטראנט של חברה. האפליקציה תאפשר לעובדים לחפש עובדים אחרים. כדי לספק חוויה מהירה וחלקה יותר, מסד הנתונים של העובדים מועתק למחשב של הלקוח ונשמר באמצעות IndexedDB. ההדגמה פשוט מספקת חיפוש בסגנון השלמה אוטומטית ותצוגה של רשומת עובד בודדת, אבל מה שנחמד הוא שברגע שהנתונים האלה זמינים בלקוח, אנחנו יכולים להשתמש בהם גם בדרכים רבות אחרות. הנה תיאור בסיסי של מה שהאפליקציה שלנו צריכה לעשות.
- אנחנו צריכים להגדיר ולאתחל מכונה של IndexedDB. ברוב המקרים זה פשוט, אבל קצת יותר מסובך לגרום לזה לעבוד גם ב-Chrome וגם ב-Firefox.
- אנחנו צריכים לבדוק אם יש לנו נתונים, ואם לא, להוריד אותם. בדרך כלל, הפעולה הזו מתבצעת באמצעות קריאות AJAX. לצורך ההדגמה, יצרנו מחלקת שירות פשוטה כדי ליצור במהירות נתונים מדומים. האפליקציה תצטרך לזהות מתי היא יוצרת את הנתונים האלה ולמנוע מהמשתמש להשתמש בהם עד אז. התהליך הזה הוא חד פעמי. בפעם הבאה שהמשתמש יפעיל את האפליקציה, לא יהיה צורך לעבור את התהליך הזה. בהדגמה מתקדמת יותר, אפשר לטפל בפעולות הסנכרון בין הלקוח לשרת, אבל ההדגמה הזו מתמקדת יותר בהיבטים של ממשק המשתמש.
- כשהאפליקציה מוכנה, אנחנו יכולים להשתמש בפקד ההשלמה האוטומטית של ממשק המשתמש של 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>
לא הרבה, נכון? יש שלושה היבטים עיקריים בממשק המשתמש הזה שחשובים לנו. השדה הראשון הוא "name" (שם) שישמש להשלמה אוטומטית. הוא נטען כשהוא מושבת ומופעל בהמשך באמצעות JavaScript. הטווח שלידו משמש במהלך ההתחלה הראשונית כדי לספק עדכונים למשתמש. לבסוף, ה-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:
$(document).ready(function() {
console.log("Startup...");
...
});
בהדגמה שלנו נעשה שימוש ב-Handlebars.js כדי להציג את פרטי העובדים. נשתמש בזה רק בהמשך, אבל אפשר להמשיך ולקמפל את התבנית עכשיו כדי לסיים עם זה. יש לנו בלוק סקריפט שמוגדר כסוג שמוכר על ידי Handlebars. הוא לא מאוד מתוחכם, אבל הוא מקל על הצגת ה-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 נותנת לנו גישה לקריאה ולכתיבה של נתונים, אבל לפני שנעשה את זה, אנחנו צריכים לוודא שיש לנו objectStore. objectStore הוא כמו טבלה במסד נתונים. ל-IndexedDB אחד יכולים להיות הרבה מאגרי אובייקטים, וכל אחד מהם מכיל אוסף של אובייקטים קשורים. ההדגמה שלנו פשוטה וצריכה רק objectStore אחד שנקרא "employee". כשפותחים את indexedDB בפעם הראשונה, או כשמשנים את הגרסה בקוד, מופעל אירוע 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, אנחנו בודקים את objectStoreNames, מערך של מאגרי אובייקטים, כדי לראות אם הוא מכיל את employee. אם לא, אנחנו פשוט גורמים לו לעשות את זה. הקריאה createIndex חשובה. אנחנו צריכים להגדיר ל-IndexedDB באילו שיטות, מלבד מפתחות, נשתמש כדי לאחזר נתונים. נשתמש במפתח שנקרא searchkey. הסבר על כך מופיע בהמשך.
האירוע onungradeneeded יופעל באופן אוטומטי בפעם הראשונה שנריץ את הסקריפט. אחרי שהיא מופעלת, או אם מדלגים עליה בהרצות עתידיות, ה-handler של onsuccess מופעל. הגדרנו פונקציה פשוטה (ומכוערת) לטיפול בשגיאות, ואז אנחנו קוראים ל-handleSeed.
אז לפני שנמשיך, בואו נסקור במהירות מה קורה כאן. אנחנו פותחים את מסד הנתונים. אנחנו בודקים אם מאגר האובייקטים שלנו קיים. אם לא, אנחנו יוצרים אותו. לבסוף, אנחנו קוראים לפונקציה בשם handleSeed. עכשיו נתמקד בחלק של הוספת נתונים להדגמה.
אני רוצה לראות נתונים!
כפי שצוין במבוא למאמר הזה, ההדגמה הזו משחזרת אפליקציה בסגנון אינטראנט שצריכה לאחסן עותק של כל העובדים הידועים. בדרך כלל, זה כולל יצירת API מבוסס-שרת שיכול להחזיר את מספר העובדים ולספק לנו דרך לאחזר קבוצות של רשומות. אפשר לדמיין שירות פשוט שתומך בספירת התחלה ומחזיר 100 אנשים בכל פעם. התהליך הזה יכול לפעול באופן אסינכרוני ברקע בזמן שהמשתמש עושה דברים אחרים.
בהדגמה שלנו, נעשה משהו פשוט. אנחנו רואים כמה אובייקטים, אם בכלל, יש לנו ב-IndexedDB. אם המספר נמוך ממספר מסוים, פשוט ניצור משתמשים מזויפים. אחרת, אנחנו מחשיבים את החלק של ה-seed כגמור ואפשר להפעיל את החלק של ההשלמה האוטומטית בדמו. בואו נסתכל על handleSeed.
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()
מריצים את ה-API של הספירה, שכמו שאפשר לנחש, מבצע ספירה.
onsuccess = function(e) {
ובסיום – להפעיל את הקריאה החוזרת (callback) הזו. בתוך הקריאה החוזרת (callback) אנחנו יכולים לקבל את ערך התוצאה, שהוא מספר האובייקטים. אם הספירה הייתה אפס, אנחנו מתחילים בתהליך ההתחלתי.
אנחנו משתמשים ב-div של הסטטוס שהזכרנו קודם כדי להציג למשתמש הודעה שאנחנו עומדים להתחיל לקבל נתונים. בגלל האופי האסינכרוני של IndexedDB, הגדרנו משתנה פשוט, done, שיעקוב אחרי ההוספות. אנחנו עוברים בלולאה ומכניסים את האנשים המזויפים. המקור של הפונקציה הזו זמין בהורדה, אבל היא מחזירה אובייקט שנראה כך:
{
firstname: "Random Name",
lastname: "Some Random Last Name",
department: "One of 8 random departments",
email: "first letter of firstname+lastname@fakecorp.com"
}
המידע הזה לבדו מספיק כדי להגדיר אדם. אבל יש לנו דרישה מיוחדת כדי שנוכל לחפש בנתונים שלנו. ב-IndexedDB אין דרך לחפש פריטים באופן לא תלוי-רישיות. לכן, אנחנו יוצרים עותק של השדה lastname במאפיין חדש, searchkey. אם אתם זוכרים, זה המפתח שאמרנו שצריך ליצור כאינדקס לנתונים שלנו.
// Modify our data to add a searchable field
person.searchkey = person.lastname.toLowerCase();
מכיוון שזה שינוי ספציפי ללקוח, הוא מתבצע כאן ולא בשרת לקצה העורפי (או במקרה שלנו, בשרת הדמיוני לקצה העורפי).
כדי להוסיף נתונים למסד הנתונים בצורה יעילה, צריך להשתמש שוב בטרנזקציה לכל הכתיבות באצווה. אם יוצרים טרנזקציה חדשה לכל כתיבה, הדפדפן עלול לגרום לכתיבה בדיסק לכל טרנזקציה, וזה יפגע מאוד בביצועים כשמוסיפים הרבה פריטים (למשל, "דקה אחת לכתיבת 1,000 אובייקטים").
אחרי שההזנה מסתיימת, החלק הבא של האפליקציה מופעל – setupAutoComplete.
יצירת ההשלמה האוטומטית
עכשיו לחלק הכיפי – חיבור לפלאגין Autocomplete של jQuery UI. כמו ברוב ממשקי המשתמש של jQuery, אנחנו מתחילים עם רכיב HTML בסיסי ומשפרים אותו על ידי קריאה ל-constructor method. הפכנו את כל התהליך לפונקציה שנקראת 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));
}
});
}
החלק הכי מורכב בקוד הזה הוא יצירת נכס המקור. הפקד Autocomplete של jQuery UI מאפשר להגדיר מאפיין מקור שאפשר להתאים אישית כדי לענות על כל צורך אפשרי – אפילו נתוני IndexedDB שלנו. ה-API מספק לכם את הבקשה (בעצם מה שהוקלד בשדה הטופס) וקריאה חוזרת (callback) של התגובה. אתם אחראים לשלוח מערך של תוצאות בחזרה לאותו קריאה חוזרת (callback).
הדבר הראשון שאנחנו עושים הוא להסתיר את ה-div של displayEmployee. הוא משמש להצגת עובד בודד, ואם אחד כבר נטען, לנקות אותו. עכשיו אפשר להתחיל לחפש.
אנחנו מתחילים ביצירת טרנזקציה לקריאה בלבד, מערך שנקרא result ו-oncomplete handler שפשוט מעביר את התוצאה לפקד ההשלמה האוטומטית.
כדי למצוא פריטים שתואמים לקלט שלנו, נשתמש בטיפ של משתמש StackOverflow בשם Fong-Wan Chau: נשתמש בטווח אינדקס שמבוסס על הקלט כגבול התחתון, ועל הקלט בתוספת האות z כגבול העליון. שימו לב שגם המונח הזה מופיע באותיות קטנות, כדי להתאים לנתונים שהזנו באותיות קטנות.
אחרי שסיימנו, אנחנו יכולים לפתוח סמן (כמו הרצת שאילתה במסד נתונים) ולעבור על התוצאות. פקד ההשלמה האוטומטית של jQuery UI מאפשר להחזיר כל סוג של נתונים שרוצים, אבל דורש מפתח ערך מינימלי. אנחנו מגדירים את הערך לגרסה של השם בפורמט נחמד. אנחנו גם מחזירים את כל האדם. עוד רגע תבינו למה. קודם כול, הנה צילום מסך של ההשלמה האוטומטית בפעולה. אנחנו משתמשים בעיצוב Vader ל-jQuery UI.
זה מספיק כדי להחזיר את התוצאות של ההתאמות ב-IndexedDB להשלמה האוטומטית. אבל אנחנו רוצים גם לתמוך בהצגת תצוגה מפורטת של ההתאמה כשבוחרים אחת. ציינו handler לבחירה כשיצרנו את ההשלמה האוטומטית שמשתמשת בתבנית Handlebars שצוינה קודם.