מבוא
IndexedDB היא דרך יעילה לאחסון נתונים בצד הלקוח. אם עדיין לא קראת אותם, מומלץ לקרוא את מדריכי MDN השימושיים בנושא. במאמר הזה נדרש ידע בסיסי ב-API ובתכונות. גם אם לא נתקלתם ב-IndexedDB בעבר, סרטון ההדגמה במאמר הזה יעזור לכם להבין מה אפשר לעשות איתו.
הדמו שלנו הוא אפליקציית אינטראנט פשוטה להוכחת היתכנות לחברה. האפליקציה תאפשר לעובדים לחפש עובדים אחרים. כדי לספק חוויה מהירה ומהירה יותר, מסדי הנתונים של העובדים מועתקים למכונה של הלקוח ונשמרים באמצעות IndexedDB. הדמו מספק פשוט חיפוש בסגנון מילוי אוטומטי והצגה של רשומת עובד אחת, אבל היתרון הוא שכאשר הנתונים האלה יהיו זמינים אצל הלקוח, נוכל להשתמש בהם גם בדרכים נוספות. לפניכם סקירה כללית בסיסית של הפעולות שהאפליקציה שלנו צריכה לבצע.
- אנחנו צריכים להגדיר ולהפעיל מופע של IndexedDB. ברוב המקרים זה פשוט, אבל כדי שהתכונה תפעל גם ב-Chrome וגם ב-Firefox, צריך לבצע כמה פעולות.
- אנחנו צריכים לבדוק אם יש לנו נתונים, ואם לא, להוריד אותם. בדרך כלל, הפעולה הזו מתבצעת באמצעות קריאות AJAX. בדגמה שלנו יצרנו סוג פשוט של כלי כדי ליצור במהירות נתונים מזויפים. האפליקציה תצטרך לזהות מתי היא יוצרת את הנתונים האלה ולמנוע מהמשתמש להשתמש בנתונים עד אז. זו פעולה חד-פעמית. בפעם הבאה שהמשתמש יפעיל את האפליקציה, לא יהיה צורך לעבור את התהליך הזה. הדגמה מתקדמת יותר תעסוק בפעולות סנכרון בין הלקוח לשרת, אבל הדגמה הזו מתמקדת יותר בהיבטים של ממשק המשתמש.
- כשהאפליקציה מוכנה, אפשר להשתמש ברכיב Autocomplete של jQuery UI כדי לסנכרן עם 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. ה-span שלצידו משמש במהלך הזריעה הראשונית כדי לספק עדכונים למשתמש. לבסוף, ה-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 אחד יכולים להיות הרבה objectStores, כל אחד מהם מכיל אוסף של אובייקטים קשורים. הדגמה שלנו פשוטה, ונדרש לה רק 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
ירוץ באופן אוטומטי בפעם הראשונה שתפעילו את הסקריפט. אחרי שהיא מבוצעת, או אחרי שהיא מופעלת בפעמים הבאות, הטיפול onsuccess
מופעל. הגדרנו טיפול פשוט (ומכוער) בשגיאות, ואז אנחנו קוראים ל-handleSeed
.
לפני שנמשיך, נסביר במהירות מה קורה כאן. פותחים את מסד הנתונים. אנחנו בודקים אם מאגר האובייקטים קיים. אם לא, אנחנו יוצרים אותה. לבסוף, אנחנו קוראים לפונקציה בשם handleSeed. עכשיו נעבור לחלק של הדגמה שבו אנו יוצרים נתונים.
Gimme Some Data!
כפי שצוין בהקדמה של המאמר, הדגמה הזו יוצרת מחדש אפליקציה בסגנון אינטראנט שצריכה לאחסן עותק של כל העובדים הידועים. בדרך כלל, כדי לעשות זאת צריך ליצור ממשק API מבוסס-שרת שיכול להחזיר את מספר העובדים ולספק לנו דרך לאחזר קבוצות של רשומות. אפשר לדמיין שירות פשוט שתומך בספירת התחלה ומחזיר 100 אנשים בכל פעם. הפעולה הזו יכולה לפעול באופן אסינכרוני ברקע בזמן שהמשתמש עושה דברים אחרים.
בדגמה שלנו, אנחנו עושים משהו פשוט. אנחנו רואים כמה אובייקטים יש לנו ב-IndexedDB, אם יש כאלה. אם המספר נמוך ממספר מסוים, פשוט ניצור משתמשים מזויפים. אחרת, נחשב שהסתיים החלק של יצירת המקור ואנחנו יכולים להפעיל את החלק של השלמה אוטומטית בדמו. נבחן את 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()
מריצים את count API – כפי שאפשר לנחש, הוא מבצע ספירה.
onsuccess = function(e) {
בסיום, מריצים את הקריאה החוזרת. בתוך פונקציית הקריאה החוזרת אפשר לקבל את ערך התוצאה, שהוא מספר האובייקטים. אם המספר היה אפס, נתחיל בתהליך הזריעה.
אנחנו משתמשים ב-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.
יצירת ההשלמה האוטומטית
עכשיו מגיע החלק הכיפי – חיבור לפלאגין השלמה אוטומטית של jQuery UI. כמו ברוב הרכיבים של jQuery UI, אנחנו מתחילים ברכיב HTML בסיסי ומשתפרים אותו על ידי קריאה ל-method של ה-constructor. הוצאנו את התהליך כולו לפונקציה שנקראת 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) עם תשובה. אתם אחראים לשלוח מערך של תוצאות בחזרה לקריאה החוזרת.
הדבר הראשון שאנחנו עושים הוא להסתיר את ה-div של displayEmployee. ה-div הזה משמש להצגת עובד ספציפי, ואם כבר נטען עובד, הוא מנקה אותו. עכשיו אפשר להתחיל לחפש.
מתחילים ביצירת טרנזקציה לקריאה בלבד, מערך שנקרא result וטיפולון oncomplete שפשוט מעביר את התוצאה לבקרת ההשלמה האוטומטית.
כדי למצוא פריטים שתואמים לקלט שלנו, נשתמש בטיפים של המשתמש Fong-Wan Chau ב-StackOverflow: אנחנו משתמשים בטווח אינדקס שמבוסס על הקלט כגבול קצה נמוך, ובקלט בתוספת האות z כגבול קצה עליון. חשוב גם לציין שאנחנו הופכים את המונח לאותיות קטנות כדי להתאים לנתונים שהזנתם באותיות קטנות.
בסיום, נוכל לפתוח סרגל תנועה (אפשר לחשוב עליו כעל הרצת שאילתה במסד נתונים) ולעבור על התוצאות. אמצעי הבקרה של השלמה אוטומטית ב-jQuery UI מאפשרים להחזיר כל סוג של נתונים שרוצים, אבל נדרש לפחות מפתח ערך. אנחנו מגדירים את הערך לגרסה של השם בפורמט נוח. אנחנו גם מחזירים את כל האדם. מיד נסביר למה. קודם כול, הנה צילום מסך של ההשלמה האוטומטית בפעולה. אנחנו משתמשים בעיצוב Vader ל-jQuery UI.
זה מספיק בפני עצמו כדי להחזיר את התוצאות של ההתאמות שלנו ב-IndexedDB להשלמה האוטומטית. אבל אנחנו רוצים גם לתמוך בהצגת תצוגת פרטים של ההתאמה כשבוחרים אחת. צייינו בורר טיפול כשיצרנו את ההשלמה האוטומטית שמשתמשת בתבנית Handlebars מקודם.