このステップでは、次のことを学習します。
- 既存のウェブ アプリケーションを Chrome アプリ プラットフォーム用に適応させる方法。
- アプリ スクリプトをコンテンツ セキュリティ ポリシー(CSP)に準拠させる方法。
- chrome.storage.local を使用してローカル ストレージを実装する方法
このステップの所要時間: 20 分
このステップで完了する内容をプレビューするには、このページの下部に移動 ↓してください。
既存の ToDo アプリをインポートする
最初に、一般的なベンチマーク アプリである TodoMVC のバニラ JavaScript バージョンをプロジェクトにインポートします。
TodoMVC アプリのバージョンは、リファレンス コード zip の todomvc フォルダに含まれています。todomvc のすべてのファイル(フォルダを含む)をプロジェクト フォルダにコピーします。
index.html の置き換えを求められます。承諾してください。
アプリケーション フォルダのファイル構造は次のようになります。
青色でハイライト表示されたファイルは、todomvc フォルダのものです。
今すぐアプリを再読み込みします(右クリック > [再読み込み])。基本的な UI は表示されますが、ToDo を追加することはできません。
スクリプトをコンテンツ セキュリティ ポリシー(CSP)に準拠させる
DevTools コンソールを開きます(右クリック > [要素を検証] を選択し、[コンソール] タブを選択します)。インライン スクリプトの実行を拒否するエラーが表示されます。
アプリをコンテンツ セキュリティ ポリシーに準拠させて、このエラーを修正しましょう。CSP に準拠していない最も一般的な原因の 1 つは、インライン JavaScript によるものです。インライン JavaScript の例としては、DOM 属性としてのイベント ハンドラ(<button onclick=''>
など)や、HTML 内にコンテンツを含む <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
が使用できないという新しいエラーが発生します。
localStorage
は同期であるため、Chrome アプリでは localStorage
はサポートされていません。シングルスレッド ランタイムでブロックリソース(I/O)に同期的にアクセスすると、アプリが応答しなくなる可能性があります。
Chrome アプリには、オブジェクトを非同期で保存できる同等の API があります。これにより、オブジェクトから文字列、オブジェクトへのシリアル化プロセスが不要になる場合があります。
アプリのエラー メッセージに対処するには、localStorage
を chrome.storage.local に変換する必要があります。
アプリの権限の更新
chrome.storage.local
を使用するには、storage
権限をリクエストする必要があります。manifest.json で、permissions
配列に "storage"
を追加します。
"permissions": ["storage"],
local.storage.set() と local.storage.get() について
タスクリストを保存して取得するには、chrome.storage
API の set()
メソッドと get()
メソッドについて理解する必要があります。
set() メソッドは、Key-Value ペアのオブジェクトを最初のパラメータとして受け取ります。2 つ目のパラメータは、省略可能なコールバック関数です。例:
chrome.storage.local.set({secretMessage:'Psst!',timeSet:Date.now()}, function() {
console.log("Secret message saved");
});
get() メソッドは、取得するデータストアキーの最初のパラメータ(省略可)を受け付けます。単一のキーは文字列として渡すことができます。複数のキーは、文字列の配列または辞書オブジェクトに配置できます。
2 番目のパラメータ(必須)はコールバック関数です。返されたオブジェクトで、最初のパラメータでリクエストされたキーを使用して、格納されている値にアクセスします。例:
chrome.storage.local.get(['secretMessage','timeSet'], function(data) {
console.log("The secret message:", data.secretMessage, "saved at:", data.timeSet);
});
現在 chrome.storage.local
にあるすべてのものを get()
する場合は、最初のパラメータを省略します。
chrome.storage.local.get(function(data) {
console.log(data);
});
localStorage
とは異なり、DevTools の [Resources] パネルを使用して、ローカルに保存されているアイテムを検査することはできません。ただし、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; });
this
がStore
プロトタイプのthis
を参照するように、すべてのコールバックで関数.bind(this)
が使用されます。(バインド関数の詳細については、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 アイテムをすべて使用して、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()
メソッドは、モデルから ToDo を読み取るときに使用されます。返される結果は、[すべて]、[有効]、[完了] のいずれでフィルタするかによって異なります。
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()
はモデルからすべての ToDo を取得します。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()
メソッドには課題があります。これは、モノリシックな JSON ストレージ全体に対して毎回実行される 2 つの非同期オペレーション(get と set)に依存します。「すべての ToDo を完了済みとしてマークする」など、複数の ToDo アイテムに対してバッチ更新を行うと、書き込み後読み取りと呼ばれるデータハザードが発生します。IndexedDB などの適切なデータ ストレージを使用すれば、この問題は発生しませんが、この Codelab では変換作業を最小限に抑えようとしています。
修正方法はいくつかあるため、この機会に ToDo ID の配列を一度に更新して、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));
};
やることリストのアイテムを完了としてマークする
アプリが配列で動作するようにしたので、ユーザーが [Clear completed (#)] ボタンをクリックしたときにアプリが処理する方法を変える必要があります。
1. controller.js で、todo を 1 つずつ完了としてマークするのではなく、todo の配列で toggleComplete()
を 1 回だけ呼び出すように toggleAll()
を更新します。toggleComplete
_filter()
を調整するため、_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()
を更新して、1 つの 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:
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
を使用するメソッドがもう 1 つあります。
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 の詳細については、以下をご覧ください。
- コンテンツ セキュリティ ポリシー ↑
- 権限を宣言する ↑
- chrome.storage ↑
- chrome.storage.local.get() ↑
- chrome.storage.local.set() ↑
- chrome.storage.local.remove() ↑
- chrome.storage.local.clear() ↑
次のステップに進む準備はできていますか?ステップ 3 - アラートと通知を追加する » に進みます。