Pada langkah ini, Anda akan mempelajari:
- Cara menyesuaikan aplikasi web yang ada untuk platform Aplikasi Chrome.
- Cara membuat skrip aplikasi Anda mematuhi Kebijakan Keamanan Konten (CSP).
- Cara menerapkan penyimpanan lokal menggunakan chrome.storage.local.
Perkiraan waktu untuk menyelesaikan langkah ini: 20 menit.
Untuk melihat pratinjau hal yang akan Anda selesaikan pada langkah ini, buka bagian bawah halaman ini ↓.
Mengimpor aplikasi Daftar Tugas yang ada
Sebagai titik awal, impor versi JavaScript vanilla dari TodoMVC, aplikasi benchmark umum, ke project Anda.
Kami telah menyertakan versi aplikasi TodoMVC dalam zip kode referensi di folder todomvc. Salin semua file (termasuk folder) dari todomvc ke folder project Anda.
Anda akan diminta untuk mengganti index.html. Lanjutkan dan terima.
Sekarang Anda akan memiliki struktur file berikut di folder aplikasi:
File yang ditandai dengan warna biru berasal dari folder todomvc.
Muat ulang aplikasi Anda sekarang (klik kanan > Muat Ulang Aplikasi). Anda akan melihat UI dasar, tetapi tidak akan dapat menambahkan daftar tugas.
Membuat skrip yang mematuhi Kebijakan Keamanan Konten (CSP)
Buka Konsol DevTools (klik kanan > Periksa Elemen, lalu pilih tab Konsol). Anda akan melihat error tentang penolakan untuk menjalankan skrip inline:
Mari kita perbaiki error ini dengan membuat aplikasi mematuhi Kebijakan Keamanan Konten. Salah satu ketidakpatuhan CSP yang paling umum disebabkan oleh JavaScript inline. Contoh JavaScript inline mencakup pengendali peristiwa sebagai atribut DOM (misalnya, <button onclick=''>
) dan tag <script>
dengan konten di dalam HTML.
Solusinya sederhana: pindahkan konten inline ke file baru.
1. Di dekat bagian bawah index.html, hapus JavaScript inline dan sertakan 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. Buat file di folder js bernama bootstrap.js. Pindahkan kode inline sebelumnya ke dalam file ini:
// Bootstrap app data
window.app = {};
Anda masih akan memiliki aplikasi Daftar Tugas yang tidak berfungsi jika memuat ulang aplikasi sekarang, tetapi Anda semakin dekat.
Mengonversi localStorage ke chrome.storage.local
Jika Anda membuka Konsol DevTools sekarang, error sebelumnya akan hilang. Namun, ada error baru
tentang window.localStorage
yang tidak tersedia:
Aplikasi Chrome tidak mendukung localStorage
karena localStorage
bersifat sinkron. Akses sinkron
ke resource pemblokiran (I/O) dalam runtime single-thread dapat membuat aplikasi Anda tidak responsif.
Aplikasi Chrome memiliki API yang setara yang dapat menyimpan objek secara asinkron. Hal ini akan membantu menghindari proses serialisasi objek->string->objek yang terkadang mahal.
Untuk mengatasi pesan error di aplikasi kami, Anda perlu mengonversi localStorage
menjadi
chrome.storage.local.
Memperbarui izin aplikasi
Untuk menggunakan chrome.storage.local
, Anda perlu meminta izin storage
. Di
manifest.json, tambahkan "storage"
ke array permissions
:
"permissions": ["storage"],
Pelajari local.storage.set() dan local.storage.get()
Untuk menyimpan dan mengambil item daftar tugas, Anda perlu mengetahui metode set()
dan get()
dari
chrome.storage
API.
Metode set() menerima objek key-value pair sebagai parameter pertamanya. Fungsi callback opsional adalah parameter kedua. Contoh:
chrome.storage.local.set({secretMessage:'Psst!',timeSet:Date.now()}, function() {
console.log("Secret message saved");
});
Metode get() menerima parameter pertama opsional untuk kunci datastore yang ingin Anda ambil. Satu kunci dapat diteruskan sebagai string; beberapa kunci dapat disusun menjadi array string atau objek kamus.
Parameter kedua, yang diperlukan, adalah fungsi callback. Dalam objek yang ditampilkan, gunakan kunci yang diminta dalam parameter pertama untuk mengakses nilai yang disimpan. Contoh:
chrome.storage.local.get(['secretMessage','timeSet'], function(data) {
console.log("The secret message:", data.secretMessage, "saved at:", data.timeSet);
});
Jika Anda ingin get()
semua yang saat ini ada di chrome.storage.local
, hapus parameter
pertama:
chrome.storage.local.get(function(data) {
console.log(data);
});
Tidak seperti localStorage
, Anda tidak akan dapat memeriksa item yang disimpan secara lokal menggunakan panel Resource DevTools. Namun, Anda dapat berinteraksi dengan chrome.storage
dari Konsol JavaScript seperti ini:
Melihat pratinjau perubahan API yang diperlukan
Sebagian besar langkah yang tersisa dalam mengonversi aplikasi Todo adalah perubahan kecil pada panggilan API. Anda harus mengubah semua tempat tempat localStorage
saat ini digunakan, meskipun proses ini memakan waktu dan rentan error.
Perbedaan utama antara localStorage
dan chrome.storage
berasal dari sifat asinkron
chrome.storage
:
Alih-alih menulis ke
localStorage
menggunakan penetapan sederhana, Anda harus menggunakanchrome.storage.local.set()
dengan callback opsional.var data = { todos: [] }; localStorage[dbName] = JSON.stringify(data);
versus
var storage = {}; storage[dbName] = { todos: [] }; chrome.storage.local.set( storage, function() { // optional callback });
Daripada mengakses
localStorage[myStorageName]
secara langsung, Anda harus menggunakanchrome.storage.local.get(myStorageName,function(storage){...})
, lalu mengurai objekstorage
yang ditampilkan dalam fungsi callback.var todos = JSON.parse(localStorage[dbName]).todos;
versus
chrome.storage.local.get(dbName, function(storage) { var todos = storage[dbName].todos; });
Fungsi
.bind(this)
digunakan di semua callback untuk memastikanthis
merujuk kethis
prototipeStore
. (Info selengkapnya tentang fungsi terikat dapat ditemukan di dokumen MDN: Function.prototype.bind().)function Store() { this.scope = 'inside Store'; chrome.storage.local.set( {}, function() { console.log(this.scope); // outputs: 'undefined' }); } new Store();
versus
function Store() { this.scope = 'inside Store'; chrome.storage.local.set( {}, function() { console.log(this.scope); // outputs: 'inside Store' }.bind(this)); } new Store();
Perhatikan perbedaan utama ini saat kita membahas pengambilan, penyimpanan, dan penghapusan item daftar tugas di bagian berikut.
Mengambil item daftar tugas
Mari perbarui aplikasi Daftar tugas untuk mengambil item daftar tugas:
1. Metode konstruktor Store
menangani inisialisasi aplikasi Todo dengan semua item daftar tugas yang sudah ada dari datastore. Metode ini pertama-tama memeriksa apakah datastore ada. Jika tidak, kode ini akan
membuat array todos
kosong dan menyimpannya ke datastore sehingga tidak ada error pembacaan runtime.
Di js/store.js, konversikan penggunaan localStorage
dalam metode konstruktor untuk menggunakan
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. Metode find()
digunakan saat membaca daftar tugas dari Model. Hasil yang ditampilkan akan berubah berdasarkan
apakah Anda memfilter menurut "Semua", "Aktif", atau "Selesai".
Konversikan find()
untuk menggunakan 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. Serupa dengan find()
, findAll()
mendapatkan semua daftar tugas dari Model. Konversikan findAll()
untuk menggunakan
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));
};
Menyimpan item daftar tugas
Metode save()
saat ini menghadirkan tantangan. Library ini bergantung pada dua operasi asinkron (dapatkan dan tetapkan)
yang beroperasi di seluruh penyimpanan JSON monolitik setiap saat. Setiap update batch pada lebih dari satu
item daftar tugas, seperti "tandai semua daftar tugas sebagai selesai", akan menghasilkan bahaya data yang dikenal sebagai
Read-After-Write. Masalah ini tidak akan terjadi jika kita menggunakan penyimpanan data yang lebih sesuai, seperti IndexedDB, tetapi kita mencoba meminimalkan upaya konversi untuk codelab ini.
Ada beberapa cara untuk memperbaikinya, sehingga kita akan menggunakan peluang ini untuk sedikit memfaktorkan ulang save()
dengan mengambil array ID daftar tugas yang akan diperbarui sekaligus:
1. Untuk memulai, gabungkan semua yang sudah ada di dalam save()
dengan 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. Konversikan semua instance localStorage
dengan 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. Kemudian, perbarui logika untuk beroperasi pada array, bukan satu item:
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));
};
Menandai item daftar tugas sebagai selesai
Setelah aplikasi beroperasi pada array, Anda perlu mengubah cara aplikasi menangani pengguna yang mengklik tombol Clear completed (#):
1. Di controller.js, perbarui toggleAll()
untuk memanggil toggleComplete()
hanya sekali dengan array daftar tugas, bukan menandai daftar tugas sebagai selesai satu per satu. Hapus juga panggilan ke _filter()
karena Anda akan menyesuaikan 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. Sekarang update toggleComplete()
untuk menerima satu daftar tugas atau array daftar tugas. Hal ini termasuk
memindahkan filter()
ke dalam update()
, bukan di luar.
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();
};
Menghapus semua item daftar tugas
Ada satu metode lagi di store.js menggunakan localStorage
:
Store.prototype.drop = function (callback) {
localStorage[this._dbName] = JSON.stringify({todos: []});
callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
};
Metode ini tidak dipanggil di aplikasi saat ini. Jadi, jika Anda menginginkan tantangan tambahan, coba
implementasikan sendiri. Petunjuk: Lihat chrome.storage.local.clear()
.
Meluncurkan aplikasi Daftar Tugas yang sudah selesai
Anda telah menyelesaikan Langkah 2. Muat ulang aplikasi Anda dan sekarang Anda akan memiliki TodoMVC versi paket Chrome yang berfungsi sepenuhnya.
Untuk informasi selengkapnya
Untuk informasi yang lebih mendetail tentang beberapa API yang diperkenalkan dalam langkah ini, lihat:
- Kebijakan Keamanan Konten ↑
- Mendeklarasikan Izin ↑
- chrome.storage ↑
- chrome.storage.local.get() ↑
- chrome.storage.local.set() ↑
- chrome.storage.local.remove() ↑
- chrome.storage.local.clear() ↑
Siap melanjutkan ke langkah berikutnya? Buka Langkah 3 - Menambahkan alarm dan notifikasi »