起因是撰寫的油猴腳本使用 localStorage 存資料,到某一天發現資料怎麼存不進去了,排查才發現資料超過 5MB 了,然後就開始找有沒有其他更好的存資料方式,最後發現了瀏覽器資料庫 API——IndexedDB。

什麼是 IndexedDB?

IndexedDB 是一個低階的 API,用於在瀏覽器中儲存大量結構化資料。它讓你可以在使用者端存放遠超 localStorage 的資料量(實際上限依磁碟配額決定),而且所有操作都是非同步的,不會卡住主執行緒。與 localStorage 不同,IndexedDB 可以處理更複雜的資料結構,也支援索引搜尋。

為什麼要用 IndexedDB?

  1. 儲存大量資料:相較於 localStorage 的 5MB 限制,IndexedDB 的上限由磁碟配額決定,通常是可用空間的數十趴,幾乎不會遇到瓶頸。
  2. 支援複雜資料結構:可以直接儲存和查詢 JavaScript 物件,不需要手動 JSON 序列化。
  3. 非同步操作:不會阻塞主執行緒,頁面不會因為資料操作而卡頓。
  4. 支援交易(Transactions):確保一批操作全部成功或全部回滾,不會出現資料寫到一半的狀況。

IndexedDB 基本概念

在開始寫程式之前,先了解幾個核心概念:

  1. 資料庫(Database):就像一個大容器,裡面可以存放多個物件儲存區。
  2. 物件儲存區(Object Store):類似資料表,用來儲存資料。
  3. 索引(Index):用來加速搜尋特定欄位的值。
  4. 交易(Transaction):確保一系列操作全部成功或全部失敗。
  5. 游標(Cursor):用來遍歷資料集合的機制。

實作教學

直接來寫程式。

1. 開啟資料庫連線

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let db;
const dbName = "MyAwesomeDB";

const request = indexedDB.open(dbName, 1);

request.onerror = function(event) {
console.error("哎呀!開啟資料庫時發生錯誤:", event.target.error);
};

request.onsuccess = function(event) {
db = event.target.result;
console.log("耶!成功連線到資料庫");
};

request.onupgradeneeded = function(event) {
db = event.target.result;
const objectStore = db.createObjectStore("users", { keyPath: "id" });
console.log("資料庫結構升級完成");
};

這段程式碼會開啟名為 "MyAwesomeDB" 的資料庫,版本 1。如果資料庫不存在或版本需要升級,就會觸發 onupgradeneeded 事件——這是唯一能建立或修改 Object Store 的地方,在這裡建立了名為 "users" 的物件儲存區。

2. 新增資料

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function addUser(user) {
const transaction = db.transaction(["users"], "readwrite");
const objectStore = transaction.objectStore("users");
const request = objectStore.add(user);

request.onerror = function(event) {
console.error("新增資料失敗:", event.target.error);
};

request.onsuccess = function(event) {
console.log("新增資料成功");
};
}

// 使用範例
addUser({ id: 1, name: "小明", email: "xiaoming@example.com" });

3. 讀取資料

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function getUser(id) {
const transaction = db.transaction(["users"]);
const objectStore = transaction.objectStore("users");
const request = objectStore.get(id);

request.onerror = function(event) {
console.error("讀取資料失敗:", event.target.error);
};

request.onsuccess = function(event) {
if (request.result) {
console.log("找到使用者:", request.result);
} else {
console.log("哎呀!找不到這個使用者");
}
};
}

// 使用範例
getUser(1);

4. 更新資料

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function updateUser(user) {
const transaction = db.transaction(["users"], "readwrite");
const objectStore = transaction.objectStore("users");
const request = objectStore.put(user);

request.onerror = function(event) {
console.error("更新資料失敗:", event.target.error);
};

request.onsuccess = function(event) {
console.log("更新資料成功");
};
}

// 使用範例
updateUser({ id: 1, name: "小明", email: "xiaoming_new@example.com" });

5. 刪除資料

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function deleteUser(id) {
const transaction = db.transaction(["users"], "readwrite");
const objectStore = transaction.objectStore("users");
const request = objectStore.delete(id);

request.onerror = function(event) {
console.error("刪除資料失敗:", event.target.error);
};

request.onsuccess = function(event) {
console.log("刪除資料成功");
};
}

// 使用範例
deleteUser(1);

進階技巧

1. 使用索引加速搜尋

索引必須在 onupgradeneeded 裡建立,和 Object Store 一樣只能在版本升級時定義:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 在 onupgradeneeded 中建立索引
objectStore.createIndex("nameIndex", "name", { unique: false });

// 使用索引搜尋
function getUserByName(name) {
const transaction = db.transaction(["users"]);
const objectStore = transaction.objectStore("users");
const index = objectStore.index("nameIndex");
const request = index.get(name);

request.onsuccess = function(event) {
if (request.result) {
console.log("找到使用者:", request.result);
} else {
console.log("哎呀!找不到這個使用者");
}
};
}

2. 使用游標遍歷資料

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function getAllUsers() {
const users = [];
const transaction = db.transaction(["users"]);
const objectStore = transaction.objectStore("users");
const request = objectStore.openCursor();

request.onsuccess = function(event) {
const cursor = event.target.result;
if (cursor) {
users.push(cursor.value);
cursor.continue();
} else {
console.log("所有使用者:", users);
}
};
}

注意事項

  1. 處理錯誤:IndexedDB 的操作可能會失敗(磁碟滿、隱私模式限制、權限問題),一定要掛 onerror
  2. 交易範圍:把相關操作放在同一個交易中,能確保資料一致性,也能減少 overhead。
  3. 非同步思維:所有操作都是事件驅動的,實際專案中建議用 Promise 包裝,或直接改用 idb 這類薄包裝函式庫。
  4. 版本控制:修改資料庫結構(新增/刪除 Object Store、索引)時,一定要調高版本號,否則 onupgradeneeded 不會被觸發。

結語

IndexedDB 的 API 設計比 localStorage 繁瑣不少,但如果是持續增長的資料,早換早省事。一般腳本用 localStorage 其實夠用——我自己的腳本跑了好幾年才撞到 5MB 上限,所以資料量少的話不必想太多;但如果預期資料會一直長,直接從 IndexedDB 起手,比之後再遷移省力。