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

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.

Skopiuj folder todomvc do folderu codelab

Zobaczysz prośbę o zastąpienie pliku index.html. Zaakceptuj.

Zastąp index.html

W folderze aplikacji powinna być teraz następująca struktura plików:

Nowy folder projektu

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:

Aplikacja Todo z błędem w dzienniku konsoli CSP

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:

Aplikacja Todo z błędem w dzienniku konsoli localStorage

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:

Debugowanie pliku chrome.storage przy użyciu konsoli

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 dane 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 zapewnić, że this odwołuje się do this funkcji Prototyp Store. (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:

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

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:

Chcesz przejść dalej? Przejdź do sekcji Krok 3. Dodaj alarmy i powiadomienia »