IndexedDBとは?Chrome拡張機能でコメント履歴を保存する方法

公開日: 

前回の記事「chrome.storageとは?Chrome拡張機能で設定を保存する方法」では、popupのON/OFF設定を chrome.storage.local へ保存する方法を紹介しました。

設定値が数個だけなら、chrome.storage.local は扱いやすい保存先です。

しかし、コメント履歴、操作ログ、取得した一覧データのように、時間とともに件数が増えるデータは、設定値と同じ場所へ詰め込まない方が管理しやすくなります。

こうした履歴データをブラウザ内へ保存する候補が、IndexedDBです。

この記事では、Chrome拡張機能のpopupから投稿者名とコメントを入力し、記録日時と一緒にIndexedDBへ保存します。保存済みのコメント履歴を一覧表示できるところまで作ります。

popupからコメント履歴をIndexedDBへ保存して一覧表示する図解
popupからコメント履歴をIndexedDBへ保存して一覧表示する図解

IndexedDBとは

IndexedDBは、ブラウザ内へ構造化されたデータを保存するためのAPIです。

JavaScriptのオブジェクトに近い形でデータを保存でき、後から読み込み、更新、削除、検索を行えます。

Chrome拡張機能でもIndexedDBを利用できます。popup、拡張機能のページ、background service workerなど、必要な処理からブラウザ内のデータベースへアクセスできます。

IndexedDBの処理は非同期です。データベースを開く処理、データを保存する処理、一覧を読み込む処理は、完了や失敗を待ってから次の画面更新を行います。

chrome.storage.localとIndexedDBを使い分ける

chrome.storage.local とIndexedDBは、どちらもブラウザ内へデータを保存できます。

ただし、向いている用途が異なります。

保存方法向いている用途データ例
chrome.storage.local少量の設定値ON/OFF設定、表示テーマ、表示件数
IndexedDB件数が増える履歴や一覧コメント履歴、操作ログ、取得済みデータ
外部サーバー複数端末共有やユーザー管理が必要なデータアカウント別データ、共同利用データ

たとえば、通知を表示するかどうかは chrome.storage.local に向いています。

一方、配信中のコメントを記録する場合は、時間とともに行数が増えます。後から一覧表示、検索、CSV出力などを追加する可能性もあります。

このようなデータは、1件ずつレコードとして扱えるIndexedDBが候補になります。

今回作るもの

今回は、拡張機能アイコンを押すと、小さなpopupが開くサンプルを作ります。

popupには、投稿者名とコメントの入力欄、保存ボタン、保存済み履歴の一覧を配置します。

保存ボタンを押すと、次のようなデータをIndexedDBへ追加します。

{
  author: "山田",
  text: "配信を見ています",
  createdAt: "2026-05-31T12:34:56.789Z"
}

実際に保存するときは、IndexedDBが自動的に id を付けます。

今回の目的は、次の流れを理解することです。

  1. indexedDB.open() でデータベースを開く
  2. 初回だけobject storeを作る
  3. readwrite transactionでコメントを追加する
  4. readonly transactionで履歴を読み込む
  5. getAll() で取得した履歴をpopupへ表示する
  6. 入力エラーや保存失敗を利用者へ表示する

検索、削除ボタン、CSV出力、外部サーバー同期は扱いません。まずは保存と一覧表示を確実に動かします。

IndexedDBの基本用語

IndexedDBでは、最初にデータの入れ物を作ります。

今回使う主な用語は次の通りです。

用語役割今回の例
databaseデータベース全体comment-history-db
object storeレコードを保存する入れ物comments
record保存する1件分のデータ投稿者名、コメント、記録日時
keyレコードを識別する値自動採番する id
transaction読み書きの処理単位保存時は readwrite、取得時は readonly

一般的な表形式のデータベースに慣れている場合、object storeはテーブルに近い役割と考えると理解しやすくなります。

ただし、IndexedDBはブラウザ向けのAPIです。SQL文を書くのではなく、JavaScriptからobject storeへレコードを追加したり、取得したりします。

IndexedDBのdatabaseとobject storeとrecordの関係図
IndexedDBのdatabaseとobject storeとrecordの関係図

ファイル構成

今回のフォルダ構成は次の通りです。

indexeddb-comment-history-extension/
├─ manifest.json
├─ popup.html
├─ popup.css
└─ popup.js

役割は次の通りです。

ファイル役割
manifest.jsonpopup画面を設定する
popup.html入力欄、保存ボタン、履歴一覧を表示する
popup.csspopupの見た目を整える
popup.jsIndexedDBを開き、保存と一覧表示を行う

今回のサンプルでは、content scriptやbackground service workerは使いません。

popup内だけで完結させると、IndexedDBの基本処理へ集中できます。

manifest.jsonを作る

まず、manifest.json を作ります。

{
  "manifest_version": 3,
  "name": "IndexedDB Comment History",
  "version": "1.0.0",
  "description": "コメント履歴をIndexedDBへ保存するサンプルです。",
  "action": {
    "default_popup": "popup.html"
  }
}

今回のサンプルでは、IndexedDBを使うために storage 権限を追加する必要はありません。

前回の記事では、chrome.storage APIを使うために、次の権限を追加しました。

"permissions": ["storage"]

しかし、IndexedDBはWeb PlatformのストレージAPIです。今回のようにpopupからIndexedDBを使うだけなら、storage 権限は不要です。

Webページへ処理を追加しないため、host_permissionscontent_scripts も不要です。

必要な権限だけを使う構成にすると、拡張機能の役割を説明しやすくなります。

popup.htmlで入力欄と履歴一覧を作る

次に、popup.html を作ります。

<!doctype html>
<html lang="ja">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>コメント履歴</title>
    <link rel="stylesheet" href="popup.css">
  </head>
  <body>
    <main class="popup">
      <h1>コメント履歴</h1>

      <form id="comment-form">
        <label>
          投稿者名
          <input id="author-input" type="text" maxlength="40">
        </label>

        <label>
          コメント
          <textarea id="comment-input" maxlength="200"></textarea>
        </label>

        <button type="submit">履歴へ追加</button>
      </form>

      <p id="status-text" aria-live="polite">履歴を読み込み中です</p>

      <section>
        <h2>保存済み履歴</h2>
        <ul id="history-list"></ul>
      </section>
    </main>

    <script src="popup.js"></script>
  </body>
</html>

入力欄には、分かりやすいIDを付けます。

<input id="author-input" type="text" maxlength="40">
<textarea id="comment-input" maxlength="200"></textarea>

maxlength を付けると、極端に長い入力を防ぎやすくなります。

保存結果やエラーは、状態メッセージへ表示します。

<p id="status-text" aria-live="polite">履歴を読み込み中です</p>

履歴一覧は、JavaScriptから <li> 要素を追加します。

<ul id="history-list"></ul>

popup.cssで見た目を整える

次に、popup.css を作ります。

body {
  margin: 0;
  font-family: system-ui, sans-serif;
  color: #1f2937;
  background: #f8fafc;
}

.popup {
  width: 340px;
  padding: 16px;
}

h1,
h2 {
  margin: 0 0 12px;
}

h1 {
  font-size: 18px;
}

h2 {
  font-size: 16px;
}

form,
label {
  display: grid;
  gap: 8px;
}

label {
  font-size: 13px;
}

input,
textarea,
button {
  box-sizing: border-box;
  width: 100%;
  font: inherit;
}

input,
textarea {
  padding: 8px;
  border: 1px solid #cbd5e1;
  border-radius: 6px;
}

textarea {
  min-height: 72px;
  resize: vertical;
}

button {
  padding: 9px;
  border: 0;
  border-radius: 6px;
  color: #ffffff;
  background: #2563eb;
  cursor: pointer;
}

#status-text {
  min-height: 1.5em;
  margin: 12px 0;
  color: #475569;
  font-size: 13px;
}

#history-list {
  display: grid;
  gap: 8px;
  max-height: 240px;
  margin: 0;
  padding: 0;
  overflow-y: auto;
  list-style: none;
}

#history-list li {
  padding: 10px;
  border: 1px solid #dbeafe;
  border-radius: 6px;
  background: #ffffff;
}

#history-list time {
  display: block;
  margin-top: 4px;
  color: #64748b;
  font-size: 12px;
}

履歴が増えてもpopup全体が伸び続けないように、一覧へ最大の高さを設定しています。

max-height: 240px;
overflow-y: auto;

保存処理そのものには影響しませんが、件数が増えるデータを表示するときは、画面の大きさも考慮します。

popup.jsでIndexedDBを開く

次に、popup.js を作ります。

最初に、データベース名、バージョン、object store名を定数にします。

const DB_NAME = "comment-history-db";
const DB_VERSION = 1;
const STORE_NAME = "comments";

次に、データベースを開く関数を作ります。

function openDatabase() {
  return new Promise((resolve, reject) => {
    const request = indexedDB.open(DB_NAME, DB_VERSION);

    request.onupgradeneeded = () => {
      const database = request.result;

      if (!database.objectStoreNames.contains(STORE_NAME)) {
        database.createObjectStore(STORE_NAME, {
          keyPath: "id",
          autoIncrement: true
        });
      }
    };

    request.onsuccess = () => {
      resolve(request.result);
    };

    request.onerror = () => {
      reject(request.error);
    };
  });
}

中心になる行は、次の部分です。

const request = indexedDB.open(DB_NAME, DB_VERSION);

indexedDB.open() でデータベースを開きます。

初回起動時は、まだデータベースが存在しません。このとき、onupgradeneeded が呼ばれます。

request.onupgradeneeded = () => {
  const database = request.result;

  if (!database.objectStoreNames.contains(STORE_NAME)) {
    database.createObjectStore(STORE_NAME, {
      keyPath: "id",
      autoIncrement: true
    });
  }
};

createObjectStore() で、comments という入れ物を作ります。

keyPath: "id",
autoIncrement: true

keyPath は、レコードを識別するキー名です。

autoIncrement: true にすると、コメントを保存するたびに id が自動採番されます。

コメント履歴を保存する

データベースを開けたら、コメント履歴を保存する関数を作ります。

function addComment(database, comment) {
  return new Promise((resolve, reject) => {
    const transaction = database.transaction(STORE_NAME, "readwrite");
    const store = transaction.objectStore(STORE_NAME);

    store.add(comment);

    transaction.oncomplete = () => {
      resolve();
    };

    transaction.onerror = () => {
      reject(transaction.error);
    };

    transaction.onabort = () => {
      reject(transaction.error);
    };
  });
}

保存するときは、readwrite transactionを作ります。

const transaction = database.transaction(STORE_NAME, "readwrite");

transaction.objectStore() で、保存先のobject storeを取得します。

const store = transaction.objectStore(STORE_NAME);

add() で、コメントを1件追加します。

store.add(comment);

処理が完了したら、Promiseを成功にします。

transaction.oncomplete = () => {
  resolve();
};

IndexedDBでは、データを追加する命令を出した直後ではなく、transactionの完了を確認してから保存成功として扱います。

保存済み履歴を読み込む

次に、保存済みのコメント履歴を取得する関数を作ります。

function getComments(database) {
  return new Promise((resolve, reject) => {
    const transaction = database.transaction(STORE_NAME, "readonly");
    const store = transaction.objectStore(STORE_NAME);
    const request = store.getAll();

    request.onsuccess = () => {
      resolve(request.result);
    };

    request.onerror = () => {
      reject(request.error);
    };
  });
}

一覧取得では、データを書き換えないため、readonly transactionを使います。

const transaction = database.transaction(STORE_NAME, "readonly");

getAll() で、object store内のレコードをまとめて取得します。

const request = store.getAll();

今回のような小さなサンプルでは、getAll() が分かりやすい方法です。

実際に大量の履歴を扱う場合は、全件を毎回読み込まない設計も検討します。ページ分割、cursor、検索条件などが候補になります。

IndexedDBを開いてコメント履歴を保存し一覧表示する流れの図解
IndexedDBを開いてコメント履歴を保存し一覧表示する流れの図解

popup.js全体を書く

保存と一覧表示をまとめた popup.js 全体は、次の通りです。

const DB_NAME = "comment-history-db";
const DB_VERSION = 1;
const STORE_NAME = "comments";

const commentForm = document.getElementById("comment-form");
const authorInput = document.getElementById("author-input");
const commentInput = document.getElementById("comment-input");
const statusText = document.getElementById("status-text");
const historyList = document.getElementById("history-list");

function openDatabase() {
  return new Promise((resolve, reject) => {
    const request = indexedDB.open(DB_NAME, DB_VERSION);

    request.onupgradeneeded = () => {
      const database = request.result;

      if (!database.objectStoreNames.contains(STORE_NAME)) {
        database.createObjectStore(STORE_NAME, {
          keyPath: "id",
          autoIncrement: true
        });
      }
    };

    request.onsuccess = () => {
      resolve(request.result);
    };

    request.onerror = () => {
      reject(request.error);
    };
  });
}

function addComment(database, comment) {
  return new Promise((resolve, reject) => {
    const transaction = database.transaction(STORE_NAME, "readwrite");
    const store = transaction.objectStore(STORE_NAME);

    store.add(comment);

    transaction.oncomplete = () => {
      resolve();
    };

    transaction.onerror = () => {
      reject(transaction.error);
    };

    transaction.onabort = () => {
      reject(transaction.error);
    };
  });
}

function getComments(database) {
  return new Promise((resolve, reject) => {
    const transaction = database.transaction(STORE_NAME, "readonly");
    const store = transaction.objectStore(STORE_NAME);
    const request = store.getAll();

    request.onsuccess = () => {
      resolve(request.result);
    };

    request.onerror = () => {
      reject(request.error);
    };
  });
}

function renderComments(comments) {
  historyList.replaceChildren();

  for (const comment of comments.reverse()) {
    const item = document.createElement("li");
    const author = document.createElement("strong");
    const text = document.createElement("p");
    const createdAt = document.createElement("time");

    author.textContent = comment.author;
    text.textContent = comment.text;
    createdAt.textContent = new Date(comment.createdAt).toLocaleString();
    createdAt.dateTime = comment.createdAt;

    item.append(author, text, createdAt);
    historyList.append(item);
  }
}

async function refreshHistory(database) {
  const comments = await getComments(database);
  renderComments(comments);
  statusText.textContent = `${comments.length}件の履歴を表示しています`;
}

async function initialize() {
  const database = await openDatabase();

  await refreshHistory(database);

  commentForm.addEventListener("submit", async (event) => {
    event.preventDefault();

    const author = authorInput.value.trim();
    const text = commentInput.value.trim();

    if (!author || !text) {
      statusText.textContent = "投稿者名とコメントを入力してください";
      return;
    }

    try {
      await addComment(database, {
        author,
        text,
        createdAt: new Date().toISOString()
      });

      commentInput.value = "";
      await refreshHistory(database);
      statusText.textContent = "コメント履歴を保存しました";
    } catch (error) {
      console.error("コメント履歴の保存に失敗しました", error);
      statusText.textContent = "コメント履歴を保存できませんでした";
    }
  });
}

initialize().catch((error) => {
  console.error("コメント履歴の読み込みに失敗しました", error);
  statusText.textContent = "コメント履歴を読み込めませんでした";
});

入力値を検証する

保存ボタンが押されたときは、最初に入力値を確認します。

const author = authorInput.value.trim();
const text = commentInput.value.trim();

if (!author || !text) {
  statusText.textContent = "投稿者名とコメントを入力してください";
  return;
}

trim() で前後の空白を除きます。

投稿者名またはコメントが空なら、保存処理を行いません。

HTMLの maxlength だけに頼らず、JavaScript側でも保存してよいデータか確認します。

将来、content scriptでWebページからコメントを取得する場合も、外部から入る値をそのまま信頼しないことが重要です。

textContentで安全に表示する

履歴をpopupへ表示するときは、innerHTML ではなく textContent を使います。

author.textContent = comment.author;
text.textContent = comment.text;

コメント本文には、HTMLのように見える文字列が含まれる可能性があります。

textContent を使うと、入力された内容をHTMLとして実行せず、文字列として表示できます。

利用者の入力、Webページから取得した文字列、外部APIの応答などを表示するときは、安全な出力方法を選びます。

Chromeで動作確認する

4つのファイルを作ったら、Chromeで拡張機能を読み込みます。

手順は次の通りです。

  1. Chromeで chrome://extensions を開く
  2. 右上のデベロッパーモードをオンにする
  3. 「パッケージ化されていない拡張機能を読み込む」を押す
  4. indexeddb-comment-history-extension フォルダを選ぶ
  5. 拡張機能アイコンを押してpopupを開く
  6. 投稿者名とコメントを入力する
  7. 「履歴へ追加」を押す
  8. 「コメント履歴を保存しました」と表示されることを確認する
  9. 保存済み履歴にコメントが表示されることを確認する
  10. popupを閉じる
  11. もう一度popupを開き、履歴が残っていることを確認する

空の入力も試します。

投稿者名またはコメントを空にして保存ボタンを押し、「投稿者名とコメントを入力してください」と表示されれば、入力値の検証も動いています。

ファイルを変更した場合は、chrome://extensions で再読み込みボタンを押します。

DevToolsで保存データを確認する

popupを開いた状態で右クリックし、「検証」を選ぶと、popup用のDevToolsを開けます。

ApplicationパネルのIndexedDBを開くと、保存したデータを確認できます。

今回のサンプルでは、次の構造が見つかります。

comment-history-db
└─ comments
   ├─ id
   ├─ author
   ├─ text
   └─ createdAt

保存処理が動かない場合は、Consoleのエラーと、IndexedDB内のデータを確認します。

よくあるエラーと注意点

object storeは初回またはバージョン更新時に作る

object storeの作成は、onupgradeneeded の中で行います。

request.onupgradeneeded = () => {
  const database = request.result;
  database.createObjectStore(STORE_NAME, {
    keyPath: "id",
    autoIncrement: true
  });
};

実際のコードでは、すでに存在するか確認してから作成します。

if (!database.objectStoreNames.contains(STORE_NAME)) {
  // object storeを作成する
}

同じ名前のobject storeを何度も作ろうとすると、エラーになります。

transactionのモードを使い分ける

保存、更新、削除を行うときは、readwrite を使います。

database.transaction(STORE_NAME, "readwrite");

読み込みだけなら、readonly を使います。

database.transaction(STORE_NAME, "readonly");

処理内容に合ったモードを選ぶと、コードの意図が分かりやすくなります。

全件取得を過信しない

getAll() は、初心者向けの小さなサンプルに向いています。

ただし、履歴が数千件、数万件と増える場合は、毎回全件を画面へ表示しない方がよい場合があります。

必要に応じて、表示件数の上限、ページ分割、cursor、indexを使った検索を検討します。

削除方法と保持期間を考える

履歴データは、保存するだけでは増え続けます。

実際の拡張機能では、次の項目を決めます。

  • 1件ずつ削除できるか
  • 全件削除できるか
  • 一定期間を過ぎた履歴を削除するか
  • 最大件数を決めるか
  • CSV出力後に削除するか

利用者が履歴を消せる方法を用意すると、保存データを管理しやすくなります。

重要データの唯一の保存先にしない

IndexedDBは、ブラウザ内へ履歴を保存するために便利です。

しかし、利用者が拡張機能を削除した場合や、ブラウザのデータを消去した場合は、保存済みデータも失われる可能性があります。

失うと困るデータを扱う場合は、CSV出力、バックアップ、外部サーバー同期などを検討します。

ローカル保存だけで完結する場合も、利用者へ保存場所と削除方法を説明します。

スキーマを変更するときはバージョンを上げる

今回のデータベースバージョンは、1 です。

const DB_VERSION = 1;

後からobject storeを追加したり、indexを追加したりする場合は、バージョンを上げます。

const DB_VERSION = 2;

新しいバージョンで indexedDB.open() を呼ぶと、onupgradeneeded が実行されます。

データ構造を変更するときは、既存データを壊さずに更新できるかを考えます。

小さな試作では単純な構造から始め、本番向けに広げる段階で移行処理を設計します。

検索やCSV出力へ広げる

今回のサンプルは、コメント履歴を保存して、全件を一覧表示するところまでです。

実際のコメント保存ツールでは、次の機能が候補になります。

機能考え方
キーワード検索text を検索対象にする
投稿者別の絞り込みauthor を条件にする
日時順の表示createdAt を使う
CSV出力保存済み履歴を読み込み、ファイルとして出力する
最大件数の制限古い履歴を削除する
配信単位の管理配信IDなどを追加して履歴を分ける

件数が増えた場合や、条件検索を高速にしたい場合は、indexやcursorを検討します。

最初からすべてを作り込まず、保存、一覧表示、削除、検索、出力の順に機能を分けて追加すると、問題を切り分けやすくなります。

外部送信の有無を説明する

今回のサンプルは、入力されたコメント履歴をブラウザ内のIndexedDBへ保存します。

外部サーバーへデータを送信する処理はありません。

ただし、実際の拡張機能で外部API、分析ツール、クラウド保存などを追加する場合は、何をどこへ送信するかを利用者へ説明します。

Chrome Web Storeで公開する場合も、保存するデータ、外部送信の有無、削除方法を整理しておくことが重要です。

今回のサンプルで分かること

今回の拡張機能では、IndexedDBで履歴データを保存する基本的な流れを確認できました。

  • indexedDB.open() でデータベースを開く
  • onupgradeneeded でobject storeを作る
  • keyPath: "id"autoIncrement: true でIDを自動採番する
  • readwrite transactionで履歴を追加する
  • readonly transactionで履歴を読み込む
  • add() で1件保存する
  • getAll() で小規模な履歴一覧を取得する
  • 空の入力を保存しない
  • textContent で文字列として安全に表示する
  • 保存失敗や読み込み失敗を画面とConsoleへ表示する
  • 増え続ける履歴には削除方法や保持期間が必要になる

参考リンク

まとめ

この記事では、Chrome拡張機能のpopupからコメント履歴をIndexedDBへ保存しました。

少量の設定値には chrome.storage.local、件数が増える履歴データにはIndexedDBという使い分けを考えると、保存処理を整理しやすくなります。

IndexedDBでは、indexedDB.open() でデータベースを開き、onupgradeneeded でobject storeを作ります。

保存時は readwrite transactionと add()、一覧取得時は readonly transactionと getAll() を使いました。

履歴データは、保存できれば終わりではありません。

実際の拡張機能では、削除方法、保持期間、最大件数、バックアップ、外部送信の有無も考えます。

まずはコメント履歴を1件ずつ保存し、popupを開き直しても一覧が残ることを確認します。その後で、検索、削除、CSV出力などを必要に応じて追加します。




Your Message

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください

スポンサードリンク

記事が気に入ったらシェアお願いします

PAGE TOP ↑