起因是撰寫的油猴腳本使用 localStorage 存資料,到某一天發現資料怎麼存不進去了,排查才發現資料超過 5MB 了,然後就開始找有沒有其他更好的存資料方式,最後發現了瀏覽器資料庫 API——IndexedDB。
什麼是 IndexedDB?
IndexedDB 是一個低階的 API,用於在瀏覽器中儲存大量結構化資料。它讓你可以在使用者端存放遠超 localStorage 的資料量(實際上限依磁碟配額決定),而且所有操作都是非同步的,不會卡住主執行緒。與 localStorage 不同,IndexedDB 可以處理更複雜的資料結構,也支援索引搜尋。
為什麼要用 IndexedDB?
- 儲存大量資料:相較於 localStorage 的 5MB 限制,IndexedDB 的上限由磁碟配額決定,通常是可用空間的數十趴,幾乎不會遇到瓶頸。
- 支援複雜資料結構:可以直接儲存和查詢 JavaScript 物件,不需要手動 JSON 序列化。
- 非同步操作:不會阻塞主執行緒,頁面不會因為資料操作而卡頓。
- 支援交易(Transactions):確保一批操作全部成功或全部回滾,不會出現資料寫到一半的狀況。
IndexedDB 基本概念
在開始寫程式之前,先了解幾個核心概念:
- 資料庫(Database):就像一個大容器,裡面可以存放多個物件儲存區。
- 物件儲存區(Object Store):類似資料表,用來儲存資料。
- 索引(Index):用來加速搜尋特定欄位的值。
- 交易(Transaction):確保一系列操作全部成功或全部失敗。
- 游標(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
| 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); } }; }
|
注意事項
- 處理錯誤:IndexedDB 的操作可能會失敗(磁碟滿、隱私模式限制、權限問題),一定要掛
onerror。
- 交易範圍:把相關操作放在同一個交易中,能確保資料一致性,也能減少 overhead。
- 非同步思維:所有操作都是事件驅動的,實際專案中建議用 Promise 包裝,或直接改用 idb 這類薄包裝函式庫。
- 版本控制:修改資料庫結構(新增/刪除 Object Store、索引)時,一定要調高版本號,否則
onupgradeneeded 不會被觸發。
結語
IndexedDB 的 API 設計比 localStorage 繁瑣不少,但如果是持續增長的資料,早換早省事。一般腳本用 localStorage 其實夠用——我自己的腳本跑了好幾年才撞到 5MB 上限,所以資料量少的話不必想太多;但如果預期資料會一直長,直接從 IndexedDB 起手,比之後再遷移省力。