בשלב הזה תלמדו:
- איך להתאים אפליקציית אינטרנט קיימת לפלטפורמת אפליקציות Chrome.
- איך מוודאים שהסקריפטים של האפליקציה עומדים בדרישות של Content Security Policy (CSP).
- איך מטמיעים אחסון מקומי באמצעות chrome.storage.local.
זמן משוער להשלמת השלב הזה: 20 דקות.
כדי לראות מה תצטרכו להשלים בשלב הזה, אפשר לגלול למטה לתחתית הדף ↓.
ייבוא אפליקציית Todo קיימת
בתור התחלה, מייבאים לפרויקט את גרסת ה-JavaScript וניל של TodoMVC, אפליקציה נפוצה לבנצ'מרק.
כללנו גרסה של אפליקציית TodoMVC בקובץ ה-zip של קוד העזר בתיקייה todomvc. מעתיקים את כל הקבצים (כולל התיקיות) מ-todomvc לתיקיית הפרויקט.
תתבקשו להחליף את index.html. עכשיו צריך לאשר.
עכשיו אמור להיות בתיקיית האפליקציות מבנה הקבצים הבא:
הקבצים שמודגשים בכחול הם מהתיקייה todomvc.
טוענים מחדש את האפליקציה עכשיו (לוחצים לחיצה ימנית > טוענים מחדש את האפליקציה). אתם אמורים לראות את ממשק המשתמש הבסיסי, אבל לא תוכלו להוסיף משימות.
איך מוודאים שהסקריפטים תואמים למדיניות Content Security Policy (CSP)
פותחים את מסוף כלי הפיתוח (לוחצים לחיצה ימנית > בדיקת הרכיב ולאחר מכן בוחרים בכרטיסייה מסוף). תוצג שגיאה לגבי דחייה של הפעלת סקריפט בקוד:
כדי לפתור את השגיאה הזו, נצטרך לוודא שהאפליקציה עומדת בדרישות של מדיניות אבטחת התוכן. אחת מהסיבות הנפוצות ביותר לפעולות שלא עומדות בדרישות של CSP היא JavaScript מוטמע. דוגמאות ל-JavaScript בשורה אחת כוללות בוררי אירועים כמאפייני DOM (למשל <button onclick=''>
) ותגי <script>
עם תוכן בתוך ה-HTML.
הפתרון פשוט: מעבירים את התוכן המוטבע לקובץ חדש.
1. ליד החלק התחתון של הדף index.html, מסירים את קוד ה-JavaScript המוטבע ובמקום זאת כוללים את js/shoestrap.js:
<script src="bower_components/director/build/director.js"></script>
<script>
// Bootstrap app data
window.app = {};
</script>
<script src="js/bootstrap.js"></script>
<script src="js/helpers.js"></script>
<script src="js/store.js"></script>
2. יוצרים קובץ בתיקייה js בשם bootrap.js. מעבירים את הקוד שקודם היה בשורה אחת לקובץ הזה:
// Bootstrap app data
window.app = {};
אם תטעינו מחדש את האפליקציה עכשיו, היא עדיין לא תפעל, אבל אנחנו מתקרבים לפתרון.
המרת localStorage ל-chrome.storage.local
אם תפתחו עכשיו את מסוף כלי הפיתוח, השגיאה הקודמת אמורה להיעלם. עם זאת, יש שגיאה חדשה לגבי window.localStorage
שלא זמין:
אפליקציות Chrome לא תומכות ב-localStorage
כי localStorage
הוא אסינכרוני. גישה סינכרונית לחסימת משאבים (I/O) בסביבת זמן ריצה עם שרשור יחיד עלולה לגרום לאפליקציה להגיב.
לאפליקציות Chrome יש ממשק API מקביל שיכול לאחסן אובייקטים באופן אסינכרוני. כך תוכלו להימנע מתהליך היקר של אובייקט->string->אובייקטים.
כדי לטפל בהודעת השגיאה באפליקציה שלנו, צריך להמיר את localStorage
ל-chrome.storage.local.
עדכון ההרשאות של האפליקציה
כדי להשתמש ב-chrome.storage.local
, צריך לבקש את ההרשאה storage
. בקובץ manifest.json, מוסיפים את "storage"
למערך permissions
:
"permissions": ["storage"],
מידע על local.storage.set() ו-local.storage.get()
כדי לשמור ולשלוף פריטים של 'דברים שצריך לעשות', צריך להכיר את השיטות set()
ו-get()
של API chrome.storage
.
המתודה set() מקבלת אובייקט של צמדי מפתח-ערך כפרמטר הראשון. הפרמטר השני הוא פונקציית קריאה חוזרת אופציונלית. לדוגמה:
chrome.storage.local.set({secretMessage:'Psst!',timeSet:Date.now()}, function() {
console.log("Secret message saved");
});
השיטה get() מקבלת פרמטר ראשון אופציונלי למפתחות של מאגר הנתונים שרוצים לאחזר. אפשר להעביר מפתח יחיד כמחרוזת, ומספר מפתחות אפשר לארגן במערך של מחרוזות או באובייקט מילון.
הפרמטר השני, שהוא חובה, הוא פונקציית קריאה חוזרת. באובייקט המוחזר, משתמשים במפתחות שביקשת בפרמטר הראשון כדי לגשת לערכים השמורים. לדוגמה:
chrome.storage.local.get(['secretMessage','timeSet'], function(data) {
console.log("The secret message:", data.secretMessage, "saved at:", data.timeSet);
});
אם רוצים get()
את כל מה שנמצא כרגע ב-chrome.storage.local
, צריך להשמיט את הפרמטר הראשון:
chrome.storage.local.get(function(data) {
console.log(data);
});
בניגוד ל-localStorage
, לא תוכלו לבדוק פריטים שמאוחסנים באופן מקומי באמצעות חלונית המשאבים של כלי הפיתוח. עם זאת, אפשר לבצע פעולות ב-chrome.storage
דרך מסוף JavaScript באופן הבא:
הצגה מקדימה של השינויים הנדרשים ב-API
רוב השלבים שנותרו בהמרת אפליקציית Todo הם שינויים קטנים בקריאות ל-API. צריך לשנות את כל המקומות שבהם נעשה כרגע שימוש ב-localStorage
, למרות שזה כרוך בזמן רב ובסיכון לשגיאות.
ההבדלים העיקריים בין localStorage
לבין chrome.storage
נובעים מהאופי האסינכרוני של chrome.storage
:
במקום לכתוב ל-
localStorage
באמצעות מטלה פשוטה, צריך להשתמש ב-chrome.storage.local.set()
עם קריאות חוזרות (callback) אופציונליות.var data = { todos: [] }; localStorage[dbName] = JSON.stringify(data);
לעומת
var storage = {}; storage[dbName] = { todos: [] }; chrome.storage.local.set( storage, function() { // optional callback });
במקום לגשת ישירות ל-
localStorage[myStorageName]
, צריך להשתמש ב-chrome.storage.local.get(myStorageName,function(storage){...})
ואז לנתח את האובייקטstorage
שהוחזר בפונקציית הקריאה החוזרת.var todos = JSON.parse(localStorage[dbName]).todos;
לעומת
chrome.storage.local.get(dbName, function(storage) { var todos = storage[dbName].todos; });
הפונקציה
.bind(this)
משמשת בכל פונקציות ה-call back כדי לוודא ש-this
מתייחס ל-this
של אב הטיפוסStore
. (מידע נוסף על פונקציות מקושרות זמין במסמכי העזרה של MDN: Function.prototype.bind()).function Store() { this.scope = 'inside Store'; chrome.storage.local.set( {}, function() { console.log(this.scope); // outputs: 'undefined' }); } new Store();
לעומת
function Store() { this.scope = 'inside Store'; chrome.storage.local.set( {}, function() { console.log(this.scope); // outputs: 'inside Store' }.bind(this)); } new Store();
חשוב לזכור את ההבדלים העיקריים האלה כשנלמד איך לאחזר, לשמור ולהסיר פריטים ברשימת המשימות בקטעים הבאים.
אחזור פריטים ברשימת המשימות
נעדכן את אפליקציית Todo כדי לאחזר את משימות המשימות:
1. שיטת המבנה (constructor) Store
מטפלת בהפעלה הראשונית של אפליקציית Todo עם כל הפריטים הקיימים של המשימות במאגר הנתונים. השיטה בודקת קודם אם מאגר הנתונים קיים. אם לא, היא תיצור מערך ריק של todos
ותשמור אותו במאגר הנתונים כדי שלא יהיו שגיאות קריאה בסביבת זמן הריצה.
בקובץ js/store.js, ממירים את השימוש ב-localStorage
ב-method של ה-constructor לשימוש ב-chrome.storage.local
:
function Store(name, callback) {
var data;
var dbName;
callback = callback || function () {};
dbName = this._dbName = name;
if (!localStorage[dbName]) {
data = {
todos: []
};
localStorage[dbName] = JSON.stringify(data);
}
callback.call(this, JSON.parse(localStorage[dbName]));
chrome.storage.local.get(dbName, function(storage) {
if ( dbName in storage ) {
callback.call(this, storage[dbName].todos);
} else {
storage = {};
storage[dbName] = { todos: [] };
chrome.storage.local.set( storage, function() {
callback.call(this, storage[dbName].todos);
}.bind(this));
}
}.bind(this));
}
2. השיטה find()
משמשת לקריאת משימות מהמודל. התוצאות שמוחזרות משתנות בהתאם לסינון לפי 'הכול', 'פעיל' או 'הושלמה'.
המרה של find()
לשימוש ב-chrome.storage.local
:
Store.prototype.find = function (query, callback) {
if (!callback) {
return;
}
var todos = JSON.parse(localStorage[this._dbName]).todos;
callback.call(this, todos.filter(function (todo) {
chrome.storage.local.get(this._dbName, function(storage) {
var todos = storage[this._dbName].todos.filter(function (todo) {
for (var q in query) {
return query[q] === todo[q];
}
});
callback.call(this, todos);
}.bind(this));
}));
};
3. בדומה ל-find()
, הפונקציה findAll()
מקבלת את כל המשימות מהדגם. המרה של findAll()
לשימוש ב-chrome.storage.local
:
Store.prototype.findAll = function (callback) {
callback = callback || function () {};
callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
chrome.storage.local.get(this._dbName, function(storage) {
var todos = storage[this._dbName] && storage[this._dbName].todos || [];
callback.call(this, todos);
}.bind(this));
};
שמירת פריטים ב'דברים שצריך לעשות'
השיטה הנוכחית של save()
מציבה אתגר. הדבר תלוי בשתי פעולות אסינכרוניות (get and set) שפועלות על כל אחסון ה-JSON המונוליתי בכל פעם. כל עדכון בכמות גדולה על יותר מפריט משימות אחד, כמו "mark כל המשימות לביצוע משימות שהושלמו", יגרום לסכנת נתונים שנקראת Read-After-Write. הבעיה הזו לא הייתה מתרחשת אם היינו משתמשים באחסון נתונים מתאים יותר, כמו IndexedDB, אבל אנחנו מנסים לצמצם את מאמץ ההמרה ב-Codelab הזה.
יש כמה דרכים לתקן את הבעיה, לכן נשתמש בהזדמנות הזו כדי לבצע שינויים קלים ב-save()
, על ידי שימוש במערך של מזהי משימות שצריך לעדכן בבת אחת:
1. כדי להתחיל, צריך לעטוף את כל מה שכבר נמצא ב-save()
ב-callback של chrome.storage.local.get()
:
Store.prototype.save = function (id, updateData, callback) {
chrome.storage.local.get(this._dbName, function(storage) {
var data = JSON.parse(localStorage[this._dbName]);
// ...
if (typeof id !== 'object') {
// ...
}else {
// ...
}
}.bind(this));
};
2. ממירים את כל המופעים של localStorage
באמצעות chrome.storage.local
:
Store.prototype.save = function (id, updateData, callback) {
chrome.storage.local.get(this._dbName, function(storage) {
var data = JSON.parse(localStorage[this._dbName]);
var data = storage[this._dbName];
var todos = data.todos;
callback = callback || function () {};
// If an ID was actually given, find the item and update each property
if ( typeof id !== 'object' ) {
// ...
localStorage[this._dbName] = JSON.stringify(data);
callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
chrome.storage.local.set(storage, function() {
chrome.storage.local.get(this._dbName, function(storage) {
callback.call(this, storage[this._dbName].todos);
}.bind(this));
}.bind(this));
} else {
callback = updateData;
updateData = id;
// Generate an ID
updateData.id = new Date().getTime();
localStorage[this._dbName] = JSON.stringify(data);
callback.call(this, [updateData]);
chrome.storage.local.set(storage, function() {
callback.call(this, [updateData]);
}.bind(this));
}
}.bind(this));
};
3. לאחר מכן מעדכנים את הלוגיקה כך שתפעול על מערך במקום על פריט יחיד:
Store.prototype.save = function (id, updateData, callback) {
chrome.storage.local.get(this._dbName, function(storage) {
var data = storage[this._dbName];
var todos = data.todos;
callback = callback || function () {};
// If an ID was actually given, find the item and update each property
if ( typeof id !== 'object' || Array.isArray(id) ) {
var ids = [].concat( id );
ids.forEach(function(id) {
for (var i = 0; i < todos.length; i++) {
if (todos[i].id == id) {
for (var x in updateData) {
todos[i][x] = updateData[x];
}
}
}
});
chrome.storage.local.set(storage, function() {
chrome.storage.local.get(this._dbName, function(storage) {
callback.call(this, storage[this._dbName].todos);
}.bind(this));
}.bind(this));
} else {
callback = updateData;
updateData = id;
// Generate an ID
updateData.id = new Date().getTime();
todos.push(updateData);
chrome.storage.local.set(storage, function() {
callback.call(this, [updateData]);
}.bind(this));
}
}.bind(this));
};
סימון פריטים ברשימת המשימות כ'בוצעו'
עכשיו האפליקציה פועלת על מערכים, צריך לשנות את האופן שבו האפליקציה מטפלת במשתמש כשלוחצים על הלחצן נקה הושלמה (#):
1. בקובץ controller.js, מעדכנים את toggleAll()
כך שיפעיל את toggleComplete()
רק פעם אחת עם מערך של משימות במקום לסמן כל משימה כ'הושלמה' בנפרד. צריך למחוק גם את השיחה אל _filter()
, כי תצטרכו לשנות את toggleComplete
_filter()
.
Controller.prototype.toggleAll = function (e) {
var completed = e.target.checked ? 1 : 0;
var query = 0;
if (completed === 0) {
query = 1;
}
this.model.read({ completed: query }, function (data) {
var ids = [];
data.forEach(function (item) {
this.toggleComplete(item.id, e.target, true);
ids.push(item.id);
}.bind(this));
this.toggleComplete(ids, e.target, false);
}.bind(this));
this._filter();
};
2. עכשיו מעדכנים את toggleComplete()
כך שיקבל גם משימה אחת וגם מערך של משימות. זה כולל העברה של filter()
לתוך update()
, במקום מחוץ לו.
Controller.prototype.toggleComplete = function (ids, checkbox, silent) {
var completed = checkbox.checked ? 1 : 0;
this.model.update(ids, { completed: completed }, function () {
if ( ids.constructor != Array ) {
ids = [ ids ];
}
ids.forEach( function(id) {
var listItem = $$('[data-id="' + id + '"]');
if (!listItem) {
return;
}
listItem.className = completed ? 'completed' : '';
// In case it was toggled from an event and not by clicking the checkbox
listItem.querySelector('input').checked = completed;
});
if (!silent) {
this._filter();
}
}.bind(this));
};
Count todo items
After switching to async storage, there is a minor bug that shows up when getting the number of todos. You'll need to wrap the count operation in a callback function:
1. In model.js, update getCount()
to accept a callback:
Model.prototype.getCount = function (callback) {
var todos = {
active: 0,
completed: 0,
total: 0
};
this.storage.findAll(function (data) {
data.each(function (todo) {
if (todo.completed === 1) {
todos.completed++;
} else {
todos.active++;
}
todos.total++;
});
if (callback) callback(todos);
});
return todos;
};
2. Back in controller.js, update _updateCount()
to use the async getCount()
you edited in
the previous step:
Controller.prototype._updateCount = function () {
var todos = this.model.getCount();
this.model.getCount(function(todos) {
this.$todoItemCounter.innerHTML = this.view.itemCounter(todos.active);
this.$clearCompleted.innerHTML = this.view.clearCompletedButton(todos.completed);
this.$clearCompleted.style.display = todos.completed > 0 ? 'block' : 'none';
this.$toggleAll.checked = todos.completed === todos.total;
this._toggleFrame(todos);
}.bind(this));
};
You are almost there! If you reload the app now, you will be able to insert new todos without any console errors.
Remove todos items
Now that the app can save todo items, you're close to being done! You still get errors when you attempt to remove todo items:
1. In store.js, convert all the localStorage
instances to use chrome.storage.local
:
a) To start off, wrap everything already inside remove()
with a get()
callback:
Store.prototype.remove = function (id, callback) {
chrome.storage.local.get(this._dbName, function(storage) {
var data = JSON.parse(localStorage[this._dbName]);
var todos = data.todos;
for (var i = 0; i < todos.length; i++) {
if (todos[i].id == id) {
todos.splice(i, 1);
break;
}
}
localStorage[this._dbName] = JSON.stringify(data);
callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
}.bind(this));
};
b) Then convert the contents within the get()
callback:
Store.prototype.remove = function (id, callback) {
chrome.storage.local.get(this._dbName, function(storage) {
var data = JSON.parse(localStorage[this._dbName]);
var data = storage[this._dbName];
var todos = data.todos;
for (var i = 0; i < todos.length; i++) {
if (todos[i].id == id) {
todos.splice(i, 1);
break;
}
}
localStorage[this._dbName] = JSON.stringify(data);
callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
chrome.storage.local.set(storage, function() {
callback.call(this, todos);
}.bind(this));
}.bind(this));
};
2. The same Read-After-Write data hazard issue previously present in the save()
method is also
present when removing items so you will need to update a few more places to allow for batch
operations on a list of todo IDs.
a) Still in store.js, update remove()
:
Store.prototype.remove = function (id, callback) {
chrome.storage.local.get(this._dbName, function(storage) {
var data = storage[this._dbName];
var todos = data.todos;
var ids = [].concat(id);
ids.forEach( function(id) {
for (var i = 0; i < todos.length; i++) {
if (todos[i].id == id) {
todos.splice(i, 1);
break;
}
}
});
chrome.storage.local.set(storage, function() {
callback.call(this, todos);
}.bind(this));
}.bind(this));
};
b) In controller.js, change removeCompletedItems()
to make it call removeItem()
on all IDs
at once:
Controller.prototype.removeCompletedItems = function () {
this.model.read({ completed: 1 }, function (data) {
var ids = [];
data.forEach(function (item) {
this.removeItem(item.id);
ids.push(item.id);
}.bind(this));
this.removeItem(ids);
}.bind(this));
this._filter();
};
c) Finally, still in controller.js, change the removeItem()
to support removing multiple items
from the DOM at once, and move the _filter()
call to be inside the callback:
Controller.prototype.removeItem = function (id) {
this.model.remove(id, function () {
var ids = [].concat(id);
ids.forEach( function(id) {
this.$todoList.removeChild($$('[data-id="' + id + '"]'));
}.bind(this));
this._filter();
}.bind(this));
this._filter();
};
הסרת כל הפריטים לביצוע משימות
יש שיטה נוספת ב-store.js שמשתמשת ב-localStorage
:
Store.prototype.drop = function (callback) {
localStorage[this._dbName] = JSON.stringify({todos: []});
callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
};
לא קוראים לשיטה הזו באפליקציה הנוכחית. אם אתם רוצים אתגר נוסף, נסו
להטמיע אותה בעצמכם. רמז: כדאי לעיין במאמר chrome.storage.local.clear()
.
הפעלת אפליקציית Todo שהשלמתם
סיימתם את שלב 2. צריך לטעון מחדש את האפליקציה, ועכשיו אמורה להיות לכם גרסת TodoMVC ארוזת ל-Chrome שפועלת בצורה תקינה.
אפשר לקבל מידע נוסף
למידע מפורט יותר על כמה מממשקי ה-API שהוצגו בשלב הזה, אפשר לעיין במאמרים הבאים:
- מדיניות אבטחת תוכן ↑
- הצהרה על הרשאות ↑
- chrome.storage ↑
- chrome.storage.local.get() ↑
- chrome.storage.local.set() ↑
- chrome.storage.local.remove() ↑
- chrome.storage.local.clear() ↑
רוצה להמשיך לשלב הבא? עוברים אל שלב 3 – הוספת התראות וזעקות השכמה »