Шаг 2. Импортируйте существующее веб-приложение

На этом этапе вы узнаете:

  • Как адаптировать существующее веб-приложение для платформы Chrome Apps.
  • Как обеспечить соответствие сценариев приложения политике безопасности контента (CSP).
  • Как реализовать локальное хранилище с помощью chrome.storage.local .

Примерное время выполнения этого шага: 20 минут.
Чтобы просмотреть то, что вы выполните на этом этапе, прыгните вниз этой страницы ↓ .

Импортируйте существующее приложение Todo

В качестве отправной точки импортируйте в свой проект стандартную версию JavaScript TodoMVC , обычного эталонного приложения.

Мы включили версию приложения TodoMVC в ZIP-архив со справочным кодом в папке todomvc . Скопируйте все файлы (включая папки) из todomvc в папку вашего проекта.

Скопируйте папку todomvc в папку codelab.

Вам будет предложено заменить index.html . Идите и примите.

Заменить index.html

Теперь в папке вашего приложения должна быть следующая файловая структура:

Новая папка проекта

Файлы, выделенные синим цветом, находятся в папке todomvc .

Перезагрузите приложение сейчас ( щелкните правой кнопкой мыши > Перезагрузить приложение ). Вы должны увидеть базовый пользовательский интерфейс, но вы не сможете добавлять задачи.

Приведение сценариев в соответствие с Политикой безопасности контента (CSP)

Откройте консоль DevTools ( щелкните правой кнопкой мыши > «Проверить элемент» , затем выберите вкладку «Консоль» ). Вы увидите ошибку об отказе в выполнении встроенного скрипта:

Приложение Todo с ошибкой журнала консоли CSP

Давайте исправим эту ошибку, приведя приложение в соответствие с Политикой безопасности контента . Одно из наиболее распространенных несоответствий CSP вызвано встроенным JavaScript. Примеры встроенного JavaScript включают обработчики событий в виде атрибутов DOM (например, <button onclick=''> ) и теги <script> с содержимым внутри HTML.

Решение простое: переместите встроенный контент в новый файл.

1. В нижней части index.html удалите встроенный JavaScript и вместо него включите 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. Создайте в папке js файл с именем bootstrap.js . Переместите ранее встроенный код в этот файл:

// Bootstrap app data
window.app = {};

Если вы перезагрузите приложение сейчас, у вас все еще будет нерабочее приложение Todo, но вы приближаетесь к этому.

Преобразовать localStorage в chrome.storage.local

Если вы сейчас откроете консоль DevTools, предыдущая ошибка должна исчезнуть. Однако появилась новая ошибка: window.localStorage недоступен:

Приложение Todo с ошибкой журнала консоли localStorage

Приложения Chrome не поддерживают localStorage , поскольку localStorage является синхронным. Синхронный доступ к блокирующим ресурсам (I/O) в однопоточной среде выполнения может привести к тому, что ваше приложение перестанет отвечать на запросы.

Приложения Chrome имеют эквивалентный API, который может хранить объекты асинхронно. Это поможет избежать иногда дорогостоящего процесса сериализации объекта->строка->объекта.

Чтобы устранить сообщение об ошибке в нашем приложении, вам необходимо преобразовать localStorage в chrome.storage.local .

Обновить разрешения приложения

Чтобы использовать chrome.storage.local , вам необходимо запросить разрешение storage . В файле Manifest.json добавьте "storage" в массив permissions :

"permissions": ["storage"],

Узнайте о local.storage.set() и local.storage.get().

Чтобы сохранять и получать элементы задач, вам необходимо знать о методах set() и get() API chrome.storage .

Метод set() принимает объект пары ключ-значение в качестве своего первого параметра. Необязательная функция обратного вызова является вторым параметром. Например:

chrome.storage.local.set({secretMessage:'Psst!',timeSet:Date.now()}, function() {
  console.log("Secret message saved");
});

Метод get() принимает необязательный первый параметр для ключей хранилища данных, которые вы хотите получить. Один ключ можно передать как строку; несколько ключей могут быть организованы в массив строк или объект словаря.

Второй обязательный параметр — это функция обратного вызова. В возвращаемом объекте используйте ключи, запрошенные в первом параметре, для доступа к сохраненным значениям. Например:

chrome.storage.local.get(['secretMessage','timeSet'], function(data) {
  console.log("The secret message:", data.secretMessage, "saved at:", data.timeSet);
});

Если вы хотите get() все, что в данный момент находится в chrome.storage.local , опустите первый параметр:

chrome.storage.local.get(function(data) {
  console.log(data);
});

В отличие от localStorage , вы не сможете проверять локально хранящиеся элементы с помощью панели ресурсов DevTools. Однако вы можете взаимодействовать с chrome.storage из консоли JavaScript следующим образом:

Используйте консоль для отладки chrome.storage

Предварительный просмотр необходимых изменений API

Большинство оставшихся шагов по преобразованию приложения Todo — это небольшие изменения в вызовах API. Изменение всех мест, где в данный момент используется localStorage , требует, хотя и отнимает много времени и подвержено ошибкам.

Ключевые различия между localStorage и chrome.storage обусловлены асинхронной природой chrome.storage :

  • Вместо записи в localStorage с использованием простого присваивания вам нужно использовать chrome.storage.local.set() с дополнительными обратными вызовами.

    var data = { todos: [] };
    localStorage[dbName] = JSON.stringify(data);
    

    против

    var storage = {};
    storage[dbName] = { todos: [] };
    chrome.storage.local.set( storage, function() {
      // optional callback
    });
    
  • Вместо прямого доступа к localStorage[myStorageName] вам нужно использовать chrome.storage.local.get(myStorageName,function(storage){...}) а затем проанализировать возвращенный объект storage в функции обратного вызова.

    var todos = JSON.parse(localStorage[dbName]).todos;
    

    против

    chrome.storage.local.get(dbName, function(storage) {
      var todos = storage[dbName].todos;
    });
    
  • Функция .bind(this) используется во всех обратных вызовах, чтобы гарантировать, this относится к this прототипа Store . (Дополнительную информацию о связанных функциях можно найти в документации MDN: Function.prototype.bind() .)

    function Store() {
      this.scope = 'inside Store';
      chrome.storage.local.set( {}, function() {
        console.log(this.scope); // outputs: 'undefined'
      });
    }
    new Store();
    

    против

    function Store() {
      this.scope = 'inside Store';
      chrome.storage.local.set( {}, function() {
        console.log(this.scope); // outputs: 'inside Store'
      }.bind(this));
    }
    new Store();
    

Помните об этих ключевых различиях, когда в следующих разделах мы будем рассматривать получение, сохранение и удаление элементов задач.

Получение элементов задач

Давайте обновим приложение Todo, чтобы получать элементы todo:

1. Метод конструктора Store инициализирует приложение Todo со всеми существующими элементами задач из хранилища данных. Метод сначала проверяет, существует ли хранилище данных. Если этого не произойдет, он создаст пустой массив todos и сохранит его в хранилище данных, чтобы не возникало ошибок чтения во время выполнения.

В js/store.js измените использование localStorage в методе конструктора на использование 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. Метод find() используется при чтении задач из Модели. Возвращаемые результаты изменяются в зависимости от того, фильтруете ли вы по «Всем», «Активным» или «Завершенным».

Преобразуйте 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. Подобно find() , findAll() получает все задачи из модели. Преобразуйте 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));
};

Сохраняйте элементы задач

Текущий метод save() представляет собой проблему. Это зависит от двух асинхронных операций (get и set), которые каждый раз работают со всем монолитным хранилищем JSON. Любые пакетные обновления более чем одного элемента задачи, например «отметить все задачи как выполненные», приведут к возникновению опасности для данных, известной как чтение после записи . Этой проблемы не возникло бы, если бы мы использовали более подходящее хранилище данных, например IndexedDB, но мы пытаемся свести к минимуму усилия по преобразованию для этой лаборатории кода.

Есть несколько способов исправить это, поэтому мы воспользуемся этой возможностью, чтобы немного реорганизовать save() , взяв массив идентификаторов задач для одновременного обновления:

1. Для начала оберните все, что уже находится внутри save() , обратным вызовом 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. Преобразуйте все экземпляры localStorage с помощью 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. Затем обновите логику, чтобы она работала с массивом, а не с одним элементом:

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));
};

Отметить задачи как выполненные

Теперь, когда приложение работает с массивами, вам нужно изменить способ обработки пользователем нажатия кнопки «Очистить завершено» (#) :

1. В контроллере.js обновите toggleAll() , чтобы он вызывал toggleComplete() только один раз с массивом задач вместо того, чтобы помечать задачу как завершенную одну за другой. Также удалите вызов _filter() так как вы будете настраивать 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. Теперь обновите toggleComplete() чтобы он мог принимать как одну задачу, так и массив задач. Это включает в себя перемещение filter() внутри update() , а не снаружи.

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();
};

Удалить все задачи

В store.js есть еще один метод, использующий localStorage :

Store.prototype.drop = function (callback) {
  localStorage[this._dbName] = JSON.stringify({todos: []});
  callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
};

Этот метод не вызывается в текущем приложении, поэтому, если вам нужна дополнительная задача, попробуйте реализовать его самостоятельно. Подсказка: взгляните на chrome.storage.local.clear() .

Запустите готовое приложение Todo.

Вы закончили Шаг 2! Перезагрузите приложение, и теперь у вас должна быть полностью работающая версия TodoMVC в Chrome.

Для получения дополнительной информации

Более подробную информацию о некоторых API, представленных на этом этапе, см.:

Готовы перейти к следующему шагу? Перейдите к шагу 3. Добавление сигналов тревоги и уведомлений »