🗃️

chrome extension StorageArea の仕様

2022/08/15に公開

微妙に分かりにくいので、調べた。🧐🧐🧐

chrome.storage - Chrome Developers
StorageArea

はじめに

Chromeバージョン: 104.0.5112.81(Official Build) (64 ビット)
@types/chrome でいうと、バージョン 0.0.193

全て?のメソッドが Promise 実装になっていた。🙌
storage は大きく localsync がある。

  • sync: クラウドに保存され、アカウント内で共有される。
    • 容量制限厳し目、キー数も上限あり(512)
  • local: ローカルで沢山保存するものは local に保存する。
    • unlimitedStorage 権限を渡せば、いくらでも保存できる。

なので、設定情報は sync 、通常使用の情報は local が良いだろう。
使い方は同じ。

(その他の情報は、最上部リンクか、他記事のほうがきっと詳しい)
(見逃している機能があったら失礼&教えて下さい🙇)

基本

set()

await chrome.storage.local.set({ a: 1 })
await chrome.storage.local.get()
> { a: 1 }

await chrome.storage.local.set({ b: 1 })
await chrome.storage.local.get()
> { a: 1, b: 1 }

await chrome.storage.local.set({ b: 2, c: 2 })
await chrome.storage.local.get()
> { a: 1, b: 2, c: 2 }

get() の仕様はオブジェクトのマージ。
気にせず突っ込めば、そのまま上書きしてくれる。

get()

await chrome.storage.local.get()
> { a: 1, b: 2, c: 2 }

await chrome.storage.local.get('a')
> { a: 1 }

await chrome.storage.local.get(['b'])
> { b: 2 }

await chrome.storage.local.get(['a', 'd'])
> { a: 1, b: 2 }

await chrome.storage.local.get(['d'])
> {}

set() は空なら全て、指定すればそのキーが存在したら取ってくる。
単体でも無くても、必ずオブジェクトが帰ってくるので注意。
key の順番は保証されない。(そらオブジェクトなんで)
?. とか in 使って存在確認しながら使うのが吉。

delete()

await chrome.storage.local.get()
> { a: 1, b: 2, c: 2 }

await chrome.storage.local.remove('c')
await chrome.storage.local.get()
> { a: 1, b: 2 }

await chrome.storage.local.remove(['a', 'd'])
await chrome.storage.local.get()
> { b: 2 }

await chrome.storage.local.set({ a: 1, b: 2, c: 2 })
await chrome.storage.local.clear()
await chrome.storage.local.get()
> {}

指定の方法は get() と同様。
データが無くても何も起きない。
全消ししたい場合は clear() を使う。

getBytesInUse()

await chrome.storage.local.set({ a: 1, b: 2, c: 2 })
await chrome.storage.local.getBytesInUse()
> 6

await chrome.storage.local.clear()
await chrome.storage.local.getBytesInUse()
> 0

この文字列で 6byte らしい。
他アプリでMB単位になってもそんなに重くないんで、かなり優秀。(SSD)

挿入可能な値

Primitive values such as numbers will serialize as expected. Values with a typeof "object" and "function" will typically serialize to {}, with the exception of Array (serializes as expected), Date, and Regex (serialize using their String representation).

Deeplさん
数値のような原始的な値は、期待通りにシリアライズされます。typeof "object" と "function" を持つ値は、Array(期待どおりにシリアライズされます)、Date、Regex(String表現でシリアライズされます)を除いて、通常{}にシリアライズされます。

とのこと。

Date

でも Dateを入れると {} になってしまう。🤔
まぁ ISO8601 や ISO9075 で保存すれば良いかなと。

await chrome.storage.local.set({ date: new Date() })
await chrome.storage.local.get()
> { date:{} }

Array

Array を入れると想定通り挿入される。
これは使い勝手が良さそう。

await chrome.storage.local.set({ items: [1, 2, 3] })

await chrome.storage.local.get()
> { items: [1, 2, 3] }

応用

特定のキー名の item を取得する

s から始まるものを取得する。

await chrome.storage.local.set({
  s000: 'アイテムS0',
  a000: 'A000',
  s001: 'アイテムS1',
})

const bucket = await chrome.storage.local.get()

Object.entries(bucket)
> [['a000', 'A000'], ['s000', 'アイテムS0'], ['s001', 'アイテムS1']]

Object.entries(items).filter(([key]) => key.startsWith('s'))
> [['s000', 'アイテムS0'], ['s001', 'アイテムS1']]

Object.fromEntries(
  Object.entries(items).filter(([key]) => key.startsWith('s'))
)
> { s000: 'アイテムS0', s001: 'アイテムS1' }

Object.entries()Object.fromEntries() で object -> array -> object とするよく見る感じ。
キーは分割代入で取り出し、そのまま配列操作。
object じゃなくて良いなら、fromEntries() は省略できる。

get にフィルター機能は無いので、全件取得してフィルターをかける。
startsWith とか includes とか match とか使えば、どんな条件でも取り出せるはず。

カーソルみたいな機能があればなぁ...と。

特定のキー名の item を削除する

s から始まるものを消す。

await chrome.storage.local.set({
  s000: 'アイテムS0',
  a000: 'A000',
  s001: 'アイテムS1',
})

const bucket = await chrome.storage.local.get()

Object.keys(bucket)
> ['a000', 's000', 's001']

Object.keys(bucket).filter((key) => key.startsWith('s'))
> ['s000', 's001']

await chrome.storage.local.remove(
  Object.keys(bucket).filter((key) => key.startsWith('s'))
)
await chrome.storage.local.get()
> ['a000']

Object.keys() でキーを取り出し、そのまま配列操作。
storage 参照に配列を使うことができるので、お手軽実装となる。

データ構造考察

要するに key-value ストレージなので、どの粒度で保存するか考えるべし。

config を保存する場合、いくつかパターンが思いつく。

  • ① キーごとに保存
    • color: 'blue'
    • showMessage: false
    • screenName: 'foo'
  • ② config というオブジェクトを保存
    • config: { color: 'blue', 'showMessage': false, screenName: 'foo' }

post - message という構造の場合こんな感じ。

  • ① post, message それぞれ別の採番を行って保存
    • post000: Post
    • post001: Post
    • ...
    • message000: Message
    • message001: Message
    • ...
  • (①の派生) post - messages のオブジェクトを作って保存する
    • 000: Post & { messages: Messages[] }
    • 001: Post & { messages: Messages[] }
    • ...
  • ② post, message それぞれを配列として保存する
    • posts: Post[]
    • messages: Message[]

実装が簡単なのは①、情報のまとまりがきれいなのが②かな。

value 側の要素が大きくなりすぎると、取得時のコストが上がる気がする。
このあたりの最適解は分からないねぇ。。。

まぁ、②で実装したプログラムも、なんの遅延も無く動いているので、現代のPCじゃあんまり気にすることじゃないのかもしれない。

  • key1つに対して、紐づくvalueの塊を取り出す。
  • 検索機能はなく、全てを走査する必要がある。

というのを前提に、よく呼び出すものをkeyに置けるといいな、の精神で構築するのが良いと思っている。
キーが被らない設計のほうが大事。(1敗)

Discussion