今回はローカルにデータを保存するJSライブラリのlocalForageをPhaserに導入してみよう、というお話です。
ここで保存対象とするデータは、個人情報やカード情報、セッション情報などの重要なデータ以外とします。たとえばユーザーが指定した背景の色のカラーコードや、webアプリケーション側が生成した重要情報を含まないデータなどです。
Phaserに限らず、クライアント側に何かしらのデータを保存しておきたいというニーズはたまにあったりします。サイトの背景色や文字サイズなどの表示設定を個人に合わせるケースや、前サイトでの重要でない行動情報などのデータを保存しておきたいケースなどです。
こういう場合にパッと思いつくのはweb storageを使う方法です。とくに「local storage」は、クッキーと比べてそこそこのサイズのデータを有効期限なしに保存できます。
ただ、代償として非同期処理できないことやセキュリティ的な保護機構は一切存在しないデメリットがあります。つまり、local storageは同期処理であるため、パフォーマンス的にイマイチです。さらに、個人情報やセッション情報などをlocal storageに保存したら、攻撃者から窃取されたい放題になってしまいます。明示的に削除しないかぎり、データが残存し続けるというのも考え物なのですね。さらに、保存できるデータ型はstringのみ、という縛りもあります。
こういうデメリットのせいで、さして重要じゃないデータについてもlocal storageで運用するには二の足を踏む状況なわけです。
じゃあ代替手段はないの?という話ですが、手段は、ありまぁす。たとえばindexedDBなどは、その代表格でしょう。ただ、コイツはその実装方法が若干複雑です。シンプルにちょっとしたデータを格納したいんだよね、という場合には少し重いです。そこでよりフレンドリーに扱えるlocalForageの出番です。
localForageは、シンプルにストレージを扱えるJSライブラリです。前述のIndexedDBやWebSQLといった機能を、シンプルな記述で利用できるラッパーのような感じでしょうか。こういう機能だと他にはStore.jsもあります。
コイツの個人的に一番いいところは、メジャーなブラウザでもサポートに差のあるストレージ機能の複数に対応していることです。さらに、ブラウザが対応しているドライバーに勝手に合わせて保存してくれる、という親切機能付き!素晴らしいー。
優先度で言えば、上記の順になります。たとえばプログラマーがIndexedDBを使え!と指定していたとしても、ブラウザがIndexedDBを利用できなければ勝手にWebSQLで処理を進めてくれる、というわけです。
他にもAPIの実装が簡単、というのも大きいメリットです。localForage API Docsを見ると実装方法が記載されていますが、とても見通しのいいコードです。Promiseを利用している、ということもありますがコールバック関数での記述でもそこまで可動性は落ちません(でもPromiseが推奨なのでPromiseで書くのが正解かな)。コードの体裁が整っているのももちろんですが、このページ自体とても可読性が高いので見習いたいものです。
では実際に、PhaserへlocalForageを導入してみましょう。ここでは、以前作成したシーンの切り替えでフェードアウト・フェードインするコードを流用しています。
class FirstScene extends Phaser.Scene {
constructor() {
super('firstScene');
this.hoge = 'hoge';
}
create() {
// ローカルからデータを取得
localforage.getItem('hogekey')
.then(value=>{
if (value){
console.log('get item: ' + value);
this.hoge = value;
this.add.text(width / 2, height / 2 + 100, this.hoge).setOrigin(0.5);
localforage.removeItem('hogekey');
} else {
console.log('hoge is null.');
}
})
.catch(err=>console.log('get err: ' + err));
const { width, height } = this.game.canvas;
this.add.text(width / 2, height / 2, 'First Scene').setOrigin(0.5);
const zone = this.add.zone(width / 2, height / 2, width, height);
zone.setInteractive({
useHandCursor: true
});
zone.on('pointerdown', () => {
this.cameras.main.fadeOut(1200, 0, 0, 0);
this.cameras.main.once(Phaser.Cameras.Scene2D.Events.FADE_OUT_COMPLETE, () => {
this.scene.start('secondScene');
});
});
}
}
class SecondScene extends Phaser.Scene {
constructor() {
super('secondScene');
}
create() {
// ローカルにデータを保存
localforage.setItem('hogekey', "hoge fuga piyo")
.then(value=>console.log('value: ' + value))
.catch(err=>console.log('err: ' + err));
this.cameras.main.fadeIn(1000, 0, 0, 0);
const { width, height } = this.game.canvas;
this.add.text(width / 2, height / 2, 'Second Scene').setOrigin(0.5);
}
}
const config = {
type: Phaser.AUTO,
width: 400,
height: 300,
scene: [FirstScene, SecondScene]
};
new Phaser.Game(config);
最初のシーンでは、ローカルにデータが存在するか確認します。データが存在する場合は値を取得して表示しデータを削除します。が、存在しなければ何もしません。2つ目のシーンでは、ローカルにデータを保存します。つまり、2つ目のシーンまで最低1度でも見ているなら、最初のシーンではローカルに保存されたデータが表示されるわけです。逆に最初のシーンを見て1度ブラウザの更新ボタンを押すと、ローカルデータを作成する2つ目のシーンを見ていないためローカルにデータが保存されません。よってこの場合、データが存在しないのでローカルデータなしとなって何も表示されません。
これだけのコードで、簡単にデータを保存・読み込みが実装できました。
保存されたデータは、タブを閉じたとしても保持されます。一度タブを閉じて別タブで開いてみると、一度保存した情報を保持していることがわかります。
注意点は、存在しないキーのデータを取得しようとした場合は、エラーにならずnull
が返ってくるという点です。Issuesでも話題に上ったことがありますが、これは仕様です。よって、データの取得処理が正常に終わったとしても、取得結果がnull
のケースを想定しないといけないわけです。データが取得できなかったら、それはそれとして何かしらの処理をコードに落とし込んでおく必要がある、ってことですね。こういうのって、気付かないとスルーしそうだったので、事前にわかってよかったですホント。
今回は、localForageを使ってローカルにデータを保存する実装について考えてみました。
実装するのにさほど苦労せずそれでいて効果抜群なので、セキュリティ的なところがクリアできるデータならどんどん利用していきたいものです。