W tym kroku poznasz:
- Dostosowywanie istniejącej aplikacji internetowej 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 zobaczyć podgląd czynności, które wykonasz w tym kroku, przeskocz na dół strony ↓.
Importowanie istniejącej aplikacji Todo
Na początek zaimportuj standardową wersję JavaScriptu z narzędzia TodoMVC, do swojego projektu.
Wersję aplikacji TodoMVC zamieściliśmy w pliku ZIP z kodem referencyjnym w pliku todomvc. folderu Dysku. Skopiuj wszystkie pliki (w tym foldery) z todomvc do folderu projektu.
Zobaczysz prośbę o zastąpienie pliku index.html. Zaakceptuj.
W folderze aplikacji powinna być teraz następująca struktura plików:
Pliki zaznaczone na niebiesko pochodzą z folderu todomvc.
Załaduj ponownie aplikację teraz (kliknij prawym przyciskiem myszy > Odśwież aplikację). Powinien być widoczny podstawowy interfejs użytkownika, dodawać czynności do wykonania.
Zadbaj o zgodność skryptów z zasadami CSP
Otwórz konsolę Narzędzi deweloperskich (kliknij prawym przyciskiem myszy > Zbadaj element, a następnie wybierz kartę Console (Konsola). Ty pojawi się błąd dotyczący odmowy wykonania wbudowanego skryptu:
Naprawimy ten błąd, dostosowując aplikację do Content Security Policy. Jedna z najbardziej
typowe niezgodności z zasadami CSP są powodowane przez wbudowany kod JavaScript. Przykłady wbudowanego kodu JavaScript:
moduły obsługi zdarzeń jako atrybuty DOM (np.<button onclick=''>
) i tagi <script>
z treścią.
wewnątrz kodu HTML.
Rozwiązanie jest proste: przenieś treść wewnętrzną do nowego pliku.
1. Na dole strony index.html usuń wbudowany JavaScript, a zamiast tego dołącz 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. Przenoszenie kodu w treści strony znajduje się w tym pliku:
// Bootstrap app data
window.app = {};
Jeśli ponownie załadujesz aplikację Todo, nadal będziesz mieć do niej niedziałającą aplikację, ale będziesz coraz bliżej tej funkcji.
Konwertuj localStorage na chrome.storage.local
Jeśli teraz otworzysz konsolę Narzędzi deweloperskich, poprzedni błąd powinien zniknąć. Wystąpił nowy błąd,
ale około window.localStorage
jest niedostępnych:
Aplikacje Chrome nie obsługują localStorage
, ponieważ localStorage
jest synchroniczna. Dostęp synchroniczny
na blokowanie zasobów (I/O) w środowisku wykonawczym jednowątkowym może sprawić, że aplikacja przestanie reagować.
Aplikacje Chrome mają odpowiedni interfejs API, który może asynchronicznie przechowywać obiekty. Pomoże to uniknąć czasem kosztowny proces serializacji obiekt->ciąg znaków->obiekt.
Aby rozwiązać problem z komunikatem o błędzie w naszej aplikacji, musisz przekonwertować plik localStorage
na
chrome.storage.local
Aktualizowanie uprawnień aplikacji
Aby korzystać z usługi chrome.storage.local
, musisz poprosić o uprawnienie storage
. W
manifest.json, dodaj "storage"
do tablicy permissions
:
"permissions": ["storage"],
Więcej informacji o funkcjach local.storage.set() i local.storage.get()
Aby zapisywać i pobierać zadania do wykonania, musisz znać metody set()
i get()
funkcji
Interfejs API chrome.storage
.
Metoda set() akceptuje jako pierwszy parametr obiekt par klucz-wartość. Parametr opcjonalny funkcja wywołania zwrotnego jest drugim parametrem. Na przykład:
chrome.storage.local.set({secretMessage:'Psst!',timeSet:Date.now()}, function() {
console.log("Secret message saved");
});
Metoda get() akceptuje opcjonalny pierwszy parametr dla kluczy magazynu danych, które mają być zareagować. Pojedynczy klucz może być przekazywany jako ciąg znaków. wiele kluczy można umieścić w tablicy ciąg znaków lub obiekt słownika.
Drugi parametr, który jest wymagany, jest funkcją wywołania zwrotnego. W zwróconym obiekcie użyj funkcji żądane w pierwszym parametrze klucze umożliwiające dostęp do przechowywanych wartości. 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 folderze chrome.storage.local
, pomiń pierwsze
:
chrome.storage.local.get(function(data) {
console.log(data);
});
W przeciwieństwie do usługi localStorage
nie będzie można sprawdzać elementów przechowywanych lokalnie za pomocą Narzędzi deweloperskich
Panel zasobów. Z chrome.storage
możesz jednak korzystać w konsoli JavaScriptu
czyli:
Wyświetl podgląd wymaganych zmian w interfejsie API
Większość pozostałych kroków konwertowania aplikacji Todo to niewielkie zmiany w wywołaniach interfejsu API. Zmiana
wszystkich miejsc, w których jest obecnie używana funkcja localStorage
, choć jest to czasochłonne i podatne na błędy,
jest wymagane.
Główne różnice między localStorage
a chrome.storage
wynikają z natury asynchronicznej
chrome.storage
:
Zamiast pisać do
localStorage
za pomocą prostego zadania, musisz użyćchrome.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 bezpośredniego dostępu do
localStorage[myStorageName]
, musisz użyćchrome.storage.local.get(myStorageName,function(storage){...})
, a następnie przeanalizuj zwrócone danestorage
w funkcji wywołania zwrotnego.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
odwołuje się dothis
funkcji PrototypStore
. (Więcej informacji o funkcjach powiązanych można znaleźć 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 kluczowych różnicach w pobieraniu, zapisywaniu i usuwaniu elementów listy zadań poniższych sekcji.
Odzyskaj zadania do wykonania
Uaktualnijmy aplikację Todo, by pobierać zadania do wykonania:
1. Metoda konstruktora Store
zajmuje się inicjowaniem aplikacji Todo ze wszystkimi istniejącymi
z magazynu danych. Metoda najpierw sprawdza, czy magazyn danych istnieje. Jeśli nie,
utwórz pustą tablicę typu todos
i zapisz ją w magazynie danych, aby uniknąć błędów odczytu w czasie działania aplikacji.
W pliku js/store.js przekonwertuj użycie localStorage
w metodzie konstruktora, tak aby zamiast tego użyć
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 odczytywania zadań do wykonania z modelu. Zwracane wyniki zmieniają się w zależności od tego,
określając, czy filtr ma być „Wszystkie”, „Aktywne” czy „Gotowe”.
Przekonwertuj find()
na 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. Zmień findAll()
na użycie
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));
};
Zapisz pozycje do wykonania
Obecna metoda save()
stanowi wyzwanie. Zależy to od 2 operacji asynchronicznych (get i set)
które działają za każdym razem na całej monolitycznej pamięci JSON. Zbiorczych aktualizacji więcej niż jednej
takie jak „oznacz wszystkie zadania do wykonania jako ukończone”, będą stanowić zagrożenie dla danych:
Read-After-Write. Ten problem nie wystąpiłby, gdyby nasze miejsce na dane było bardziej odpowiednie,
np. IndexedDB, ale w tym ćwiczeniu z programowania staramy się zminimalizować nakład pracy związanej z konwersją.
Istnieje kilka sposobów rozwiązania tego problemu, więc użyjemy tej okazji do niewielkiego refaktoryzacji elementu save()
przez
na podstawie tablicy identyfikatorów czynności do zaktualizowania jednocześnie:
1. Na początek zapakuj wszystko, co jest już w środku save()
, za pomocą tagu chrome.storage.local.get()
wywołanie zwrotne:
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 operowała na tablicy, a nie 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 obsługuje użytkownika klikającego Przycisk Wyczyść ukończone (#):
1. W pliku controller.js zaktualizuj toggleAll()
tak, aby wywoływał toggleComplete()
tylko raz z tablicy
zadań do wykonania, zamiast oznaczać je jako wykonane pojedynczo. Usuń też połączenie z numerem _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. Teraz zaktualizuj aplikację toggleComplete()
, aby akceptowała zarówno pojedyncze zadanie do wykonania, jak i ich tablicę. Obejmuje to m.in.
przenoszę obiekt filter()
do wnętrza komponentu 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 potrzebujesz dodatkowego wyzwania, wypróbuj
na własną rękę. Podpowiedź: przyjrzyj się chrome.storage.local.clear()
.
Uruchom gotową aplikację Todo
Krok 2 ukończony! Załaduj ponownie aplikację. Aplikacja Chrome powinna być już w pełni działająca w pakiecie w organizacji TodoMVC.
Więcej informacji
Szczegółowe informacje o niektórych interfejsach API wprowadzonych w tym kroku znajdziesz tutaj:
- Polityka bezpieczeństwa treści ↑
- Deklarowanie uprawnień ↑
- chrome.storage ↑
- chrome.storage.local.get() ↑
- chrome.storage.local.set() ↑
- chrome.storage.local.remove() ↑
- chrome.storage.local.clear() ↑
Chcesz przejść dalej? Przejdź do sekcji Krok 3. Dodaj alarmy i powiadomienia »