ページのリロード全体でのデータの永続化:Cookie、IndexedDB、およびその間のすべて
公開: 2022-03-11Webサイトにアクセスしているとします。 ナビゲーションリンクの1つを右クリックし、新しいウィンドウでリンクを開くことを選択します。 何が起こるべきですか? ほとんどのユーザーと同じように、新しいページには、リンクを直接クリックした場合と同じコンテンツが含まれていると思います。 唯一の違いは、ページが新しいウィンドウに表示されることです。 ただし、Webサイトがシングルページアプリケーション(SPA)の場合、このケースを慎重に計画しない限り、奇妙な結果が表示される可能性があります。
SPAでは、一般的なナビゲーションリンクは、ハッシュマーク(#)で始まるフラグメント識別子であることが多いことを思い出してください。 リンクを直接クリックしてもページは再読み込みされないため、JavaScript変数に保存されているすべてのデータが保持されます。 しかし、新しいタブまたはウィンドウでリンクを開くと、ブラウザはページをリロードし、すべてのJavaScript変数を再初期化します。 したがって、これらの変数にバインドされたHTML要素は、何らかの方法でそのデータを保持するための手順を実行しない限り、表示が異なります。
F5キーを押すなどして、ページを明示的にリロードした場合にも、同様の問題が発生します。 サーバーから変更を自動的にプッシュするメカニズムを設定しているので、F5キーを押す必要はないと思うかもしれません。 しかし、私が一般的なユーザーであれば、ページをリロードすることは間違いありません。 たぶん、私のブラウザが画面を間違って塗り直したように見えるか、または私が最新の株価を持っていることを確認したいだけです。
APIはステートレスである可能性がありますが、人間の相互作用はそうではありません
RESTful APIを介した内部リクエストとは異なり、人間のユーザーによるWebサイトとの対話はステートレスではありません。 Webユーザーとして、私はあなたのサイトへの訪問を、まるで電話のようなセッションだと思っています。 営業またはサポートラインに電話をかけるときと同じように、ブラウザが私のセッションに関するデータを記憶していることを期待しています。担当者は、電話の前半で話されたことを覚えていると思います。
セッションデータの明らかな例は、ログインしているかどうか、ログインしている場合はどのユーザーとしてログインしているかです。 ログイン画面を通過すると、サイトのユーザー固有のページを自由にナビゲートできるようになります。 新しいタブまたはウィンドウでリンクを開き、別のログイン画面が表示された場合、それはあまりユーザーフレンドリーではありません。
もう1つの例は、eコマースサイトのショッピングカートの内容です。 F5キーを押すとショッピングカートが空になると、ユーザーは動揺する可能性があります。
PHPで記述された従来のマルチページアプリケーションでは、セッションデータは$_SESSIONスーパーグローバル配列に格納されます。 ただし、SPAでは、クライアント側のどこかにある必要があります。 SPAにセッションデータを保存するには、主に4つのオプションがあります。
- クッキー
- フラグメント識別子
- Webストレージ
- IndexedDB
4キロバイトのCookie
Cookieは、ブラウザの古い形式のWebストレージです。 これらは元々、サーバーから受信したデータを1つのリクエストに保存し、それを後続のリクエストでサーバーに送り返すことを目的としていました。 ただし、JavaScriptからは、Cookieを使用して、Cookieあたり最大4 KBのサイズ制限まで、ほぼすべての種類のデータを保存できます。 AngularJSは、Cookieを管理するためのngCookiesモジュールを提供します。 どのフレームワークでも同様の機能を提供するjs-cookiesパッケージもあります。
作成したCookieは、ページのリロードであろうとAjaxリクエストであろうと、リクエストごとにサーバーに送信されることに注意してください。 ただし、保存する必要のあるメインセッションデータがログインユーザーのアクセストークンである場合は、とにかくすべてのリクエストでこれをサーバーに送信する必要があります。 この自動Cookie送信を、Ajaxリクエストのアクセストークンを指定する標準的な手段として使用するのは自然なことです。
この方法でCookieを使用することは、RESTfulアーキテクチャと互換性がないと主張するかもしれません。 ただし、この場合、APIを介した各リクエストはステートレスであり、いくつかの入力といくつかの出力があるため、問題ありません。 入力の1つがCookieを介して面白い方法で送信されているだけです。 ログインAPIリクエストでアクセストークンをCookieで返送するように手配できる場合は、クライアント側のコードでCookieを処理する必要はほとんどありません。 繰り返しになりますが、これはリクエストからの別の出力であり、通常とは異なる方法で返されます。
Cookieには、Webストレージに比べて1つの利点があります。 ログインフォームに「ログインしたままにする」チェックボックスを指定できます。 セマンティクスでは、チェックを外したままにすると、ページをリロードしたり、新しいタブやウィンドウでリンクを開いたりしてもログインしたままになると思いますが、ブラウザを閉じると必ずログアウトされます。 共有コンピューターを使用している場合、これは重要な安全機能です。 後で説明するように、Webストレージはこの動作をサポートしていません。
では、このアプローチは実際にどのように機能するのでしょうか。 サーバー側でLoopBackを使用しているとします。 Personモデルを定義し、組み込みのUserモデルを拡張して、ユーザーごとに維持するプロパティを追加しました。 RESTを介して公開されるようにPersonモデルを構成しました。 次に、server / server.jsを微調整して、目的のCookieの動作を実現する必要があります。 以下はserver/server.jsで、slcループバックによって生成されたものから始まり、マークされた変更が加えられています。
var loopback = require('loopback'); var boot = require('loopback-boot'); var app = module.exports = loopback(); app.start = function() { // start the web server return app.listen(function() { app.emit('started'); var baseUrl = app.get('url').replace(/\/$/, ''); console.log('Web server listening at: %s', baseUrl); if (app.get('loopback-component-explorer')) { var explorerPath = app.get('loopback-component-explorer').mountPath; console.log('Browse your REST API at %s%s', baseUrl, explorerPath); } }); }; // start of first change app.use(loopback.cookieParser('secret')); // end of first change // Bootstrap the application, configure models, datasources and middleware. // Sub-apps like REST API are mounted via boot scripts. boot(app, __dirname, function(err) { if (err) throw err; // start of second change app.remotes().after('Person.login', function (ctx, next) { if (ctx.result.id) { var opts = {signed: true}; if (ctx.req.body.rememberme !== false) { opts.maxAge = 1209600000; } ctx.res.cookie('authorization', ctx.result.id, opts); } next(); }); app.remotes().after('Person.logout', function (ctx, next) { ctx.res.cookie('authorization', ''); next(); }); // end of second change // start the server if `$ node server.js` if (require.main === module) app.start(); });最初の変更は、Cookieパーサーが「secret」をCookie署名シークレットとして使用するように構成し、それによって署名されたCookieを有効にします。 LoopBackはCookieの「authorization」または「access_token」のいずれかでアクセストークンを検索しますが、そのようなCookieに署名する必要があるため、これを行う必要があります。 実際、この要件は無意味です。 Cookieに署名することは、Cookieが変更されていないことを確認することを目的としています。 ただし、アクセストークンを変更する危険はありません。 結局のところ、通常のパラメーターとして、署名されていない形式でアクセストークンを送信することもできます。 したがって、他の目的で署名されたCookieを使用している場合を除き、Cookieの署名の秘密を推測するのが難しいことを心配する必要はありません。
2番目の変更は、Person.loginメソッドとPerson.logoutメソッドのいくつかの後処理を設定します。 Person.loginの場合、結果のアクセストークンを取得し、署名されたCookieの「承認」としてクライアントに送信する必要があります。 クライアントは、資格情報パラメータremembermeにもう1つのプロパティを追加して、Cookieを2週間永続化するかどうかを示すことができます。 デフォルトはtrueです。 ログインメソッド自体はこのプロパティを無視しますが、ポストプロセッサはそれをチェックします。
Person.logoutの場合、このCookieをクリアする必要があります。
これらの変更の結果は、StrongLoopAPIExplorerですぐに確認できます。 通常、Person.loginリクエストの後、アクセストークンをコピーして右上のフォームに貼り付け、[アクセストークンの設定]をクリックする必要があります。 しかし、これらの変更により、それを行う必要はありません。 アクセストークンはCookieの「承認」として自動的に保存され、後続のリクエストごとに返送されます。 エクスプローラーがPerson.loginからの応答ヘッダーを表示しているとき、JavaScriptがSet-Cookieヘッダーを表示することは許可されていないため、Cookieは省略されます。 ただし、Cookieはそこにありますのでご安心ください。
クライアント側では、ページのリロードで、Cookieの「承認」が存在するかどうかを確認します。 その場合は、現在のuserIdのレコードを更新する必要があります。 おそらくこれを行う最も簡単な方法は、ログインが成功したときにuserIdを別のCookieに保存して、ページのリロード時に取得できるようにすることです。
フラグメント識別子
SPAとして実装されているWebサイトにアクセスしているとき、ブラウザのアドレスバーのURLは「https://example.com/#/my-photos/37」のようになります。 このフラグメント識別子の部分である「#/ my-photos / 37」は、セッションデータとして表示できる状態情報のコレクションです。 この場合、私はおそらく私の写真の1つ、IDが37の写真を見ています。
フラグメント識別子内に他のセッションデータを埋め込むことを決定できます。 前のセクションでは、アクセストークンがCookieの「承認」に保存されているため、何らかの方法でuserIdを追跡する必要があったことを思い出してください。 1つのオプションは、別のCookieに保存することです。 しかし、別のアプローチは、それをフラグメント識別子に埋め込むことです。 ログインしている間、アクセスするすべてのページに「#/ u / XXX」で始まるフラグメント識別子が含まれると判断できます。ここで、XXXはuserIdです。 したがって、前の例では、userIdが59の場合、フラグメント識別子は「#/ u / 59 / my-photos/37」になる可能性があります。
理論的には、アクセストークン自体をフラグメント識別子に埋め込んで、CookieやWebストレージの必要性を回避することができます。 しかし、それは悪い考えです。 アクセストークンがアドレスバーに表示されます。 カメラを持って私の肩越しに見ている人は誰でも画面のスナップショットを撮ることができ、それによって私のアカウントにアクセスできます。

最後に、フラグメント識別子をまったく使用しないようにSPAを設定することができます。 代わりに、「http://example.com/app/dashboard」や「http://example.com/app/my-photos/37」などの通常のURLを使用し、サーバーはトップレベルのHTMLを返すように構成されています。これらのURLのいずれかの要求に応答するSPA。 次に、SPAは、フラグメント識別子ではなく、パス(たとえば、「/ app/dashboard」または「/app/ my-photos / 37」)に基づいてルーティングを実行します。 ナビゲーションリンクのクリックをインターセプトし、 History.pushState()を使用して新しいURLをプッシュし、通常どおりルーティングを続行します。 また、ポップステートイベントをリッスンして、ユーザーが[戻る]ボタンをクリックしたことを検出し、復元されたURLのルーティングを再度続行します。 これを実装する方法の詳細は、この記事の範囲を超えています。 ただし、この手法を使用すると、フラグメント識別子の代わりにセッションデータをパスに格納できることは明らかです。
Webストレージ
Webストレージは、JavaScriptがブラウザ内にデータを保存するためのメカニズムです。 Cookieと同様に、Webストレージはオリジンごとに分離されています。 保存された各アイテムには名前と値があり、どちらも文字列です。 ただし、Webストレージはサーバーからは完全に見えず、Cookieよりもはるかに大きなストレージ容量を提供します。 Webストレージには、ローカルストレージとセッションストレージの2種類があります。
ローカルストレージのアイテムは、すべてのウィンドウのすべてのタブに表示され、ブラウザを閉じた後も保持されます。 この点で、それは非常に遠い将来の有効期限を持つクッキーのように振る舞います。 したがって、ユーザーがログインフォームで「ログインしたままにする」にチェックを入れた場合にアクセストークンを保存するのに適しています。
セッションストレージのアイテムは、それが作成されたタブ内にのみ表示され、そのタブを閉じると消えます。 これにより、その存続期間は他のCookieの存続期間とは大きく異なります。 セッションCookieは、すべてのウィンドウのすべてのタブに引き続き表示されることを思い出してください。
AngularJS SDK for LoopBackを使用する場合、クライアント側は自動的にWebストレージを使用して、アクセストークンとuserIdの両方を保存します。 これは、js / services/lb-services.jsのLoopBackAuthサービスで発生します。 RememberMeパラメータがfalse(通常は「ログインしたままにする」チェックボックスがオフになっていることを意味します)でない限り、ローカルストレージを使用します。この場合、セッションストレージを使用します。
その結果、「ログインしたままにする」をオフにしてログインし、新しいタブまたはウィンドウでリンクを開くと、そこにログインできなくなります。 ほとんどの場合、ログイン画面が表示されます。 これが許容できる動作であるかどうかは、自分で判断できます。 それぞれが異なるユーザーとしてログインする複数のタブを持つことができる優れた機能だと考える人もいるかもしれません。 または、共有コンピュータを使用する人がほとんどいないと判断する場合もあるので、[ログインしたままにする]チェックボックスを完全に省略できます。
では、AngularJS SDK for LoopBackを使用することにした場合、セッションデータの処理はどのようになりますか? サーバー側で以前と同じ状況が発生したとします。Personモデルを定義し、Userモデルを拡張し、RESTを介してPersonモデルを公開しました。 Cookieを使用しないため、前述の変更は必要ありません。
クライアント側では、最も外側のコントローラーのどこかに、現在ログインしているユーザーのuserIdを保持する$ scope.currentUserIdのような変数があるか、ユーザーがログインしていない場合はnullがあります。次に、ページの再読み込みを適切に処理するには、このステートメントをそのコントローラーのコンストラクター関数に含めるだけです。
$scope.currentUserId = Person.getCurrentId();とても簡単です。 コントローラの依存関係として「Person」を追加します(まだ追加されていない場合)。
IndexedDB
IndexedDBは、ブラウザに大量のデータを保存するための新しい機能です。 これを使用すると、シリアル化することなく、オブジェクトや配列などの任意のJavaScriptタイプのデータを格納できます。 データベースに対するすべてのリクエストは非同期であるため、リクエストが完了するとコールバックを受け取ります。
IndexedDBを使用して、サーバー上のデータとは関係のない構造化データを格納できます。 例としては、カレンダー、やることリスト、ローカルでプレイされるセーブドゲームなどがあります。 この場合、アプリケーションは実際にはローカルのものであり、Webサイトはそれを配信するための手段にすぎません。
現在、InternetExplorerとSafariはIndexedDBを部分的にしかサポートしていません。 他の主要なブラウザはそれを完全にサポートしています。 ただし、現時点での重大な制限の1つは、FirefoxがプライベートブラウジングモードでIndexedDBを完全に無効にすることです。
IndexedDBの具体的な使用例として、PavolDanišによるスライディングパズルアプリケーションを微調整して、移動するたびに最初のパズルであるAngularJSロゴに基づくBasic3x3スライディングパズルの状態を保存してみましょう。 ページをリロードすると、この最初のパズルの状態が復元されます。
これらの変更を加えてリポジトリのフォークを設定しました。これらはすべてapp/js / plugin/slidingPuzzle.jsにあります。 ご覧のとおり、IndexedDBの基本的な使用法でさえかなり複雑です。 以下にハイライトを示します。 まず、ページの読み込み中に関数restoreが呼び出され、IndexedDBデータベースが開きます。
/* * Tries to restore game */ this.restore = function(scope, storekey) { this.storekey = storekey; if (this.db) { this.restore2(scope); } else if (!window.indexedDB) { console.log('SlidingPuzzle: browser does not support indexedDB'); this.shuffle(); } else { var self = this; var request = window.indexedDB.open('SlidingPuzzleDatabase'); request.onerror = function(event) { console.log('SlidingPuzzle: error opening database, ' + request.error.name); scope.$apply(function() { self.shuffle(); }); }; request.onupgradeneeded = function(event) { event.target.result.createObjectStore('SlidingPuzzleStore'); }; request.onsuccess = function(event) { self.db = event.target.result; self.restore2(scope); }; } };request.onupgradeneededイベントは、データベースがまだ存在しない場合を処理します。 この場合、オブジェクトストアを作成します。
データベースが開かれると、関数restore2が呼び出され、指定されたキー(この場合は実際には定数「Basic」)を持つレコードが検索されます。
/* * Tries to restore game, once database has been opened */ this.restore2 = function(scope) { var transaction = this.db.transaction('SlidingPuzzleStore'); var objectStore = transaction.objectStore('SlidingPuzzleStore'); var self = this; var request = objectStore.get(this.storekey); request.onerror = function(event) { console.log('SlidingPuzzle: error reading from database, ' + request.error.name); scope.$apply(function() { self.shuffle(); }); }; request.onsuccess = function(event) { if (!request.result) { console.log('SlidingPuzzle: no saved game for ' + self.storekey); scope.$apply(function() { self.shuffle(); }); } else { scope.$apply(function() { self.grid = request.result; }); } }; }そのようなレコードが存在する場合、その値がパズルのグリッド配列に置き換わります。 ゲームの復元でエラーが発生した場合は、以前と同じようにタイルをシャッフルします。 グリッドはタイルオブジェクトの3x3配列であり、それぞれがかなり複雑であることに注意してください。 IndexedDBの大きな利点は、シリアル化せずにそのような値を格納および取得できることです。
$ applyを使用して、モデルが変更されたことをAngularJSに通知するため、ビューが適切に更新されます。 これは、更新がDOMイベントハンドラー内で行われているため、AngularJSが変更を検出できないためです。 IndexedDBを使用するAngularJSアプリケーションは、この理由からおそらく$applyを使用する必要があります。
ユーザーによる移動など、グリッド配列を変更するアクションの後に、関数saveが呼び出され、更新されたグリッド値に基づいて、適切なキーでレコードが追加または更新されます。
/* * Tries to save game */ this.save = function() { if (!this.db) { return; } var transaction = this.db.transaction('SlidingPuzzleStore', 'readwrite'); var objectStore = transaction.objectStore('SlidingPuzzleStore'); var request = objectStore.put(this.grid, this.storekey); request.onerror = function(event) { console.log('SlidingPuzzle: error writing to database, ' + request.error.name); }; request.onsuccess = function(event) { // successful, no further action needed }; }残りの変更は、適切なタイミングで上記の関数を呼び出すことです。 すべての変更を示すコミットを確認できます。 3つの高度なパズルではなく、基本的なパズルに対してのみ復元を呼び出していることに注意してください。 3つの高度なパズルにはapi属性があるという事実を利用しているため、それらのパズルでは通常のシャッフルを実行します。
高度なパズルも保存して復元したい場合はどうなりますか? それにはいくつかのリストラが必要になります。 高度なパズルのそれぞれで、ユーザーは画像ソースファイルとパズルのサイズを調整できます。 したがって、この情報を含めるには、IndexedDBに格納されている値を拡張する必要があります。 さらに重要なのは、復元からそれらを更新する方法が必要になることです。 これは、このすでに長い例では少し多いです。
結論
ほとんどの場合、セッションデータを保存するにはWebストレージが最善の策です。 すべての主要なブラウザで完全にサポートされており、Cookieよりもはるかに大きなストレージ容量を提供します。
サーバーがCookieを使用するようにすでに設定されている場合、またはすべてのウィンドウのすべてのタブでデータにアクセスできるようにする必要があるが、ブラウザーを閉じたときにCookieが削除されるようにする場合は、Cookieを使用します。
すでにフラグメント識別子を使用して、ユーザーが見ている写真のIDなど、そのページに固有のセッションデータを保存しています。 フラグメント識別子に他のセッションデータを埋め込むこともできますが、これはWebストレージやCookieに勝る利点はありません。
IndexedDBを使用すると、他のどの手法よりもはるかに多くのコーディングが必要になる可能性があります。 ただし、格納している値がシリアル化が難しい複雑なJavaScriptオブジェクトである場合、またはトランザクションモデルが必要な場合は、価値がある可能性があります。
