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

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.

Skopiuj folder todomvc do folderu codelab

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

Zastąp plik index.html

W folderze aplikacji powinna teraz być taka struktura plików:

Nowy folder projektu

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:

Aplikacja do zarządzania zadaniami z błędem w dzienniku konsoli CSP

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:

Aplikacja Todo z błędem w dzienniku konsoli 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:

Debugowanie chrome.storage za pomocą konsoli

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 localStoragechrome.storage wynikają z asychronionego charakteru funkcji chrome.storage:

  • Zamiast zapisywać do localStorage za pomocą prostego przypisania, musisz użyć funkcji 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ć dostęp do funkcji localStorage[myStorageName] bezpośrednio, musisz użyć funkcji chrome.storage.local.get(myStorageName,function(storage){...}), a potem w funkcji wywołania zwrotnego przeanalizować zwrócony obiekt storage.

    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 odnosi się do this prototypu Store. (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()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()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:

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 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:

Czy chcesz przejść do następnego kroku? Przejdź do sekcji Krok 3. Dodaj alarmy i powiadomienia »