Trong bước này, bạn sẽ tìm hiểu:
- Cách điều chỉnh ứng dụng web hiện có cho nền tảng Ứng dụng Chrome.
- Cách giúp tập lệnh ứng dụng tuân thủ Chính sách bảo mật nội dung (CSP).
- Cách triển khai bộ nhớ cục bộ bằng chrome.storage.local.
Thời gian ước tính để hoàn tất bước này: 20 phút.
Để xem trước những việc bạn sẽ hoàn thành trong bước này, hãy chuyển xuống cuối trang này ↓.
Nhập ứng dụng việc cần làm hiện có
Để bắt đầu, hãy nhập phiên bản JavaScript Vanilla của TodoMVC, một điểm chuẩn chung vào dự án của bạn.
Chúng tôi đã thêm một phiên bản của ứng dụng TodoMVC vào mã bưu chính tham chiếu trong todomvc . Sao chép tất cả các tệp (bao gồm cả thư mục) từ todomvc vào thư mục dự án.
Bạn sẽ được yêu cầu thay thế index.html. Hãy tiếp tục và chấp nhận.
Bây giờ, bạn sẽ có cấu trúc tệp sau trong thư mục ứng dụng:
Các tệp được đánh dấu màu xanh dương là các tệp nằm trong thư mục todomvc.
Tải lại ứng dụng ngay (nhấp chuột phải > Tải lại ứng dụng). Bạn sẽ thấy giao diện người dùng cơ bản có thể thêm việc cần làm.
Giúp tập lệnh tuân thủ Chính sách bảo mật nội dung (CSP) của tập lệnh
Mở Bảng điều khiển Công cụ cho nhà phát triển (nhấp chuột phải > Kiểm tra phần tử, sau đó chọn thẻ Bảng điều khiển). Bạn sẽ thấy lỗi về việc từ chối thực thi một tập lệnh cùng dòng:
Hãy khắc phục lỗi này bằng cách làm cho ứng dụng tuân thủ Chính sách bảo mật nội dung. Một trong những
tình trạng không tuân thủ CSP phổ biến là do JavaScript cùng dòng gây ra. Ví dụ về JavaScript cùng dòng bao gồm
trình xử lý sự kiện dưới dạng thuộc tính DOM (ví dụ: <button onclick=''>
) và thẻ <script>
có nội dung
bên trong HTML.
Giải pháp rất đơn giản: di chuyển nội dung cùng dòng sang một tệp mới.
1. Ở gần cuối index.html, hãy xoá JavaScript cùng dòng và thay vào đó, hãy bao gồm 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. Tạo tệp trong thư mục js có tên bootstrap.js. Di chuyển mã cùng dòng trước đó để có trong tệp này:
// Bootstrap app data
window.app = {};
Ứng dụng Công việc của bạn vẫn sẽ không hoạt động nếu bạn tải lại ứng dụng ngay bây giờ nhưng sắp đạt đến hơn.
Chuyển đổi localStorage thành chrome.storage.local
Nếu bạn mở Bảng điều khiển công cụ cho nhà phát triển ngay bây giờ, lỗi trước đó sẽ biến mất. Đã xảy ra lỗi mới,
tuy nhiên, khoảng window.localStorage
không có sẵn:
Ứng dụng Chrome không hỗ trợ localStorage
vì localStorage
là đồng bộ. Quyền truy cập đồng bộ
việc chặn tài nguyên (I/O) trong thời gian chạy đơn luồng có thể khiến ứng dụng của bạn không phản hồi.
Ứng dụng Chrome có API tương đương có thể lưu trữ các đối tượng không đồng bộ. Việc này sẽ giúp tránh đôi khi tốn kém cho quá trình chuyển đổi tuần tự đối tượng->string->đối tượng.
Để giải quyết thông báo lỗi này trong ứng dụng của chúng ta, bạn cần chuyển đổi localStorage
thành
chrome.storage.local.
Cập nhật quyền cho ứng dụng
Để sử dụng chrome.storage.local
, bạn cần yêu cầu quyền storage
. Trong
manifest.json, hãy thêm "storage"
vào mảng permissions
:
"permissions": ["storage"],
Tìm hiểu về local.storage.set() và local.storage.get()
Để lưu và truy xuất các mục việc cần làm, bạn cần biết về phương thức set()
và get()
của phương thức
API chrome.storage
.
Phương thức set() chấp nhận một đối tượng của các cặp khoá-giá trị làm tham số đầu tiên. Không bắt buộc hàm callback là tham số thứ hai. Ví dụ:
chrome.storage.local.set({secretMessage:'Psst!',timeSet:Date.now()}, function() {
console.log("Secret message saved");
});
Phương thức get() chấp nhận tham số đầu tiên (không bắt buộc) cho các khoá của kho dữ liệu mà bạn muốn thăm dò. Có thể truyền một khoá dưới dạng chuỗi; Bạn có thể sắp xếp nhiều khoá vào một mảng hoặc đối tượng từ điển.
Tham số thứ hai bắt buộc là hàm callback. Trong đối tượng được trả về, hãy sử dụng phương thức các khoá được yêu cầu trong tham số đầu tiên để truy cập vào các giá trị đã lưu trữ. Ví dụ:
chrome.storage.local.get(['secretMessage','timeSet'], function(data) {
console.log("The secret message:", data.secretMessage, "saved at:", data.timeSet);
});
Nếu bạn muốn get()
mọi nội dung hiện có trong chrome.storage.local
, hãy bỏ qua thuộc tính đầu tiên
tham số:
chrome.storage.local.get(function(data) {
console.log(data);
});
Không giống như localStorage
, bạn sẽ không thể kiểm tra các mục được lưu trữ cục bộ bằng Công cụ cho nhà phát triển
Bảng điều khiển tài nguyên. Tuy nhiên, bạn có thể tương tác với chrome.storage
từ Bảng điều khiển JavaScript như
nên:
Xem trước các thay đổi bắt buộc về API
Hầu hết các bước còn lại trong quy trình chuyển đổi ứng dụng Việc cần làm đều là những thay đổi nhỏ đối với lệnh gọi API. Đang thay đổi
tất cả các vị trí nơi localStorage
hiện đang được sử dụng, mặc dù tốn thời gian và dễ xảy ra lỗi,
là trường bắt buộc.
Điểm khác biệt chính giữa localStorage
và chrome.storage
đến từ bản chất không đồng bộ của
chrome.storage
:
Thay vì ghi vào
localStorage
bằng một bài tập đơn giản, bạn cần sử dụngchrome.storage.local.set()
với các lệnh gọi lại không bắt buộc.var data = { todos: [] }; localStorage[dbName] = JSON.stringify(data);
đấu với
var storage = {}; storage[dbName] = { todos: [] }; chrome.storage.local.set( storage, function() { // optional callback });
Thay vì truy cập trực tiếp vào
localStorage[myStorageName]
, bạn cần sử dụngchrome.storage.local.get(myStorageName,function(storage){...})
rồi phân tích cú pháp tệp được trả vềstorage
trong hàm callback.var todos = JSON.parse(localStorage[dbName]).todos;
đấu với
chrome.storage.local.get(dbName, function(storage) { var todos = storage[dbName].todos; });
Hàm
.bind(this)
được dùng trên tất cả lệnh gọi lại để đảm bảothis
tham chiếu đếnthis
củaStore
nguyên mẫu. (Bạn có thể xem thêm thông tin về các hàm ràng buộc trên các tài liệu về MDN: Function.prototype.bind().)function Store() { this.scope = 'inside Store'; chrome.storage.local.set( {}, function() { console.log(this.scope); // outputs: 'undefined' }); } new Store();
đấu với
function Store() { this.scope = 'inside Store'; chrome.storage.local.set( {}, function() { console.log(this.scope); // outputs: 'inside Store' }.bind(this)); } new Store();
Hãy ghi nhớ những khác biệt chính này khi chúng tôi đề cập đến việc truy xuất, lưu và xoá các mục việc cần làm trong các phần sau.
Truy xuất các mục việc cần làm
Hãy cập nhật ứng dụng Việc cần làm để truy xuất các mục việc cần làm:
1. Phương thức hàm khởi tạo Store
đảm nhận việc khởi chạy ứng dụng Việc cần làm bằng tất cả các dữ liệu hiện có
các mục việc cần làm từ kho dữ liệu. Trước tiên, phương thức này sẽ kiểm tra xem kho dữ liệu có tồn tại hay không. Nếu không,
tạo một mảng todos
trống rồi lưu vào kho dữ liệu để không có lỗi đọc trong thời gian chạy.
Trong js/store.js, hãy chuyển đổi việc sử dụng localStorage
trong phương thức hàm khởi tạo để sử dụng
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. Phương thức find()
được dùng khi đọc việc cần làm qua Mô hình. Kết quả trả về thay đổi dựa trên
về việc bạn đang lọc theo "Tất cả", "Đang hoạt động" hay "Đã hoàn tất".
Chuyển đổi find()
để sử dụng 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. Tương tự như find()
, findAll()
nhận tất cả việc cần làm từ Mô hình. Chuyển đổi findAll()
để sử dụng
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));
};
Lưu những việc cần làm
Phương thức save()
hiện tại đưa ra một thử thách. Điều này phụ thuộc vào 2 hoạt động không đồng bộ (lấy và đặt)
luôn hoạt động trên toàn bộ bộ nhớ JSON nguyên khối. Bất kỳ bản cập nhật theo lô nào trên nhiều hơn một
mục việc cần làm (chẳng hạn như "đánh dấu tất cả việc cần làm là đã hoàn thành") sẽ dẫn đến mối nguy hiểm dữ liệu, còn gọi là
Đọc sau khi ghi. Sự cố này sẽ không xảy ra nếu chúng tôi sử dụng bộ nhớ dữ liệu thích hợp hơn,
như IndexedDB, nhưng chúng ta đang cố gắng giảm thiểu nỗ lực chuyển đổi cho lớp học lập trình này.
Có một số cách để khắc phục vấn đề này, vì vậy, chúng ta sẽ tận dụng cơ hội này để tái cấu trúc một chút save()
bằng cách
lấy một loạt mã việc cần làm được cập nhật cùng lúc:
1. Để bắt đầu, hãy gói mọi thứ đã có bên trong save()
bằng một chrome.storage.local.get()
gọi lại:
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. Chuyển đổi mọi thực thể localStorage
bằng 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. Sau đó, hãy cập nhật logic để hoạt động trên một mảng thay vì một mục duy nhất:
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));
};
Đánh dấu mục việc cần làm là hoàn thành
Hiện ứng dụng đang hoạt động trên các mảng, bạn cần thay đổi cách ứng dụng xử lý việc người dùng nhấp vào Nút Xoá nội dung đã hoàn thành (#):
1. Trong controller.js, hãy cập nhật toggleAll()
để chỉ gọi toggleComplete()
một lần bằng một mảng
việc cần làm thay vì đánh dấu việc cần làm là đã hoàn thành từng việc một. Đồng thời xoá cuộc gọi đến _filter()
vì bạn sẽ điều chỉnh trong 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. Bây giờ, hãy cập nhật toggleComplete()
để chấp nhận cả một việc cần làm hoặc một loạt việc cần làm. bao gồm
di chuyển filter()
sang bên trong update()
thay vì bên ngoài.
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();
};
Thả tất cả các mục việc cần làm
Có một phương pháp khác trong store.js sử dụng localStorage
:
Store.prototype.drop = function (callback) {
localStorage[this._dbName] = JSON.stringify({todos: []});
callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
};
Phương thức này không được gọi trong ứng dụng hiện tại. Vì vậy, nếu bạn muốn thử thách thêm, hãy thử
tự triển khai nó. Gợi ý: Hãy xem chrome.storage.local.clear()
.
Chạy ứng dụng Việc cần làm đã hoàn thành
Bạn đã hoàn tất Bước 2! Tải lại ứng dụng và giờ đây, bạn sẽ có phiên bản đóng gói Chrome hoạt động hoàn toàn của TodoMVC.
Thông tin khác
Để biết thêm thông tin chi tiết về một số API được giới thiệu trong bước này, hãy tham khảo:
- Chính sách bảo mật nội dung ↑
- Khai báo quyền ↑
- chrome.storage ↑
- chrome.storage.local.get() ↑
- chrome.storage.local.set() ↑
- chrome.storage.local.remove() ↑
- chrome.storage.local.clear() ↑
Bạn đã sẵn sàng chuyển sang bước tiếp theo? Chuyển đến Bước 3 – Thêm chuông báo và thông báo »