🌊

chrome.storage の挙動をDevToolsで見る

2023/10/23に公開

この記事ではDevToolsを使って動作確認をしている。サービスワーカーのDevToolsを使うとchrome.storage.*が使えるが、ポップアップやWebサイトのDevToolsでは使えない。

await を使っているのはDevtools上で正しく動作させるためで、開発では使えない気がする(記憶あやふや)

実際にDevToolsを使ってこの記事のコードを動かしてみるには、chrome://extensionsを開き、確認したい拡張機能の「ビューを検証:Service Worker」をクリックして開かれるDevToolsを使います。

  • DBのように使えないのが辛い。DBというよりJSONファイルを保存している感覚に近い。検索などは別途Mapに変換して使うとか。オブジェクトを保存するときも、定義した順に保存されているとは限らない。実際、今開発している拡張機能ではキーの順番がバラバラになっている。

基本の動作

ストレージのデータを全部出力/全部削除

DevToolsで動作確認をするにあたり、データの確認をしたり不要なデータを削除することがよくある。そういうときには次の2つのコマンドが便利だ。データを全部出力するコードについて一言コメントをしておく。getの第一引数をnullにするとストレージに保存してある全てのデータが返ってくる。返ってきたデータをコールバック関数の引数で受け取り、console.logで出力している。

// 全部出力
await chrome.storage.local.get(null, ((data) => {console.log(data)}));
// 全部削除
await chrome.storage.local.clear()

上の全部出力するコードを入力して次のようなエラーが出たら、パーミッションにstorageを追加し忘れてないか確認しよう。

Uncaught TypeError: Cannot read properties of undefined (reading 'local')
    at <anonymous>:2:22
(anonymous) @ VM10:2

{key: value}という形式でオブジェクトを保存する。普通のオブジェクトと同じようにバリューの値は文字列、数値、配列、オブジェクトなどが可能

set()の構文
chrome.storage.local.set(object)
引数 説明
object object キーとバリューを指定したオブジェクトの形で保存する。キーはstring
get()の構文
chrome.storage.local.get(key, callback)
引数 説明
key string|array|object|null キーの値を指定してデータを取得する。stringの場合、単一のキーを指定して値を取り出す。arrayの場合、複数のキーを指定して値を取り出す。キーの型はstringだけなので、配列の値はstringのみ可能。objectの場合、。nullの場合、ストレージ内の全てのデータを取得する。
callback? function データを取得した後に実行するコールバック関数。

JSONに対応してる型が可能

保存できるバリューの型

  • string
  • number
  • array
  • object
  • boolean
  • null
terminal
// 文字列の場合
await chrome.storage.local.set({animal: "dog"});
await chrome.storage.local.get("animal");
// { "animal": "dog" }

// 数値の場合
await chrome.storage.local.set({point: 10});
await chrome.storage.local.get("point")
// {point: 10}

// 配列の場合
await chrome.storage.local.set({animal: ["dog", "cat"]})
await chrome.storage.local.get("animal")
// { "animal": ["dog", "cat"]}

// オブジェクトの場合
await chrome.storage.local.set({animal: {dog: "shiba", cat: "scottish"}})
await chrome.storage.local.get("animal")
// { "animal": { "cat": "scottish", "dog": "shiba" } }

存在しないキーにアクセスしたとき

存在しないキーにアクセスしたらエラーやfalseを返さずに{}を返すことに注意する。思わぬ挙動になってしまわないように、場合分けしたり、インストール時にデフォルト値を設定するといい。

await chrome.storage.local.get("notExistingKey");
# {}

保存できない型

BigInt, undefined, DOM, class, Set, Map, Regexp, Date などをそのまま保存しようとすると {} になるので注意。もし保存したければ、文字列や配列などJSONに対応してる型に変換して保存するといい。

terminal
// BigInt
await chrome.storage.local.set({bigint: BigInt(90000000000000000) })
await chrome.storage.local.get("bigint")
// {}

// undefined
await chrome.storage.local.set({undefined: undefined })
await chrome.storage.local.get("undefined")
// {}

// DOM はservice worker で作れないので省略

// Regexp
await chrome.storage.local.set({regexp:  /\w+/})
await chrome.storage.local.get("regexp")
// { "regexp": {} }

上記のように保存できない値を保存しようとしてもエラーが出るわけではない。特殊なデータ型を保存する場合は事前に調べておくといい。扱っているデータがchrome.storageAPIでどのように保存されるかを調べるには以下のようにstringify()メソッドを使う。

console.log(JSON.parse(JSON.stringify(obj)))

データは上書きされる

同じキーでデータを保存するとデータは上書きされる。当然前に入ってたデータは消えてしまう。別のキーでデータを保存すると追加で保存される。storageという大層な名前を持ちながら、実態としては1つのJSONファイルのイメージに近い。

terminal

ユースケース

キーの値を変数を使って保存したい

変数の値をキーとしてセットするときは[]で囲む必要がある。

const keyName = "animal";
const valueName = "dog"
await chrome.storage.local.set({keyName: valueName})
await chrome.storage.local.get(keyName);
// {}

一見うまく動きそうなコードだが空のオブジェクトが返ってきてしまう。set()ではkeyNameというキー名を持ったデータを保存し、get()ではanimalというキーを探してしまうからこのような挙動になってしまう。実際、get()で検索するキーを"keyName"という文字列にすると思い通りの結果が返ってくる。

const keyName = "animal";
const valueName = "dog"
await chrome.storage.local.set({keyName: valueName})
await chrome.storage.local.get("keyName");
// {keyName: 'dog'}

もちろん保存したいキーは"keyName"ではなく"animal"だから、変数を文字列として解釈させたい場合は、set()のときに変数を[]で囲んで[keyName]とすればいい。

const keyName = "animal";
const valueName = "dog"
await chrome.storage.local.set({[keyName]: valueName})
await chrome.storage.local.get(keyName);
// { animal: 'dog' }

データを更新する

chrome.storageはRDBのように同じキーを持ったまま複数の値を保存することはできない。そのため、データを更新して保存したい場合は次のように一手間かかる。

  1. 保存してあるデータを配列として取り出す
  2. 新しいデータを追加する
  3. 更新されたデータを保存
    このような手順でやればいい。get()のコールバック関数にset()を書き込むので冗長になってしまう。
terminal
// すでに保存してあるデータだとみなす
chrome.storage.local.set({animal: ["dog", "cat"]})

chrome.storage.local.get("animal", (result) => {
    // 1. 保存してあるデータを配列として取り出す
    let currentAnimals = result.animal

    // 2.新しいデータを追加する
    let newAnimals = ["bird", "fish"];
    currentAnimals.push(...newAnimals);

    // 3.更新されたデータを保存
    chrome.storage.local.set({animal: currentAnimals})
});
terminal
chrome.storage.local.get("animal")
// { animal: ['dog', 'cat', 'bird', 'fish'] }

インストール時に初期値を設定したい

存在しないキーにアクセスしたら{}を返してしまう。思わぬ挙動を引き起こさないために初期値をあらかじめ設定しておきたいときは、インストール時に発火するonInstalledイベントを使うといい。イベント名がonInstallなのでインストールしたときにした発火しないように思える。しかし拡張機能のアップデート、chromeのアップデートが行われたときも発火されるので注意する。インストール時にのみデータを動かしたい場合は、コールバック関数の中でif (details.reason === "install")のように条件分岐を行う。なお開発モードで拡張機能をリロードし、再度読み込みをした場合はif (details.reason === "install")内の処理は実行されない。なので途中から初期値を書き込みたい場合はサービスワーカーを開いてデータを直接set()するといい。

background.js
chrome.runtime.onInstalled.addListener(function(details) {
    // インストール時の挙動
    if (details.reason === "install") {

        let defaultSettings = {
            "option1": true,
            "option2": "default_value",
        };

        chrome.storage.local.set(defaultSettings, function() {
            if (chrome.runtime.lastError) {
                console.error(chrome.runtime.lastError);
            } else {
                console.log("Initial settings set.");
            }
        });
    }
});

日付でソートしたい

上で説明したようにchrome.storageではDate()オブジェクトは保存できない。そこで時間を保存するに別の形式で保存する必要がある。JSではchrome.storageに日付情報を保存できるようにtoJson()というメソッドが提供されており、Dateオブジェクトを文字列に変換することができる。もちろん文字列で保存した日付情報はDateオブジェクトに戻すことができる。以下は日付の書式を文字列に直し保存、取得するコードだ。

const currentTime = (new Date()).toJSON();
const items = { 'date': currentTime }; 

await chrome.storage.local.set(items);
const jsonDate = await chrome.storage.local.get('date')
console.log(jsonDate.date) // 2023-10-21T04:19:23.625Z

const date = new Date(jsonDate.date) // Date()の引数に入れるとDateオブジェクトに戻る
console.log(date) // Sat Oct 21 2023 13:19:23 GMT+0900 (日本標準時)

このコードでは時間が入っているオブジェクトのデータを保存し、それをソートするコードだ。chrome.storageにはソートする機能はないので、一度オブジェクトを取り出して、sort()を使って並び替える必要がある。

const data ={ user: [
  { "currentTime": "2019-10-21T14:27:18.753Z", "name": "John" },
  { "currentTime": "2022-10-21T14:27:18.753Z", "name": "Mike" },
  { "currentTime": "2018-10-21T14:27:18.753Z", "name": "Apple" },
  { "currentTime": "2020-10-21T14:27:18.753Z", "name": "Toshi" },
  { "currentTime": "2021-10-21T14:27:18.753Z", "name": "Boy" }
]};

await chrome.storage.local.set(data)
// データを取り出す
const result = await chrome.storage.local.get("user")

// 並べ替え
const users = result.user
users.sort((a, b) => new Date(a.currentTime) - new Date(b.currentTime));
console.log(users) // 並び替えた結果が出てくる

キーの存在を調べて削除する

存在しないキーを削除しても通常通りundefinedが返ってくるだけで、エラーが返ってくるわけではない。しかしきちんと調べてからデータを削除したい場合は、hasOwnProperty()メソッドを使ってキーの存在を確かめてから削除しよう。

terminal
chrome.storage.local.set({animal: ["dog", "cat"]})
terminal
const key = "animal"
chrome.storage.local.get([key], (result) => {
    if(result.hasOwnProperty(key)) {
      chrome.storage.local.remove(key, () => {
        console.log(`[${key}] を削除しました`);
      });
    } else {
      console.log(`[${key}] は存在しないため削除しませんでした`);
    }
});
// [animal] を削除しました

特定のキーの存在を調べる

ほんとにMap/Setが必要か?
https://jsprimer.net/basic/map-and-set/

chrome.storageにはソートする機能がないように、特定のキーの存在を調べる機能も存在しない。そこでMapというEX2015から導入されたオブジェクトにデータを格納するのしてキーを調べる方法がある。Map型はEX2015から導入されたオブジェクトで、chrome.storageで保存されるデータの構造と似たところが多くある。Mapで提供されている便利なメソッドは

一意キーのとバリューのペアで保存されるオブジェクト。chrome.storageで保存できるデータ構造に似ていながら、キーの存在確認や要素数の取得ができるなどchrome.storageにない機能を補完するのに便利。

https://stackoverflow.com/questions/63911640/can-chrome-storage-save-advanced-objects-like-date-or-map

Discussion