W tym kroku poznasz:
- Jak dostosować istniejącą aplikację internetową do platformy aplikacji Chrome.
- Jak zapewnić zgodność skryptów aplikacji ze standardem Content Security Policy (CSP)
- Jak wdrożyć pamięć lokalną za pomocą pliku chrome.storage.local.
Szacowany czas potrzebny na wykonanie tego kroku: 20 minut.
Aby zobaczyć, co wykonasz w tym kroku, przejdź na sam dół strony ↓.
Importowanie istniejącej aplikacji Do zrobienia
Na początek zaimportuj do projektu wersję w języku JavaScript TodoMVC, czyli popularnej aplikacji porównawczej.
Wersję aplikacji TodoMVC zamieściliśmy w pliku ZIP z kodem referencyjnym w folderze todomvc. Skopiuj wszystkie pliki (w tym foldery) z todomvc do folderu projektu.
Pojawi się prośba o zastąpienie pliku index.html. Zaakceptuj odpowiedź.
Folder aplikacji powinien mieć teraz taką strukturę pliku:
Pliki wyróżnione na niebiesko pochodzą z folderu todomvc.
Załaduj aplikację ponownie (kliknij prawym przyciskiem myszy > Załaduj ponownie aplikację). Zobaczysz podstawowy interfejs, ale nie możesz dodawać zadań do wykonania.
Zapewnianie zgodności skryptów z Content Security Policy (CSP)
Otwórz konsolę Narzędzi deweloperskich (kliknij prawym przyciskiem myszy > Zbadaj element, a następnie wybierz kartę Konsola). Pojawi się błąd związany z odmową wykonania wbudowanego skryptu:
Aby naprawić ten błąd, zadbaj o zgodność aplikacji z Content Security Policy. Jedna z najczęstszych niezgodności CSP jest spowodowana przez wbudowany kod JavaScript. Przykłady wbudowanego kodu JavaScript to moduły obsługi zdarzeń w atrybutach DOM (np. <button onclick=''>
) oraz tagi <script>
z treścią w kodzie HTML.
Rozwiązanie jest proste: przenieś treści wbudowane do nowego pliku.
1. Na dole strony index.html usuń wbudowany kod JavaScript, a zamiast tego dołącz kod 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. Utwórz w folderze js plik o nazwie bootstrap.js. Przenieś wcześniej kod wbudowany, aby znalazł się w tym pliku:
// Bootstrap app data
window.app = {};
Jeśli teraz ponownie załadujesz aplikację Dodo, nadal będzie ona niedziałająca, ale jesteś bliżej niej.
Przekonwertuj plik localStorage na chrome.storage.local
Jeśli teraz otworzysz konsolę Narzędzi deweloperskich, poprzedni błąd powinien zniknąć. Pojawił się nowy błąd dotyczący niedostępności strony window.localStorage
:
Aplikacje Chrome nie obsługują localStorage
, ponieważ localStorage
jest synchroniczny. Synchroniczny dostęp do blokujących zasobów (wejścia/wyjścia) w jednowątkowym środowisku wykonawczym może sprawić, że aplikacja przestanie reagować.
Aplikacje Chrome mają odpowiednik API, który może przechowywać obiekty asynchronicznie. Pomoże to uniknąć kosztownego, kosztownego procesu serializacji obiektów.
Aby rozwiązać problem z komunikatem o błędzie w naszej aplikacji, musisz przekonwertować plik localStorage
na chrome.storage.local.
Aktualizowanie uprawnień aplikacji
Aby użyć uprawnienia chrome.storage.local
, musisz poprosić o uprawnienie storage
. W pliku 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ć elementy do wykonania, musisz znać metody set()
i get()
interfejsu API chrome.storage
.
Metoda set() akceptuje obiekt par klucz-wartość jako pierwszy parametr. 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() akceptuje opcjonalny pierwszy parametr dla kluczy magazynu 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 obiektu słownika.
Drugi wymagany parametr to funkcja wywołania zwrotnego. W zwróconym obiekcie użyj kluczy żądanych w pierwszym parametrze, aby uzyskać dostęp do zapisanych 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()
wykonać wszystkie działania, które są obecnie w elemencie chrome.storage.local
, pomiń pierwszy parametr:
chrome.storage.local.get(function(data) {
console.log(data);
});
W przeciwieństwie do narzędzia localStorage
w panelu zasobów Narzędzi deweloperskich nie można sprawdzać elementów przechowywanych lokalnie. Z elementu chrome.storage
możesz jednak korzystać w konsoli JavaScript w ten sposób:
Wyświetl podgląd wymaganych zmian interfejsu API
Większość pozostałych etapów konwertowania aplikacji Do zrobienia to niewielkie zmiany w wywołaniach interfejsu API. Zmiana wszystkich miejsc, w których używany jest obecnie atrybut localStorage
, jest konieczna, chociaż czasochłonna i obarczająca ryzyko błędów.
Główne różnice między localStorage
a chrome.storage
wynikają z natury asynchronicznej chrome.storage
:
Zamiast wysyłać wiadomości do
localStorage
za pomocą prostego przypisania, 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 uzyskiwać bezpośredni dostęp do obiektu
localStorage[myStorageName]
, trzeba użyćchrome.storage.local.get(myStorageName,function(storage){...})
, a potem przeanalizować zwrócony obiektstorage
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 upewnić się, żethis
odnosi się dothis
prototypuStore
. (Więcej informacji o funkcjach ograniczonych 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 kluczowych różnicach, bo w kolejnych sekcjach omawiamy pobieranie, zapisywanie i usuwanie zadań do wykonania.
Pobieranie zadań do wykonania
Zaktualizujmy aplikację Do zrobienia, aby pobierać zadania do wykonania:
1. Metoda konstruktora Store
zajmuje się inicjowaniem aplikacji Todo ze wszystkimi istniejącymi elementami tego typu z magazynu danych. Metoda najpierw sprawdza, czy magazyn danych istnieje. Jeśli tak nie jest, utworzy pustą tablicę todos
i zapisze ją w magazynie danych, aby nie wystąpiły błędy odczytu w czasie działania.
W pliku js/store.js przekonwertuj użycie localStorage
w metodzie konstruktora tak, aby używać w zamiast niego 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. Wyświetlane wyniki zmieniają się w zależności od tego, czy filtrujesz według „Wszystkie”, „Aktywne” czy „Gotowe”.
Przekonwertuj plik 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 find()
, findAll()
otrzymuje wszystkie zadania do wykonania od modelu. Przekonwertuj plik findAll()
na aplikację 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 zadania do wykonania
Obecna metoda save()
stwarza wyzwanie. Jest uzależniona od 2 operacji asynchronicznych (get i set), które za każdym razem operują na całej monolitycznej pamięci JSON. Wszelkie aktualizacje zbiorcze kilku elementów do wykonania, np. „oznaczenie wszystkich zadań do wykonania jako wykonane”, mogą stanowić zagrożenie dla danych o nazwie Read-After-Write. Ten problem nie miałby miejsca, gdybyśmy używali bardziej odpowiedniego miejsca na dane, takiego jak IndexedDB, ale w przypadku tego ćwiczenia z programowania staramy się zminimalizować nakład pracy związany z konwersją.
Istnieje kilka sposobów rozwiązania tego problemu, więc wykorzystamy tę możliwość niewielkiej refaktoryzacji pliku save()
. W tym celu pobieramy tablicę identyfikatorów zadań do zaktualizowania naraz:
1. Na początek umieść wszystko, co już znajduje się w elemencie save()
, za pomocą wywołania zwrotnego 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ą narzędzia 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 funkcje logiczne, aby przeprowadzać operacje 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 kliknięcie przycisku Wyczyść ukończone (#):
1. W pliku controller.js zaktualizuj toggleAll()
, aby wywoływał toggleComplete()
tylko raz z tablicą zadań do wykonania, zamiast oznaczać poszczególne zadania jako wykonane. Usuń też wywołanie _filter()
, ponieważ będziesz dostosowywać 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 toggleComplete()
, aby akceptował zarówno pojedynczą czynność do wykonania, jak i tablicę zadań do wykonania. Obejmuje to przeniesienie filter()
do wewnątrz 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
W pliku store.js jest jeszcze jedna metoda 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, spróbuj wdrożyć ją samodzielnie. Podpowiedź: obejrzyj chrome.storage.local.clear()
.
Uruchamianie gotowej aplikacji Todo
Krok 2 został ukończony. Ponownie uruchom aplikację. TodoMVC, w pakiecie powinna już działać w pełni działająca wersja Chrome.
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ść do następnego kroku? Przejdź do Kroku 3. Dodaj alarmy i powiadomienia »