מבוא
IndexedDB הוא כלי עוצמתי לאחסון נתונים בצד הלקוח. אם עוד לא עיינת בנושא, כדאי לקרוא את המדריכים השימושיים בנושא MDN בנושא. במאמר הזה נדרש ידע בסיסי ב-API ובתכונות. גם אם לא נתקלתם ב-IndexedDB בעבר, סרטון ההדגמה במאמר הזה יעזור לכם להבין מה אפשר לעשות איתו.
ההדגמה שלנו היא הוכחה פשוטה לתפיסה של אפליקציה באינטראנט של חברה. האפליקציה תאפשר לעובדים לחפש עובדים אחרים. כדי לספק חוויה מהירה ומהירה יותר, מסדי הנתונים של העובדים מועתקים למכונה של הלקוח ונשמרים באמצעות IndexedDB. הדמו מספק פשוט חיפוש בסגנון מילוי אוטומטי והצגה של רשומת עובד אחת, אבל היתרון הוא שכאשר הנתונים האלה זמינים בצד הלקוח, אפשר להשתמש בהם גם בדרכים נוספות. לפניכם סקירה כללית בסיסית של הפעולות שהאפליקציה שלנו צריכה לבצע.
- אנחנו צריכים להגדיר ולהפעיל מופע של IndexedDB. ברוב המקרים זה פשוט, אבל לפעמים קצת מסובך לגרום ל-Chrome לעבוד גם ב-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>
לא הרבה, נכון? ממשק המשתמש הזה כולל שלושה היבטים עיקריים שחשובים לנו. הראשון הוא השדה 'שם' שישמש להשלמה אוטומטית. הוא נטען במצב מושבת ויופעל מאוחר יותר באמצעות 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". כשפותחים את ה-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
, אנחנו בודקים את objectStoreNames, מערך של מאגרי אובייקטים, כדי לראות אם הוא מכיל עובד. אם לא, אנחנו פשוט גורמים לו לעשות זאת. הקריאה createIndex חשובה. אנחנו צריכים לומר ל-IndexedDB באילו שיטות, מלבד מפתחות, נשתמש כדי לאחזר נתונים. נשתמש במשתנה שנקרא searchkey. נסביר את זה בקצרה.
האירוע onungradeneeded
ירוץ באופן אוטומטי בפעם הראשונה שנריץ את הסקריפט. אחרי שהוא מבוצע, או מושמט בהפעלות הבאות, הטיפול onsuccess
מופעל. הגדרנו טיפול פשוט (ומכוער) בשגיאות, ואז אנחנו קוראים ל-handleSeed
.
לפני שנמשיך, נסביר במהירות מה קורה כאן. פותחים את מסד הנתונים. אנחנו בודקים אם מאגר האובייקטים קיים. אם לא, אנחנו יוצרים אותה. לבסוף, אנחנו קוראים לפונקציה בשם handleSeed. עכשיו נעבור לחלק של הדגמה שבו נספק נתונים.
אני רוצה לקבל כמה נתונים!
כפי שצוין במבוא במאמר הזה, ההדגמה הזו יוצרת מחדש אפליקציה בסגנון אינטראנט שצריכה לאחסן עותק של כל העובדים הידועים. בדרך כלל, כדי לעשות זאת צריך ליצור ממשק 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) {
בסיום, מריצים את הקריאה החוזרת. בתוך הקריאה החוזרת (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 לא מספק דרך לחפש פריטים ללא התחשבות ברישיות. לכן אנחנו יוצרים עותק של השדה 'שם משפחה' למאפיין חדש, מפתח חיפוש. אם זוכרים, זהו המפתח שאמרנו שצריך ליצור כאינדקס לנתונים שלנו.
// Modify our data to add a searchable field
person.searchkey = person.lastname.toLowerCase();
מאחר שמדובר בשינוי ספציפי ללקוח, הוא מתבצע כאן ולא בשרת העורפי (או במקרה שלנו, בשרת העורפי המדומה).
כדי לבצע הוספות למסד הנתונים בצורה ביצועית, עליכם לעשות שימוש חוזר בטרנזקציה לכל הפריטים באצווה בפעולות כתיבה. אם יוצרים טרנזקציה חדשה לכל כתיבה, הדפדפן עלול לגרום לכתיבה בדיסק של כל עסקה, וזה יגרום לביצועים גרועים כשמוסיפים הרבה פריטים (למשל, "דקה אחת לכתוב 1000 אובייקטים" – נורא).
לאחר השלמת המקור, החלק הבא של האפליקציה מופעל – 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 (תוצאה) ו-handler הושלם (handler) שמעביר את התוצאה לפקד ההשלמה האוטומטית.
כדי למצוא פריטים שתואמים לקלט שלנו, נשתמש בטיפים של המשתמש Fong-Wan Chau ב-StackOverflow: אנחנו משתמשים בטווח אינדקס שמבוסס על הקלט כגבול קצה נמוך, ובקלט בתוספת האות z כגבול קצה עליון. חשוב גם לשים לב שהמונח באותיות קטנות כדי להתאים אותו לנתונים שהוזנו באותיות קטנות.
בסיום, נוכל לפתוח סרגל תנועה (אפשר לחשוב עליו כעל הרצת שאילתה במסד נתונים) ולעבור על התוצאות. אמצעי הבקרה של השלמה אוטומטית ב-jQuery UI מאפשרים להחזיר כל סוג של נתונים שרוצים, אבל נדרש לפחות מפתח ערך. אנחנו מגדירים את הערך כגרסה של השם בפורמט נוח. אנחנו גם מחזירים את כל האדם. מיד נסביר למה. קודם כול, הנה צילום מסך של ההשלמה האוטומטית בפעולה. אנחנו משתמשים בעיצוב Vader בממשק המשתמש של jQuery.
זה מספיק בפני עצמו כדי להחזיר את התוצאות של ההתאמות ב-IndexedDB להשלמה האוטומטית. אבל אנחנו רוצים גם לתמוך בהצגת תצוגת פרטים של ההתאמה כשבוחרים אחת. צייינו בורר טיפול כשיצרנו את ההשלמה האוטומטית שמשתמשת בתבנית Handlebars מקודם.