Web StorageとIndexedDB
同じ値を保存して型を確認し、同じオリジンの別文書、別タブ、別オリジンから参照します。IndexedDBでは保存後のオブジェクト変更も試します。最後に一つずつ削除し、三つの保存先が独立していることを確認します。
説明
ブラウザには、文字列を名前付きで保存するlocalStorage・sessionStorageと、構造化した値を保存するIndexedDBがあります。どの文書やタブから同じ値を使えるか、読み込み直した後に残るか、どの操作で消すかが異なります。
同じ値を保存して型を確認し、同じオリジンの別文書、別タブ、別オリジンから参照します。IndexedDBでは保存後のオブジェクト変更も試します。最後に一つずつ削除し、三つの保存先が独立していることを確認します。
1演習の保存項目を初期化
storageResetはこの演習の二つのキーとIndexedDBの一レコードを削除して、現在値を返す関数です。
await storageReset()演習用の保存項目だけを初期化三つともnullです。localStorageとsessionStorageで存在しないキーをgetItemするとnullになります。IndexedDBの読取り関数も、レコードがなければnullを返すように用意しています。
2Web Storageは文字列を保存
オブジェクトをそのままlocalStorageへ渡します。
(localStorage.setItem("wb05-local",{qty:3})オブジェクトを渡す,localStorage.getItem("wb05-local")保存された文字列)保存値は文字列"[object Object]"です。Web Storageはオブジェクトをそのまま保持するAPIではありません。構造を残すには直列化して文字列へ変換します。
3JSONで構造を保存し直す
JSON文字列を保存し、取得後に解析します。
(localStorage.setItem("wb05-local",JSON.stringify({qty:3})構造をJSON文字列へ),JSON.parse取得した文字列を解析(localStorage.getItem("wb05-local")))qtyが数値3のオブジェクトへ戻ります。JSON.stringifyとJSON.parseは保存APIの外側で行う処理です。日付やMapなどJSONだけでは元の型をそのまま復元できない値にも注意が必要です。
4同じ名前でも別の保存先
sessionStorageへdraftという文字列を保存します。
(sessionStorage.setItem("wb05-session","draft")タブのセッション領域へ保存,await storageValues()三つの保存先を取得)localにはJSON文字列、sessionにはdraftが入り、IndexedDBはまだ空です。localStorageとsessionStorageは別の保存領域で、sessionStorageはオリジンに加えてトップレベルのタブの単位でも分かれます。
5IndexedDBへ構造化した値を保存
dbWriteはitemsストアのrecordというキーに値を保存し、書込みトランザクション完了を待つ関数です。
内部ではindexedDB.open("lbox-wb05",1)でDBを開き、初回のonupgradeneededでitemsストアを作成します。transaction("items","readwrite")からobjectStore("items").put(value,"record")を呼び、トランザクションのoncompleteで完了とします。
(globalThis.record={qty:3,tags:["pen"]}保存する構造化データ,await dbWrite(record)トランザクション完了まで待機,await dbRead()保存したレコードを取得)IndexedDBには数値と配列を持つ構造を保存できます。ここではJSON文字列への変換を挟みません。APIは非同期なので、書込み完了を待ってから読み取っています。
個別のput要求の成功とトランザクション全体の完了は別です。後続の要求で失敗してトランザクションが中止される可能性があるため、この関数はoncompleteまで待ちます。
6保存後に元のオブジェクトを変更
メモリ上のrecordだけを変更して、保存済みの値を読みます。
(record.qty=9,record.tags.push("book")メモリ上の値だけ変更,{memory:record,stored:await dbRead()保存済み値を再取得})メモリ側はqty=9、tagsにbookが増えていますが、保存値はqty=3とpenのままです。IndexedDBには構造化複製された値が保存され、同じオブジェクトへの参照を保持しているわけではありません。保存値を変更するには書込みが必要です。
7同じタブ内の別文書から参照
probeFrameは同じオリジンの文書を新しく読み込み、三つの保存値を返します。
await probeFrame()同じオリジンの別文書を読み込む三つとも保存した値が読めます。文書の読込みが新しくなっても、同じオリジン・同じタブの保存領域は引き継がれます。localStorageとIndexedDBは、通常のページ再読込みだけでは消えません。
8openerのある別タブ
probeTabは同じオリジンの別タブを開き、初期値を取得した後、そのタブのsessionStorageだけをchildへ変えます。
({child:await probeTab(false)openerを持つ別タブ,parentSession:sessionStorage.getItem("wb05-session")元のタブの値})別タブのsessionStorageにはopenerからの初期コピーとしてdraftが入ります。しかし別タブでchildに変えても、元のタブはdraftのままです。コピーされた初期値が同じことと、保存領域を共有することは異なります。localStorageとIndexedDBは同じオリジンの保存先を共有します。
9openerなしの新しいタブ
noopenerを指定して別タブを開きます。
await probeTab(true)noopenerで新しいタブを開くlocalとindexedは同じ値ですが、別タブの初期sessionはnullです。openerのない新しいタブではsessionStorageの初期コピーがなく、そのタブ独自の領域になります。セッション復元などブラウザの復元機能は別途考慮が必要です。
10別オリジンからの参照
probeFrame(true)はホスト名の異なる文書で、同名の演習キーを確認します。
await probeFrame(true)別オリジンの保存値を取得別オリジン側の三つの値はnullです。この演習では127.0.0.1とlocalhostでホスト名が異なり、保存先も別になります。同じサーバーへ到達することと、オリジンが同じであることは一致しません。
11localStorageだけ削除
localStorageのキーだけを削除して全体を読みます。
(localStorage.removeItem("wb05-local")一つのキーを削除,await storageValues()他の保存先は残る)localだけがnullになり、sessionとIndexedDBのレコードは残ります。ログアウトや利用者の切替で保存値を消す場合、どの保存先に何を持たせたかを把握して削除します。
12IndexedDBのレコードを削除
IndexedDBのrecordを削除し、最後にsessionStorageの演習キーも削除します。
(await dbDelete()レコード削除の完了を待機,sessionStorage.removeItem("wb05-session")タブ内の演習キーを削除,await storageValues())三つともnullになりました。dbDeleteはデータベース全体ではなく一レコードを消しています。保存期間は容量制限・利用者の削除・プライベート閲覧などにも影響されるため、重要データの唯一の保存先として永続を保証するものではありません。
まとめ
localStorageとsessionStorageは文字列の保存先で、IndexedDBは構造化した値を非同期に保存します。
同じオリジンでもsessionStorageはタブごとに分かれます。openerがある場合の初期コピーと、その後の共有は別です。
文書の読込み・別タブ・別オリジン・項目削除を区別して、値が残る範囲を設計します。
操作履歴と解説を振り返れます。
ブラウザ / JavaScript
await storageReset(){
"local": null,
"session": null,
"indexed": null
}(localStorage.setItem("wb05-local",{qty:3}),localStorage.getItem("wb05-local"))"[object Object]"
(localStorage.setItem("wb05-local",JSON.stringify({qty:3})),JSON.parse(localStorage.getItem("wb05-local"))){
"qty": 3
}(sessionStorage.setItem("wb05-session","draft"),await storageValues()){
"local": "{\"qty\":3}",
"session": "draft",
"indexed": null
}(globalThis.record={qty:3,tags:["pen"]},await dbWrite(record),await dbRead()){
"qty": 3,
"tags": [
"pen"
]
}(record.qty=9,record.tags.push("book"),{memory:record,stored:await dbRead()}){
"memory": {
"qty": 9,
"tags": [
"pen",
"book"
]
},
"stored": {
"qty": 3,
"tags": [
"pen"
]
}
}await probeFrame(){
"local": "{\"qty\":3}",
"session": "draft",
"indexed": {
"qty": 3,
"tags": [
"pen"
]
}
}({child:await probeTab(false),parentSession:sessionStorage.getItem("wb05-session")}){
"child": {
"initial": {
"local": "{\"qty\":3}",
"session": "draft",
"indexed": {
"qty": 3,
"tags": [
"pen"
]
}
},
"childSession": "child"
},
"parentSession": "draft"
}await probeTab(true){
"initial": {
"local": "{\"qty\":3}",
"session": null,
"indexed": {
"qty": 3,
"tags": [
"pen"
]
}
},
"childSession": "child"
}await probeFrame(true){
"local": null,
"session": null,
"indexed": null
}(localStorage.removeItem("wb05-local"),await storageValues()){
"local": null,
"session": "draft",
"indexed": {
"qty": 3,
"tags": [
"pen"
]
}
}(await dbDelete(),sessionStorage.removeItem("wb05-session"),await storageValues()){
"local": null,
"session": null,
"indexed": null
}演習完了