בשלב הזה תלמדו:
- איך להתאים אפליקציית אינטרנט קיימת לפלטפורמת אפליקציות Chrome.
- איך לוודא שהסקריפטים של האפליקציה תואמים ל-Content Security Policy (CSP)
- איך מטמיעים אחסון מקומי באמצעות chrome.storage.local.
זמן משוער להשלמת השלב הזה: 20 דקות.
כדי לראות תצוגה מקדימה של מה שתשלימו בשלב הזה, דלגו לתחתית הדף הזה ↓.
ייבוא אפליקציית Todo קיימת
בתור נקודת התחלה, עליך לייבא את גרסת ה-JavaScript וניל של TodoMVC, שהיא נקודת השוואה נפוצה באפליקציה שלכם לפרויקט.
כללנו גרסה של האפליקציה TodoMVC במיקוד של קוד ההפניה שב-todomvc . מעתיקים את כל הקבצים (כולל תיקיות) מ-todomvc לתיקיית הפרויקט.
תתבקשו להחליף את index.html. עכשיו צריך לאשר.
עכשיו אמור להיות בתיקיית האפליקציות מבנה הקבצים הבא:
הקבצים שמודגשים בכחול הם מהתיקייה todomvc.
טוענים מחדש את האפליקציה עכשיו (לוחצים לחיצה ימנית > טוענים מחדש את האפליקציה). אתם אמורים לראות את ממשק המשתמש הבסיסי, אבל לא לראות אותו. להוסיף משימות לביצוע.
תאימות של סקריפטים למדיניות אבטחת התוכן (CSP)
פותחים את מסוף כלי הפיתוח (לוחצים לחיצה ימנית > בדיקת הרכיב ולאחר מכן בוחרים בכרטיסייה מסוף). שלך תוצג שגיאה לגבי סירוב להפעלה של סקריפט מוטבע:
כדי לתקן את השגיאה הזו, עליך לוודא שהאפליקציה תואמת ל-Content Security Policy. אחד
אי-תאימות שכיחה של CSP נגרמת על ידי JavaScript מוטבע. דוגמאות ל-JavaScript מוטבעות:
גורמים מטפלים באירועים כמאפייני DOM (למשל <button onclick=''>
) ותגי <script>
עם תוכן
שבקוד ה-HTML.
הפתרון פשוט: מעבירים את התוכן המוטבע לקובץ חדש.
1. ליד החלק התחתון של הדף index.html, מסירים את קוד ה-JavaScript המוטבע ובמקום זאת כוללים js/bootstrap.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()
של
API של chrome.storage
.
המתודה set() מקבלת אובייקט של צמדי מפתח-ערך כפרמטר הראשון. שדה אופציונלי פונקציית קריאה חוזרת היא הפרמטר השני. לדוגמה:
chrome.storage.local.set({secretMessage:'Psst!',timeSet:Date.now()}, function() {
console.log("Secret message saved");
});
ה-method 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()
עם התקשרות חזרה אופציונלית.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)
בכל הקריאות החוזרות כדי לוודא שהפונקציה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 המונוליתי בכל פעם. כל עדכון באצווה בכמה גרסאות
פריט משימות לביצוע, כמו "סמן את כל המשימות כמשימות שהושלמו", יגרום לסכנת נתונים שנקראת
קריאה אחרי כתיבה הבעיה הזו לא הייתה מתרחשת אם אנחנו משתמשים באחסון נתונים מתאים יותר,
כמו IndexedDB, אבל אנחנו מנסים לצמצם את מאמץ ההמרה ב-Codelab הזה.
יש כמה דרכים לפתור את הבעיה, אז נשתמש בהזדמנות הזו כדי לבצע קצת ארגון מחדש של save()
באמצעות
לוקח מערך של מזהי משימות לביצוע שמתעדכנים בבת אחת:
1. כדי להתחיל, צריך ללפף את כל התוכן שכבר נמצא בתוך save()
באמצעות 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! טוענים מחדש את האפליקציה והגרסה שלה אמורה להיות תקינה וארוזה ב-Chrome של TodoMVC.
אפשר לקבל מידע נוסף
למידע מפורט יותר על כמה מממשקי ה-API שהוצגו בשלב הזה, אפשר לעיין במאמרים הבאים:
- Content Security Policy ↑
- הצהרה על הרשאות ↑
- chrome.storage ↑
- chrome.storage.local.get() ↑
- chrome.storage.local.set() ↑
- chrome.storage.local.remove() ↑
- chrome.storage.local.clear() ↑
מוכנים להמשיך לשלב הבא? עוברים אל שלב 3 - הוספת התראות והתראות »