ステップ 2: 既存のウェブアプリをインポートする

このステップでは、次のことを学びます。

  • 既存のウェブ アプリケーションを Chrome アプリ プラットフォームに適応させる方法
  • アプリ スクリプトをコンテンツ セキュリティ ポリシー(CSP)に準拠させる方法
  • chrome.storage.local を使用してローカル ストレージを実装する方法

この手順を完了の推定所要時間: 20 分
このステップの内容を確認するには、このページの一番下に移動 ↓ をクリックしてください。

既存の Todo アプリをインポートする

まず、一般的なベンチマークである TodoMVC標準 JavaScript バージョンをインポートします。 プロジェクトに組み込めます

TodoMVC アプリのバージョンは、todomvc参照コードの zip に含まれています フォルダに配置されます。todomvc のすべてのファイル(フォルダを含む)をプロジェクト フォルダにコピーします。

todomvc フォルダを Codelab フォルダにコピーする

index.html を置き換えるように求められます。同意して続行してください。

index.html を置き換える

アプリケーションのフォルダ内には次のようなファイル構造が含まれています。

新しいプロジェクト フォルダ

青色でハイライト表示されたファイルは、todomvc フォルダのものです。

今すぐアプリを再読み込みします(右クリック > [再読み込み])。基本的な UI は表示されますが、 タスクを追加できます

スクリプトをコンテンツ セキュリティ ポリシー(CSP)に準拠させる

DevTools コンソールを開きます(右クリック > [要素を検証] を選択し、[コンソール] タブを選択します)。マイページ インライン スクリプトの実行の拒否に関するエラーが表示されます。

CSP コンソールのログエラーを含む ToDo アプリのログ

アプリをコンテンツ セキュリティ ポリシーに準拠させることで、このエラーを解決しましょう。Google の CSP のコンプライアンス違反はインライン JavaScript が原因です。インライン JavaScript の例: イベント ハンドラを DOM 属性(例: <button onclick=''>)として、およびコンテンツを含む <script> タグ あります。

解決策は簡単です。インライン コンテンツを新しいファイルに移動します。

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 コンソールのログエラーが表示されます

localStorage は同期型であるため、Chrome アプリは localStorage をサポートしていません。同期アクセス リソース(I/O)をブロックすると、アプリが応答しなくなる可能性があります。

Chrome アプリには、オブジェクトを非同期で保存できる同等の API があります。これにより オブジェクト -> 文字列 -> オブジェクトのシリアル化プロセスに費用がかかることもあります。

アプリのエラー メッセージに対処するには、localStoragechrome.storage.local:

アプリの権限の更新

chrome.storage.local を使用するには、storage 権限をリクエストする必要があります。イン manifest.json を指定して、permissions 配列に "storage" を追加します。

"permissions": ["storage"],

local.storage.set() と local.storage.get() の詳細

ToDo アイテムを保存および取得するには、set() および get() chrome.storage API。

set() メソッドは、1 つ目のパラメータとして Key-Value ペアのオブジェクトを受け取ります。オプションの 2 番目のパラメータです。例:

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

get() メソッドは、目的のデータストア キーの省略可能な 1 つ目のパラメータを受け入れます。 あります。単一のキーを文字列として渡すことができます。複数のキーを 1 つの配列として ディクショナリ オブジェクトです。

2 番目の必須パラメータはコールバック関数です。返されたオブジェクトで、 保存された値にアクセスするために、最初のパラメータで要求されたキー。例:

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

現在 chrome.storage.local にあるすべてのものを get() する場合は、最初のものを省略します。 parameter:

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

localStorage とは異なり、ローカルに保存されているアイテムを DevTools で検査することはできません。 リソースパネル。ただし、次のように JavaScript コンソールから chrome.storage を操作できます。 次のようになります。

コンソールを使用して chrome.storage をデバッグする

必要な API の変更をプレビューする

Todo アプリを変換する残りの手順のほとんどは、API 呼び出しの小さな変更です。変更中 現在 localStorage が使用されているすべての場所。ただし、時間がかかり、エラーが発生しやすいため、 は必須です。

localStoragechrome.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 アイテムを取得するために、Todo アプリを更新しましょう。

1. Store コンストラクタ メソッドは、既存のすべての To-Do 項目をデータストアから取得します。このメソッドはまず、データストアが存在するかどうかを確認します。そうでない場合、 ランタイムの読み取りエラーが発生しないように、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() メソッドは、Model から ToDo を読み取るときに使用されます。返される結果は、 フィルタの基準として [すべて]、[有効]、[完了]のいずれかを選択できます。

chrome.storage.local を使用するように find() を変換します。

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

ToDo リスト項目を保存する

現在の save() メソッドには課題があります。2 つの非同期オペレーション(get と set)に依存する モノリシック JSON ストレージ全体で 毎回動作します1 つ以上のイベントに対するバッチ アップデートは、 すべての ToDo を完了としてマークするなど、ToDo リストの項目があると、 Read-After-Write。適切なデータストレージを使用すれば この問題は発生しません IndexedDB に似ていますが、この Codelab では変換作業を最小限に抑えるようにしています。

修正方法はいくつかあるため、これを機に save() を少しリファクタリングして、 ToDo ID の配列を一度に更新します。

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

ToDo アイテムを完了としてマークする

アプリが配列を操作するようになったので、ユーザーが [完了した数(#)を削除] ボタン:

1. controller.js で、toggleAll() を更新し、配列で toggleComplete() を 1 回だけ呼び出す ToDo を 1 つずつ完了としてマークする代わりに、_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() を更新して、単一の ToDo または ToDo の配列の両方を受け入れるようにします。例 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();
};

すべての ToDo アイテムをドロップ

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 は完了です。アプリを再読み込みすると、Chrome のパッケージ化されたバージョンが完全に機能するはずです。 ダウンロードしました

詳細情報

このステップで導入した API の一部の詳細については、以下をご覧ください。

次のステップに進む準備はできましたか?ステップ 3 - アラームと通知を追加する » に移動します。