在這個步驟中,您將瞭解以下內容:
- 如何針對 Chrome 應用程式平台調整現有的網頁應用程式。
- 如何讓應用程式指令碼符合內容安全政策 (CSP) 規定。
- 如何使用 chrome.storage.local 實作本機儲存空間。
完成這個步驟所需的時間:20 分鐘。
如要預覽您將在這個步驟中完成的內容,請向下捲動到本頁底部 ↓。
匯入現有的 Todo 應用程式
首先,請匯入 TodoMVC 的基本 JavaScript 版本 (這是通用的基準測試) 放入專案中
我們已在 todomvc 的參考程式碼 ZIP 檔案中加入 TodoMVC 應用程式版本 資料夾。將 todomvc 中的所有檔案 (包括資料夾) 複製到專案資料夾。
系統會要求您取代 index.html。請直接接受
現在應用程式資料夾中應該會有以下的檔案結構:
以藍色醒目顯示的檔案來自 todomvc 資料夾。
立即重新載入應用程式 (按一下滑鼠右鍵 >「重新載入 App」)。您應該會看到基本 UI,但不會 新增待辦事項
讓指令碼內容安全政策 (CSP) 符合規定
開啟開發人員工具控制台 (按一下滑鼠右鍵 > 檢查元素,然後選取「Console」分頁標籤)。個人中心 會看到有關拒絕執行內嵌指令碼的錯誤:
讓應用程式符合內容安全政策,讓我們修正這項錯誤。這些是
常見的 CSP 不符規定是內嵌 JavaScript 造成。內嵌 JavaScript 的範例包括
做為 DOM 屬性 (例如 <button onclick=''>
) 的事件處理常式,以及含有內容的 <script>
標記
。
解決方法很簡單:將內嵌內容移到新檔案。
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 資料夾中,建立名稱為 bootstrap.js 的檔案。移動先前的內嵌程式碼 新增到這個檔案:
// Bootstrap app data
window.app = {};
即使現在重新載入應用程式,也比較接近了,因此仍會保留無法運作的「待辦事項」應用程式。
將 localStorage 轉換為 chrome.storage.local
如果現在開啟開發人員工具控制台,先前的錯誤應該不會再顯示。出現新的錯誤
但是,目前無法使用約 window.localStorage
:
由於 localStorage
處於同步狀態,因此 Chrome 應用程式不支援 localStorage
。同步存取
在單一執行緒執行階段封鎖資源 (I/O),可能會導致應用程式沒有回應。
Chrome 應用程式有對等的 API,可以非同步儲存物件。這能避免 有時昂貴的物件->string->物件序列化程序。
如要解決應用程式中的錯誤訊息,您需要將 localStorage
轉換為
chrome.storage.local。
更新應用程式權限
您必須要求 storage
權限,才能使用 chrome.storage.local
。於
manifest.json,然後在 permissions
陣列中加入 "storage"
:
"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
不同,您將無法使用開發人員工具檢查儲存在本機的項目
資源面板。但是,您可以透過 JavaScript 控制台與 chrome.storage
互動,例如:
例如:
預覽必要的 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. Store
建構函式方法會使用所有現有值,將 Todo 應用程式初始化
資料儲存庫中的待辦事項項目這個方法會先檢查資料儲存庫是否存在。如果沒有
建立 todos
的空白陣列,然後儲存至資料儲存庫,避免發生執行階段讀取錯誤。
在 js/store.js 中,將建構函式方法中的 localStorage
轉換為使用
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。如果我們採用更適當的資料儲存空間,就不會發生這個問題
但現在我們正設法盡量減少本程式碼研究室的轉換工作。
修正方法有很多種,我們會利用這個機會稍微重構 save()
擷取要一次更新所有待辦事項 ID 的陣列:
1. 如要開始,請使用 chrome.storage.local.get()
納入 save()
中已的所有內容
回呼:
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. 使用 chrome.storage.local
轉換所有 localStorage
例項:
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,請參閱:
- 內容安全政策 ↑
- 宣告權限 ↑
- chrome.storage ↑
- chrome.storage.local.get() ↑
- chrome.storage.local.set() ↑
- chrome.storage.local.remove() ↑
- chrome.storage.local.clear() ↑
準備好進行下一個步驟了嗎?前往步驟 3 - 新增鬧鐘和通知 »