בשלב זה נלמד:
- כיצד להתאים אפליקציית אינטרנט קיימת לפלטפורמת Chrome Apps.
- איך לוודא שהסקריפטים של האפליקציה תואמים למדיניות Content Security Policy (CSP) של סקריפטים של אפליקציות.
- כיצד להטמיע אחסון מקומי באמצעות chrome.storage.local.
זמן משוער להשלמת השלב הזה: 20 דקות.
כדי לראות תצוגה מקדימה של מה שתבצעו בשלב הזה, דלגו לתחתית הדף הזה ↓.
ייבוא אפליקציה קיימת של Todo
בתור התחלה, כדאי לייבא לפרויקט את גרסת ה-JavaScript של וניל של TodoMVC, אפליקציה נפוצה להשוואה לשוק.
כללנו גרסה של אפליקציית TodoMVC במיקוד של קוד ההפניה שבתיקייה todomvc. מעתיקים את כל הקבצים (כולל תיקיות) מ-todomvc לתיקיית הפרויקט.
תתבקשו להחליף את index.html. עליך לאשר.
כעת מבנה הקובץ אמור להיות במבנה הבא בתיקיית האפליקציות:
הקבצים המודגשים בכחול הם מהתיקייה todomvc.
כדאי לטעון מחדש את האפליקציה עכשיו (לחיצה ימנית > טעינת האפליקציה מחדש). ממשק המשתמש הבסיסי אמור להופיע, אבל לא תוכלו להוסיף משימות לביצוע.
תאימות למדיניות Content Security Policy (CSP) של סקריפטים
פותחים את מסוף כלי הפיתוח (לוחצים לחיצה ימנית > Inspect Element, ולאחר מכן בוחרים בכרטיסייה Console). תופיע הודעת שגיאה לגבי סירוב להפעיל סקריפט מוטבע:
כדי לתקן את השגיאה הזו, עליך להתאים את האפליקציה ל-Content Security Policy. אחת הסיבות הנפוצות ביותר לאי-תאימות של 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 בשם shoestrap.js. מעבירים את הקוד שהוטמע קודם לקובץ הזה:
// Bootstrap app data
window.app = {};
אם תטען מחדש את האפליקציה עכשיו ואז התקרבת עוד קצת, תמשיך להיות לך אפליקציית Todo שאינה פועלת.
המרת 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()
של chrome.storage
API.
השיטה 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
, לא תוכלו לבדוק פריטים שמאוחסנים באופן מקומי באמצעות החלונית
Resources DevTools. עם זאת, אפשר לקיים אינטראקציה עם chrome.storage
מתוך JavaScript Console באופן הבא:
תצוגה מקדימה של השינויים הנדרשים ב-API
רוב השלבים שנותרו בהמרת האפליקציה Todo הם שינויים קטנים בקריאות ל-API. צריך להחליף את כל המקומות שבהם localStorage
נמצא כרגע בשימוש, על אף שפעולות כאלה דורשות זמן רב ומובילות לשגיאות.
ההבדלים העיקריים בין localStorage
לבין chrome.storage
נובעים מאופי האסינכרוני של chrome.storage
:
במקום לכתוב אל
localStorage
באמצעות מטלה פשוטה, צריך להשתמש בפונקציהchrome.storage.local.set()
עם אפשרות להתקשרות חזרה.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)
משמשת בכל הקריאות החוזרות (callback) כדי להבטיח ש-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
בשיטת ה-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 ו-set) שפועלות בכל אחסון ה-JSON המונוליתי בכל פעם. כל עדכון אצווה ביותר מפריט אחד לביצוע, כמו "סימון כל המשימות כמשימות שהושלמו", יוביל לסכנת נתונים שנקראת 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));
};
סימון שמשימות לביצוע
עכשיו, שהאפליקציה פועלת במערכים, צריך לשנות את אופן הטיפול של האפליקציה במשתמש שילחץ על הלחצן Clearהשלמה (#):
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()
.
הפעלת האפליקציה הסתיימה של משימות לביצוע
סיימת את שלב 2! טוענים מחדש את האפליקציה כדי שגרסת TodoMVC תכיל חבילה מלאה של Chrome.
אפשר לקבל מידע נוסף
לקבלת מידע מפורט יותר על חלק מממשקי ה-API שהוצגו בשלב הזה, עיינו במקורות הבאים:
- מדיניות אבטחת תוכן ↑
- הצהרת הרשאות ↑
- chrome.storage ↑
- chrome.storage.local.get() ↑
- chrome.storage.local.set() ↑
- chrome.storage.local.remove() ↑
- chrome.storage.local.clear() ↑
מוכנים להמשיך לשלב הבא? עבור אל שלב 3 – הוספת שעונים מעוררים והתראות »