IndexedDBとは?Chrome拡張機能でコメント履歴を保存する方法
前回の記事「chrome.storageとは?Chrome拡張機能で設定を保存する方法」では、popupのON/OFF設定を chrome.storage.local へ保存する方法を紹介しました。
設定値が数個だけなら、chrome.storage.local は扱いやすい保存先です。
しかし、コメント履歴、操作ログ、取得した一覧データのように、時間とともに件数が増えるデータは、設定値と同じ場所へ詰め込まない方が管理しやすくなります。
こうした履歴データをブラウザ内へ保存する候補が、IndexedDBです。
この記事では、Chrome拡張機能の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 を付けます。
今回の目的は、次の流れを理解することです。
indexedDB.open()でデータベースを開く- 初回だけobject storeを作る
readwritetransactionでコメントを追加するreadonlytransactionで履歴を読み込むgetAll()で取得した履歴をpopupへ表示する- 入力エラーや保存失敗を利用者へ表示する
検索、削除ボタン、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-comment-history-extension/
├─ manifest.json
├─ popup.html
├─ popup.css
└─ popup.js役割は次の通りです。
| ファイル | 役割 |
|---|---|
manifest.json | popup画面を設定する |
popup.html | 入力欄、保存ボタン、履歴一覧を表示する |
popup.css | popupの見た目を整える |
popup.js | IndexedDBを開き、保存と一覧表示を行う |
今回のサンプルでは、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_permissions や content_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: truekeyPath は、レコードを識別するキー名です。
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、検索条件などが候補になります。

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で拡張機能を読み込みます。
手順は次の通りです。
- Chromeで
chrome://extensionsを開く - 右上のデベロッパーモードをオンにする
- 「パッケージ化されていない拡張機能を読み込む」を押す
indexeddb-comment-history-extensionフォルダを選ぶ- 拡張機能アイコンを押してpopupを開く
- 投稿者名とコメントを入力する
- 「履歴へ追加」を押す
- 「コメント履歴を保存しました」と表示されることを確認する
- 保存済み履歴にコメントが表示されることを確認する
- popupを閉じる
- もう一度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を自動採番するreadwritetransactionで履歴を追加するreadonlytransactionで履歴を読み込む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