# 前端如何緩存大筆資料:IndexedDB 介紹/應用
最近疫情下,開始出現許多網路應用,而最先也是最為知名的就是口罩地圖的應用,利用 google map 與政府提供的 open api 做結合, 讓使用者能夠從地圖上得知各家供應店家的口罩庫存等資訊 ; 然而也因此在大量使用 google map api 的情況下也產生了不少費用的支出。
所以當自身有類似應用時該如何減少支出? 以自身為例,地圖應用上最常用到 google map 的Geolocation API的服務, 將地址轉為座標,然後定位在地圖上。
但如果流量大的話相對應的支出也一定會暴漲! 在沒有資料庫的情況下,單純前端,我們能做的就是盡量透過緩存資料來減少 api request 的次數。
# 該使用哪種方式進行緩存
緩存資料上會建議用以下:
- For the network resources necessary to load your app and file-based content, use the Cache Storage API (part of service workers).
- For other data, use IndexedDB (with a promises wrapper).
那為什麼不用 localStorage,sessionStorage 呢?
可以參考 web.dev 的文章 refer to storage-for-the-web
# IndexedDB 介紹
簡單介紹一下自己使用後感受到的優點:
- key-value 的儲存形式,透過索引功能來高效率搜尋資料
- 同源政策 same-origin policy:只能取用同網域下的資料
- Async API : 提供非同步 api,單線程的應用下取用資料時就不會有 block the main thread 的情況造成使用者體驗不佳
- transaction : 能夠確保大量寫入資料時的完整性,如果有單筆資料寫入失敗會全數 rollback
# 相容性
瀏覽器相容性表格可參閱:When Can I Use IndexedDB
# 儲存限制
單一資料庫項目的容量/大小並沒有任何限制,但是各個 IndexedDB資料庫的容量就有限制,且根據各瀏覽器其限制會不同。
- Chrome allows the browser to use up to 60% of total disk space. You can use the StorageManager API to determine the maximum quota available. Other Chromium-based browsers may allow the browser to use more storage.
- Internet Explorer 10 and later can store up to 250MB and will prompt the user when more than 10MB has been used.
- Firefox allows an origin to use up to 2GB. You can use the StorageManager API to determine how much space is still available.
- Safari (both desktop and mobile) appears to allow up to 1GB. When the limit is reached, Safari will prompt the user, increasing the limit in 200MB increments. I was unable to find any official documentation on this.
refer to storage-for-the-web
# 資料鍵 (Key)
- data type: string, date, float和 array
- 必須是能排序的值(無法處理多國語言字串排序)
- 物件存檔有三種方式產生資料鍵: 資料鍵產生器 (key generator)、資料鍵路徑 (key path) 以及指定值。
資料鍵產生器 (key generator): 用產生器自動產生資料鍵
資料鍵路徑 (key path):空字串或是javascript identifier(包含用 "." 分隔符號的名稱)且路徑不能有空白 (實測過中文會被轉成空字串)
# 實作
稍微看完以上介紹後,接下來實作基本緩存資料的方式。
這邊會用將 indexedDB api 用 promise 包裝後的套件:idb做演示
初始化
mkdir folder
cd folder
npm init -y
Install idb
npm i idb
folder structure:
.
└─ src
└─ `index.js`
├── index.html
├── package.json
...
這邊會先用open api 來當作範例使用的資料來源
const getData = () => {
return fetch(
"https://raw.githubusercontent.com/kiang/pharmacies/master/json/points.json"
)
.then((res) => res.json())
.then((json) => json)
.catch((err) => console.error(err));
};
DB 初始化
async function initDB() {
const dbPromise = await openDB("GeoData", 1, {
upgrade(db) {
// Create a store of objects
db.createObjectStore("geo", {
// The 'id' property of the object will be the key.
keyPath: "id",
// If it isn't explicitly set, create a value by auto incrementing.
autoIncrement: true
});
// Create an index on the 'county' property of the objects.
// store.createIndex("county", "county");
}
});
const idbGeo = {
async get(key) {
return (await dbPromise).get("geo", key);
},
async set(key, val) {
return (await dbPromise).put("geo", val, key);
},
async delete(key) {
return (await dbPromise).delete("geo", key);
},
async clear() {
return (await dbPromise).clear("geo");
},
async keys() {
return (await dbPromise).getAllKeys("geo");
}
};
return { dbPromise, idbGeo };
}
接下來主程式的部分,實作簡單的緩存範例
(async function () {
// ...
const { dbPromise, idbGeo } = await initDB();
let keys = await idbGeo.keys(); // 取出key值來確認 db 是否有cache資料了
if (!keys.length) { // 無資料的情況下才進行以下動作
const jsonData = await getData(); // api 取資料回來
await storeData(jsonData.features, dbPromise); // 存進indexedDB
keys = await idbGeo.keys();
}
// ...
})();
實作大筆資料的儲存storeData
// 大量資料的存取時使用transaction確保完整性
async function storeData(data, db) {
const tx = db.transaction("geo", "readwrite"); // 參數的部分單純取資料可用`readonly`
const asyncList = (data) =>
data.map((item) => {
return tx.store.add(item);
});
await Promise.all([...asyncList(data), tx.done]); // 最後一步 call done method 來結束這次的transaction
}
# chrome devtool 查看工具使用
實作完以上程式後,可以透過chrome devtool > application > Storage > IndexedDB 查看究竟資料存進IndexedDB了沒以及儲存後的樣貌
# 錯誤處理(QuotaExceededError)
使用者瀏覽器的內存不足時會丟出 QuotaExceededError (DOMException
) 的錯誤,
務必記得handle error避免使用者體驗不好,並依照各自邏輯進行錯誤處理。
例如範例:
當transaction時出現錯誤會呼叫callback .onabort
// 以上範例加上error handler
const transaction = db.transaction("geo",, 'readwrite');
transaction.onabort = function(event) {
const error = event.target.error; // DOMException
if (error.name == 'QuotaExceededError') {
// Fallback code goes here
}
};
# 瀏覽器資料庫清空
- 使用者要求清空資料庫。許多瀏覽器讓使用者可以清空網站的 cookies、書籤、密碼和 IndexedDB 資料庫。
- 私密瀏覽。Firefox 和 Chrome 等瀏覽器提供私密瀏覽模式,當瀏覽結束後,資料庫會被清空。
- 超出硬碟空間或資料庫空間上限。
- 資料毀損。
- 當未來有不相容於現在的修改。
補充:說明空間已滿時
Web storage is categorized into two buckets, "Best Effort" and "Persistent"
indexedDB 屬於"Best Effort"(非常久性)
當瀏覽器空間不足時會開始清除非持久性資料
也就是eviction policy
每家的eviction policy 也不同:
- Chromium-based browsers: 當瀏覽器空間不足時,會開始從最少使用的data清除直到空間不再超出限制。
- Internet Explorer 10+: 沒有清除機制,但無法再寫入新資料。
- Firefox: 當硬碟空間不足時,會開始從最少使用的data清除直到空間不再超出限制。
- Safari: 以前沒有清除機制, 但現行有實施7日機制(當使用者七日沒有使用safari時,將會清空資料)。
如果是重要資訊:
You can request persistent storage for your site to protect critical user or application data.
# 總結
不再像是過去只會使用localStorage來暫存一些緩存資訊,這次學到IndexedDB來應對未來越來越龐大的緩存需求, 在使用上,需要多多注意的是針對瀏覽器空間限制與多寡的處理上,可以透過StorageManager API 來知道目前瀏覽器的內存資訊,並加以處理;
以及瀏覽器的資料清除機制,確保這些資料不是必要性的緩存,並在每次確認緩存資料來確保是否要重新更新。 最後提到,如果是重要的使用者資訊或是緩存資料,可以透過persistent storage 除非是使用者自行清除,不然是能夠避免瀏覽器的自動清除。