Z tego kroku dowiesz się:
- Jak dostosować istniejącą aplikację internetową do platformy Aplikacji Chrome.
- Jak zapewnić zgodność skryptów aplikacji z zasadami CSP (Content Security Policy – CSP).
- Jak wdrożyć pamięć lokalną za pomocą chrome.storage.local
Szacowany czas potrzebny na ukończenie tego kroku: 20 minut.
Aby wyświetlić podgląd tego, co musisz zrobić na tym etapie, przewiń na dół strony ↓.
Importowanie istniejącej aplikacji Todo
Na początek zaimportuj do projektu wersję JavaScripta bez żadnych dodatków aplikacji TodoMVC, która jest często używana jako punkt odniesienia.
Wersję aplikacji TodoMVC znajdziesz w pliku ZIP z kodem referencyjnym w folderze todomvc. Skopiuj wszystkie pliki (w tym foldery) z katalogu todomvc do folderu projektu.
Pojawi się prośba o zastąpienie pliku index.html. Zaakceptuj.
W folderze aplikacji powinna teraz być taka struktura plików:
Pliki wyróżnione na niebiesko pochodzą z folderu todomvc.
Załaduj aplikację ponownie (kliknij prawym przyciskiem myszy > Załaduj aplikację). Zobaczysz podstawowy interfejs, ale nie będziesz mieć możliwości dodawania zadań do wykonania.
Sprawdzanie zgodności skryptów ze standardem Content Security Policy (CSP)
Otwórz konsolę DevTools (kliknij prawym przyciskiem myszy > Zbadaj element, a potem wybierz kartę Konsola). Zobaczysz błąd dotyczący odmowy wykonania skryptu wbudowanego:
Aby naprawić ten błąd, sprawdź, czy aplikacja jest zgodna z zasadami Content Security Policy. Jedna z najczęstszych niezgodności z zasadami CSP jest spowodowana przez wbudowany kod JavaScript. Przykłady wbudowanego kodu JavaScript to m.in.: przetwarzacze zdarzeń jako atrybuty DOM (np. <button onclick=''>
) i tagi <script>
z treściami w HTML-u.
Rozwiązanie jest proste: przenieś treści wstawione do nowego pliku.
1. U dołu pliku index.html usuń wbudowany kod JavaScript i zamiast niego dołącz plik 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. W folderze js utwórz plik o nazwie Bootstrap.js. Przenieś wcześniejszy kod wbudowany, aby znajdował się w tym pliku:
// Bootstrap app data
window.app = {};
Jeśli teraz ponownie wczytasz aplikację, nadal nie będzie ona działać, ale jesteś już bliżej rozwiązania problemu.
Konwertuj localStorage na chrome.storage.local
Jeśli otworzysz teraz konsolę DevTools, poprzedni błąd powinien zniknąć. Wystąpił jednak nowy błąd dotyczący niedostępności window.localStorage
:
Aplikacje Chrome nie obsługują localStorage
, ponieważ localStorage
jest synchroniczny. Synchroniczny dostęp do blokowania zasobów (I/O) w jednowątkowym środowisku wykonawczym może sprawić, że aplikacja przestanie reagować.
Aplikacje Chrome mają odpowiedni interfejs API, który może asynchronicznie przechowywać obiekty. Pomoże to uniknąć czasami kosztownego procesu serializacji obiektu > ciągu znaków > obiektu.
Aby rozwiązać problem z komunikatem o błędzie w naszej aplikacji, musisz przekonwertować localStorage
na chrome.storage.local.
Aktualizowanie uprawnień aplikacji
Aby korzystać z funkcji chrome.storage.local
, musisz poprosić o uprawnienie storage
. W pliku manifest.json dodaj "storage"
do tablicy permissions
:
"permissions": ["storage"],
Więcej informacji o local.storage.set() i local.storage.get()
Aby zapisywać i pobierać zadania do wykonania, musisz znać metody set()
i get()
interfejsu API chrome.storage
.
Metoda set() przyjmuje jako pierwszy parametr obiekt par klucz-wartość. Drugim parametrem jest opcjonalna funkcja wywołania zwrotnego. Na przykład:
chrome.storage.local.set({secretMessage:'Psst!',timeSet:Date.now()}, function() {
console.log("Secret message saved");
});
Metoda get() przyjmuje opcjonalny pierwszy parametr kluczy pamięci danych, które chcesz pobrać. Jeden klucz można przekazać jako ciąg znaków. Wiele kluczy można umieścić w tablicy ciągów znaków lub obiekcie słownika.
Drugi parametr, który jest wymagany, to funkcja wywołania zwrotnego. Aby uzyskać dostęp do wartości przechowywanych w zwróconym obiekcie, użyj kluczy określonych w pierwszym parametrze. Na przykład:
chrome.storage.local.get(['secretMessage','timeSet'], function(data) {
console.log("The secret message:", data.secretMessage, "saved at:", data.timeSet);
});
Jeśli chcesz get()
wszystko, co jest obecnie w chrome.storage.local
, pomiń pierwszy parametr:
chrome.storage.local.get(function(data) {
console.log(data);
});
W przeciwieństwie do localStorage
nie będzie można sprawdzać elementów przechowywanych lokalnie za pomocą panelu Zasoby w Narzędziach deweloperskich. Z chrome.storage
możesz jednak korzystać w konsoli JavaScript w ten sposób:
Podgląd wymaganych zmian w interfejsie API
Większość pozostałych kroków konwersji aplikacji Todo to drobne zmiany w wywołaniach interfejsu API. Zmiana wszystkich miejsc, w których localStorage
jest obecnie używany, jest czasochłonna i może powodować błędy.
Najważniejsze różnice między localStorage
a chrome.storage
wynikają z asychronionego charakteru funkcji chrome.storage
:
Zamiast zapisywać do
localStorage
za pomocą prostego przypisania, musisz użyć funkcjichrome.storage.local.set()
z opcjonalnymi wywołaniami zwrotnymi.var data = { todos: [] }; localStorage[dbName] = JSON.stringify(data);
a
var storage = {}; storage[dbName] = { todos: [] }; chrome.storage.local.set( storage, function() { // optional callback });
Zamiast uzyskiwać dostęp do funkcji
localStorage[myStorageName]
bezpośrednio, musisz użyć funkcjichrome.storage.local.get(myStorageName,function(storage){...})
, a potem w funkcji wywołania zwrotnego przeanalizować zwrócony obiektstorage
.var todos = JSON.parse(localStorage[dbName]).todos;
a
chrome.storage.local.get(dbName, function(storage) { var todos = storage[dbName].todos; });
Funkcja
.bind(this)
jest używana we wszystkich wywołaniach zwrotnych, aby zapewnić, żethis
odnosi się dothis
prototypuStore
. (więcej informacji o funkcjach związanych znajdziesz w dokumentacji MDN: Function.prototype.bind()).function Store() { this.scope = 'inside Store'; chrome.storage.local.set( {}, function() { console.log(this.scope); // outputs: 'undefined' }); } new Store();
a
function Store() { this.scope = 'inside Store'; chrome.storage.local.set( {}, function() { console.log(this.scope); // outputs: 'inside Store' }.bind(this)); } new Store();
Pamiętaj o tych różnicach, gdy w następnych sekcjach będziemy omawiać pobieranie, zapisywanie i usuwanie elementów listy zadań.
Odzyskaj zadania do wykonania
Uaktualnijmy aplikację Todo, by pobierać zadania do wykonania:
1. Metoda konstruktora Store
odpowiada za inicjowanie aplikacji Todo ze wszystkimi istniejącymi elementami todo z magazynu danych. Metoda najpierw sprawdza, czy datastore istnieje. Jeśli nie, utworzy pusty tablica todos
i zapisze ją w danych, aby nie było błędów odczytu w czasie wykonywania.
W pliku js/store.js zastąp użycie funkcji konstruktora localStorage
przez 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. Metoda find()
jest używana podczas odczytu zadań z modelu. Zwracane wyniki zmieniają się w zależności od tego, czy filtrujesz dane według stanu „Wszystkie”, „Aktywne” czy „Gotowe”.
Aby przekształcić find()
w 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. Podobnie jak w przypadku elementu find()
, findAll()
pobiera wszystkie zadania do wykonania z modelu. Aby przekształcić findAll()
w 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));
};
Zapisywanie elementów listy zadań
Obecna metoda save()
stwarza problem. Zależy to od 2 operacji asynchronicznych (pobierania i ustawiania), które działają za każdym razem na całym monolitycznym magazynie danych JSON. Wszelkie aktualizacje zbiorcze dotyczące więcej niż jednego elementu listy zadań, np. „oznacz wszystkie zadania jako wykonane”, spowodują zagrożenie danych znane jako czytaj po zapisaniu. Ten problem nie wystąpiłby, gdybyśmy używali bardziej odpowiedniego miejsca do przechowywania danych, takiego jak IndexedDB, ale staramy się zminimalizować wysiłek związany z konwersją w tym przypadku.
Istnieje kilka sposobów na rozwiązanie tego problemu, więc skorzystamy z tej okazji, aby nieznacznie przerobić save()
, korzystając z tablicy identyfikatorów zadań, które mają zostać zaktualizowane jednocześnie:
1. Na początek owiń wszystko, co znajduje się w funkcji save()
, w funkcji zwracającej wywołanie 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. Przekonwertuj wszystkie instancje localStorage
za pomocą metody 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. Następnie zaktualizuj logikę, aby działała na tablicy zamiast na pojedynczym elemencie:
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));
};
Oznacz zadania do wykonania jako ukończone
Teraz, gdy aplikacja działa na tablicach, musisz zmienić sposób, w jaki aplikacja reaguje na kliknięcie przez użytkownika przycisku Wyczyść ukończone (#):
1. W pliku controller.js zaktualizuj funkcję toggleAll()
, aby wywoływała funkcję toggleComplete()
tylko raz z tablicą zadań zamiast oznaczać po kolei każde zadanie jako ukończone. Usuń też wywołanie funkcji _filter()
, ponieważ dostosowujesz 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. Zaktualizuj teraz toggleComplete()
, aby zaakceptować pojedyncze zadanie lub tablicę zadań. Obejmuje to przeniesienie elementu filter()
do wnętrza elementu update()
, a nie na zewnątrz.
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();
};
Upuść wszystkie zadania do wykonania
Jest jeszcze jedna metoda w pliku store.js wykorzystująca metodę localStorage
:
Store.prototype.drop = function (callback) {
localStorage[this._dbName] = JSON.stringify({todos: []});
callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
};
Ta metoda nie jest wywoływana w bieżącej aplikacji, więc jeśli chcesz się zmierzyć z dodatkowym wyzwaniem, spróbuj ją zaimplementować samodzielnie. Wskazówka: zapoznaj się z artykułem chrome.storage.local.clear()
.
Uruchomienie gotowej aplikacji Todo
Krok 2 został ukończony. Załaduj ponownie aplikację. Powinna być już w pełni działająca wersja TodoMVC w pakiecie Chrome.
Więcej informacji
Więcej informacji o niektórych interfejsach API wymienionych na tym etapie znajdziesz w tych artykułach:
- Polityka bezpieczeństwa treści ↑
- Zadeklaruj uprawnienia ↑
- chrome.storage ↑
- chrome.storage.local.get() ↑
- chrome.storage.local.set() ↑
- chrome.storage.local.remove() ↑
- chrome.storage.local.clear() ↑
Czy chcesz przejść do następnego kroku? Przejdź do sekcji Krok 3. Dodaj alarmy i powiadomienia »