このステップでは、次のことを学びます。
- 既存のウェブ アプリケーションを Chrome アプリ プラットフォームに適応させる方法
- アプリ スクリプトをコンテンツ セキュリティ ポリシー(CSP)に準拠させる方法
- chrome.storage.local を使用してローカル ストレージを実装する方法
この手順を完了の推定所要時間: 20 分
このステップの内容を確認するには、このページの一番下に移動 ↓ をクリックしてください。
既存の Todo アプリをインポートする
まず、一般的なベンチマークである TodoMVC の標準 JavaScript バージョンをインポートします。 プロジェクトに組み込めます
TodoMVC アプリのバージョンは、todomvc の参照コードの zip に含まれています フォルダに配置されます。todomvc のすべてのファイル(フォルダを含む)をプロジェクト フォルダにコピーします。
index.html を置き換えるように求められます。同意して続行してください。
アプリケーションのフォルダ内には次のようなファイル構造が含まれています。
青色でハイライト表示されたファイルは、todomvc フォルダのものです。
今すぐアプリを再読み込みします(右クリック > [再読み込み])。基本的な UI は表示されますが、 タスクを追加できます
スクリプトをコンテンツ セキュリティ ポリシー(CSP)に準拠させる
DevTools コンソールを開きます(右クリック > [要素を検証] を選択し、[コンソール] タブを選択します)。マイページ インライン スクリプトの実行の拒否に関するエラーが表示されます。
アプリをコンテンツ セキュリティ ポリシーに準拠させることで、このエラーを解決しましょう。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
が利用できないことについて:
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() の詳細
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
を操作できます。
次のようになります。
必要な 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 アイテムを取得するために、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:
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 の一部の詳細については、以下をご覧ください。
- コンテンツ セキュリティ ポリシー ↑
- 権限を宣言する ↑
- chrome.storagechrome.storage ↑
- chrome.storage.local.get() ↑
- chrome.storage.local.set() ↑
- chrome.storage.local.remove() ↑
- chrome.storage.local.clear() ↑
次のステップに進む準備はできましたか?ステップ 3 - アラームと通知を追加する » に移動します。