Browser Extension で storage を使う

# 目的

Chrome Extension の HelloWorld のような簡単なプログラムで storage からのデータ取得をテストします。

# Chrome Extension のファイル構成

browser-extension-storage/
│-- manifest.jsoon ....... Browser Extensionの設定ファイル
│-- content_scripts.js ... HTMLページで実行するMainプログラム
│-- options.js ..... Storage の変更をするプログラム
│-- options.html ... Storage の変更をするためのUI
│-- constants.js ... Storage に保管する変数処理のプログラム

それぞれのファイル内容の説明は、後で説明をしますがここでは manifest.json の内容を記載します。

```manifest.json
{
  "manifest_version": 3,
  "name": "Hello World Storage Extension",
  "version": "2025.5.25",
  "description": "Hello World Storage Extension.",
   "content_scripts": [
    {
        "matches": ["https://qiita.com/*"],
        "js": ["content_scripts.js"]
    }
  ],
  "web_accessible_resources": [
    {
        "matches": ["https://qiita.com/*"],
        "resources": ["constants.js"]
    }
  ],
  "options_page": "options.html",
  "permissions": ["storage"]
}
```

Main ページを動作させるのに content_scripts を指定し、storage 処理をするためにpermissionが必要になります。

# 即時実行関数式

https://developer.mozilla.org/ja/docs/Glossary/IIFE

storage から取得したデータを使用するのに、取得を待つ(await)ので、即時実行関数で async処理をさせます。

content_scripts.js の以下の部分

```content_scripts.js
var constants = "";
(async () => {
  console.log("Content script initializing...");
  constants = await import(chrome.runtime.getURL("constants.js"));
  await constants.init();
  console.log("Message from constants.getMessage() : " + constants.getMessage());
})();
```

# 動的インポート

上のコードで使用されていますが、content_scripts.js では import が使用できないので、動的importでの処理になっています。constants.jsに役割を分けるために使用しました。manifest.json の "web_accessible_resources" を指定する必要があります。


# オプションページ

Storage への変数保管や変更にBrowser Extensionの options_page を使用します。manifest.json に指定します。

```options.js
/*
 * Local variables 
 */
import * as constants from "./constants.js";   
var message;

/*
 * Code executed when js is loaded.
 * Load timing is defined in the html.
 */
(async () => {
  console.log("Options js initializing...");
  await constants.init();

  message = document.getElementById("message");
  message.value = constants.getMessage();

  message.addEventListener("change", () => {
    constants.setMessageToStorage(message.value);
  });

})();
```

動的import の必要はない事、options.jsの初期化の処理がoptions.htmlの読み込み状況に依存する場合にはHTMLの記述とあわせて調整します。今回は処理するHTMLの記述の後にscriptを書きました。これ以外だと

- scriptでdefer `<script type="module" src="options.js" defer></script>`
- js 側で document.addEventListener("DOMContentLoaded, () => {... 

とする様です。

```options.html
<!DOCTYPE html>
<html>
<head>
  <title>Hello World Storage Extension Options</title>
</head>
<body>
  <h1>Hello World Storage Extension Options</h1>
  <label for="message">Message</label>
  <input type="text" id="message">
  <script type="module" src="options.js"></script>
  <hr />
  <br />
  The changes will be automatically saved.
</body>
</html>
```

# 変数用スクリプト

変数を管理するスクリプトを作成して、管理がいろいろなソースコードに分散しないように考慮します。気をつけた点は

- Setter, Getterを用意する
- Storageからの Getter は asyncにして取得待ちができるようにする

です。以下のようなコードになりました。関数は export しましたが、制限をしてもいいかもしれません。

```constants.js
/*
 * Constants and variables of Chrome extension
 */
const DEFAULT_message="Hello World";
var message=DEFAULT_message;

/*
 * Funtion to initialize variables
 */
export async function init() {
    message = await getMessageFromStorage() || DEFAULT_message;
    console.log("constants.init message : " + message);
}

/*
 * Setter of message. async is not required.
 */
export function setMessageToStorage(msg) {
    message = msg;
    chrome.storage.local.set({ "message" : msg }, () => {
        console.log("constants.setMessageToStorage message : " + msg)
    });
}

export function setMessage(msg) {
    message = msg;
}

/*
 * Getter of message. async is required to gete from the storage. 
 */
export async function getMessageFromStorage() {
    return new Promise((resolve) => {
        chrome.storage.local.get("message", (result) => {
            console.log("constants.getMessageFromStorage message : " + result.message);
            resolve(result.message);
        });
    });
}

export function getMessage() {
    return message;
}
```

# Storage変更の検知

Browser Extensionの再起動をしなくてもStorage変更後の値でどうさするように content_scripts.jsを更新します。以下のような Event Listenerを記載しました。

```content_scripts.js
chrome.storage.onChanged.addListener((changes, areaName) => {
    console.log(`Storage area "${areaName}" changed.`);
    for (let [key, { oldValue, newValue }] of Object.entries(changes)) {
        console.log(`Key "${key}" changed from "${oldValue}" to "${newValue}".`);
    }
    (async () => {
        await constants.init();
        console.log("Message from constants.getMessage() : " + constants.getMessage());
    })();
});
```

検知した値をデバッグ目的で出力し、そのままの値は使用せずに constants の初期化をすることでデータを更新するようにしました。


# サンプルの実行結果

初期化時の BrowserのF12で確認できるコンソール

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/262366/3f78c143-0b48-4b67-9600-7debee8eb6c0.png)

オプションページから更新したときのF12コンソール

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/262366/2d0bb559-627b-47db-960a-98a584a1ec9e.png)

Storage からのデータを取得して、動作できているのが確認できます。


# まとめ

Browser Extensionを構成するファイルとその内容を説明しました。コードを各際には各処理とファイルの連携を考慮するので1つのファイルのみの処理のみを考えることはないです。動作するサンプルをもとに自分のアプリケーションでの処理を考えることになるかと思います。


# 参考
https://developer.mozilla.org/ja/docs/Glossary/IIFE

https://scrapbox.io/dojineko/Chrome%E6%8B%A1%E5%BC%B5%E3%81%AEContentScripts%E3%82%92ES_Modules%E3%81%A8%E3%81%97%E3%81%A6%E5%AE%9F%E8%A1%8C%E3%81%99%E3%82%8B

https://ics.media/entry/16511/