Krok 2. Zaimportuj istniejącą aplikację internetową

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.

Skopiuj folder todomvc do folderu ćwiczeń z programowania

Pojawi się prośba o zastąpienie pliku index.html. Zaakceptuj odpowiedź.

Zastąp index.html

Folder aplikacji powinien mieć teraz taką strukturę pliku:

Nowy folder projektu

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:

Błąd dziennika aplikacji z listą zadań z konsolą CSP

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:

Aplikacja Lista zadań z błędem dziennika konsoli 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:

Debuguj chrome.storage za pomocą konsoli

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 obiekt storage 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ę, że this odnosi się do this prototypu Store. (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:

Todo app with localStorage console log error

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:

Chcesz przejść do następnego kroku? Przejdź do Kroku 3. Dodaj alarmy i powiadomienia »