/
CATEGORY
JavaScript
/
使用 indexedDB Web API 製作預存後端 API 資料的功能

使用 indexedDB Web API 製作預存後端 API 資料的功能

MUKI AI Summary

使用 indexedDB Web API 可以有效地在瀏覽器端預存後端 API 資料,從而減少 API 呼叫次數,提高使用者的讀取速度,並支持離線資料訪問。資料結構設計上,使用 rId 和日期範圍作為索引,以便精確檢索。核心功能包括初始化資料庫、清除資料庫、存入及取得快取資料,以及處理資料限制。這些功能能提升系統效能、降低伺服器負擔,並提供離線支持。

然而,使用 indexedDB 也有一些缺點,如增加管理複雜性、儲存限制和資料一致性問題。為解決這些問題,可能需要實作資料過期機制或使用版本號判斷資料新舊。此外,選擇 idb 套件可以簡化開發過程,提高效率。總結來說,indexedDB 提供了一個靈活的數據存儲解決方案,但需根據專案需求和資料正確性進行謹慎評估和實施。...

我在 2024 年鐵人賽寫了 Web API 的系列文章,其中有一篇介紹 indexedDB API,寫完後將它運用在公司的專案上,會這樣做的原因,是因為我們的系統常常要打大量的 API,所以希望可以透過預存在瀏覽器端,減少呼叫 API 的次數,以及加快使用者的讀取體驗,也能在離線模式下提供這些資料。

核心功能介紹

資料結構

先說明我的資料結構,我會根據 rId 和日期範圍取得資料,因為同個 rId 可能有多個日期,因此以 rId 當作索引,並加上日期範圍以鎖定資料

▼ 實際存到 indexedDB 的資料結構如下,可以看到 rId 相同,但日期範圍 ➡️ value: { start: 開始時間, end: 結束時間 } 不同,所以如果要撈出正確的資料,就必須同時篩選 rId, value.start 以及 value.end

核心功能

以下是開發時的核心功能:

  1. 初始化資料庫:建立資料庫與索引
  2. 清除資料庫:清空資料庫的資料
  3. 存入快取資料:將 API 回傳的資料存到瀏覽器的資料庫
  4. 取得快取資料:用 rId 和日期範圍來判斷是否有預存資料,如果找不到就執行「第 3 點:存入快取資料
  5. 處理資料限制:設定儲存的最大筆數以及覆蓋邏輯

使用 indexedDB API 的優缺點

優點

  1. 提升效能:減少 API 呼叫,縮短網頁載入以及資料讀取的時間
  2. 降低伺服器負擔:承上,減少 API 呼叫可以減輕伺服器端的請求壓力
  3. 支持離線功能:如果使用者處於離線狀態,依然能提供部分的資料
  4. 易擴展:IndexedDB API 和 localStorage 的差別,在於前者提供了高效的數據存儲和索引能力,重點是可以儲存結構化的資料

缺點

  1. 增加管理的複雜性:需要額外實作資料過期功能,避免快取過期數據影響使用者體驗
  2. 儲存限制:資料存儲受限於瀏覽器的空間分配,需考慮資料量過大的情況
  3. 資料一致性:可能因本地快取與伺服器資料不同步,導致資料不一致的問題

乍看之下,使用 indexedDB API 的優點可能大於缺點,但缺點第三項提到的資料一致性卻是重中之重,不管效能多好、速度多快,資料不是最新的就沒有用。因此要使用 indexedDB API 之前,需要謹慎評估伺服器端的資料是否會經常變動?或者 API 回傳的資料是否常常有問題?假設無法確定資料能 100% 正確,就需要額外實作資料過期的功能,或者乾脆不要用 indexedDB API 😳。

使用與否,建議以專案與資料的正確性來評估。而且 IndexedDB API 不僅僅適合存儲伺服器的資料,我們還可以根據應用場景存儲本地產生的資料,例如:

  1. 使用者偏好設定
  2. 臨時的草稿內容
  3. 分析數據或緩存計算結果

這種彈性存儲方式能有效提升應用的使用體驗,並減少重複計算的成本

使用 idb 套件與原生 IndexedDB API 的差異

我選擇 idb 輕量化的套件幫我達成 IndexedDB API 的各項功能,以下是作者對套件的描述:

This is a tiny (~1.19kB brotli'd) library that mostly mirrors the IndexedDB API, but with small improvements that make a big difference to usability.

a big difference to usability

語法更簡潔

idb 有提供基於 Promise 的封裝,IndexedDB API 則是使用 callback,初學者一不小心容易陷入 callback hell

開發效率更高

IndexedDB API 需要手動處理資料庫開啟、交易管理... 等細節,idb 則減少了這些繁複的操作

內建錯誤處理機制

idb 可以使用 try...catch 處理錯誤,IndexedDB 則是要使用事件監聽來處理,提高了開發的複雜性

功能封裝

idb 支援常見的資料庫操作,例如 get, set, delete,IndexedDB 則需要自己實作這些額外邏輯

程式碼對照

▼ 實作「插入資料」來比較兩者的不同

import { openDB } from 'idb';

const db = await openDB('exampleDB', 1, {
  upgrade(db) {
    if (!db.objectStoreNames.contains('store')) {
      db.createObjectStore('store', { keyPath: 'id' });
    }
  }
});

await db.put('store', { id: 1, name: 'example' });
const request = indexedDB.open('exampleDB', 1);

request.onupgradeneeded = (event) => {
  const db = event.target.result;
  if (!db.objectStoreNames.contains('store')) {
    db.createObjectStore('store', { keyPath: 'id' });
  }
};

request.onsuccess = (event) => {
  const db = event.target.result;
  const transaction = db.transaction('store', 'readwrite');
  const store = transaction.objectStore('store');

  store.put({ id: 1, name: 'example' });

  transaction.oncomplete = () => {
    console.log('Data added successfully');
  };

  transaction.onerror = (error) => {
    console.error('Transaction failed:', error);
  };
};

透過以上對比與前面的統整資料,可以看到 idb 解決了事件監聽和交易管理等處理,因此有快速開發的需求推薦使用 idb;而對底層有控制需求,或想深入了解 indexedDB 的實作原理,可以使用原生的 API,也可以看看我之前寫的文章 ⬇️ XXD


洋洋灑灑寫了一堆前言,終於要開始分享前面提到的六個核心功能了!這六個核心功能是根據我實際的專案需求所開發的,也許不符合每個人的需求,但還是希望透過這些分享,可以提供參考與幫助

初始化資料庫

▼ 首先建立一個資料庫,並設定一個存放資料的 Object Store (類似資料表)

import { openDB } from 'idb';

const initDatabase = async () => {
  try {
    // 資料庫名稱為 exampleDB,版本為 1
    await openDB('exampleDB', 1, {
      upgrade: (db) => {
        if (!db.objectStoreNames.contains('storeData')) {
          // Object Store 的名稱為 storeData
          const store = db.createObjectStore('storeData', {
            // 設定主鍵 id,並自動遞增
            keyPath: 'id',
            autoIncrement: true
          });
          store.createIndex('id', 'id', { unique: true });
          // 建立 rId 索引,未來可以快速搜尋
          store.createIndex('rId', 'rId', { unique: false });
        }
      }
    });
  } catch (error) {
    console.error('初始化或清除資料庫失敗:', error);
  }
};

▼ 建立好的資料庫和 store 如下

存入快取資料

▼ 我們透過 API 正常呼叫伺服器的資料,只是在取得資料的同時,也要存一份到 exampleDB

const addDataToDB = async (DATA) => {
  const db = await openDB('exampleDB', 1);
  const tx = db.transaction('storeData', 'readwrite');
  const store = tx.objectStore('storeData');

  try {
    const { rId, data, startTime, endTime } = DATA;
    const existingData = await store.index('rId').getAll(rId);
    // 檢查是否已有相同的 rId 和日期範圍的資料,避免重複新增。
    const sameData = existingData.find(item =>
      item.startTime === startTime
      item.endTime === endTime
    );

    // 如果找不到相同的資料,就新增一筆至 store
    if (!sameData) {
      await store.add(DATA);
    }
    await tx.done;
  } catch (error) {
    console.error('新增資料失敗:', error);
  }
};

取得快取資料

▼ 取資料的步驟變成在呼叫 API 之前,要先從資料庫 exampleDB 索引是否有符合的資料,沒有才會打 API

const getMatchSensorData = async (rId, startTime, endTime) => {
  const db = await openDB('exampleDB', 1);
  const tx = db.transaction('storeData', 'readonly');
  const store = tx.objectStore('storeData');

  // 使用 rId 取得資料
  const dbData = await store.index('rId').getAll(rId);
  if (!dbData) return undefined;

  // 再用日期範圍取得符合的資料
  const matchData = dbData.find(item =>
    item.startTime === startTime &&
    item.endTime === endTime
  );
  // 返回資料內容或 undefined
  return matchData ? matchData.data : undefined;
};

清除資料庫

在介紹使用 indexedDB 的優缺點時,有提到重中之重的「資料一致性」,如果今天伺服器資料有做修改,就會產生不一致的問題,如果有這樣的風險,就要評估實作「資料過期的功能」

▼ 最暴力的方式,就是提供一個「重新整理資料」的按鈕讓使用者主動點擊,清除所有預存資料後,重新呼叫 API 取得最新的資料

export const clearDatabase = async () => {
  const db = await openDB('exampleDB', 1)
  // clear 方法會清空 storeData Object Store 中的所有記錄
  await db.clear('storeData')
  await initDatabase()
  console.log('資料庫已清空')
}

除了暴力解之外,也可以在資料裡加入標記作為更新資料的策略:

TTL (Time-to-Live) 過期機制

可以在每筆資料加入 timestamp 記錄資料的新增或更新時間。每次讀取資料時,利用 timestamp 檢查資料是否已過期,若過期則重新呼叫 API 更新資料。

用版本號判斷

如果伺服器資料有提供版本號或校驗碼,就可以用這個方式進行比對,如果版號不對就重新拉取資料

處理資料限制

雖然 indexedDB 很好用,但實際開發時,必須要考慮資料儲存的上限,避免超過瀏覽器分配的空間限制。

我原本的設計方式是每隔一段時間(例如 7 天) 就將使用者的資料清空,但開會跟老闆討論後,改成使用最大資料筆數來做設計,也就是存在資料庫的筆數固定為 N 筆,超過就從最舊的資料開始覆蓋。

// 儲存的最大筆數,超過此限制時,會覆蓋舊的資料
const AMOUNT = 2000;
// 追蹤覆蓋資料的紀錄位置
let SAVE_INDEX = 1;

const addSensorDataToDB = async (storeData) => {
  const db = await openDB('exampleDB', 1);
  const tx = db.transaction('storeData', 'readwrite');
  const store = tx.objectStore('storeData');
  const totalCount = await store.count();

  if (totalCount >= AMOUNT) {
    await store.put({ ...storeData, id: SAVE_INDEX });
    SAVE_INDEX++;
  } else {
    await store.add(storeData);
  }
  await tx.done;
};

SAVE_INDEX

SAVE_INDEX 是用來追蹤覆蓋舊資料的記錄位置。

當資料總數達到 AMOUNT(2000) 時,新的資料將使用 SAVE_INDEX 的值作為主鍵來覆蓋最舊的資料,並自動遞增以確保覆蓋循環正常運行。

小結

如果將鐵人賽的文章也加進來,這是 indexedDB API 的第三篇文章了,我用了很大的篇幅描寫 indexedDB API,可見我對他的偏愛 XXD,因為我覺得可以在瀏覽器存取結構化的資料,是利於開發使用的。這麼好用的東西,也應該要多多推廣才是。

也希望這篇文章,可以幫助各位在實際專案開發中,更有效的管理資料。以上有任何問題,都歡迎討論交流

歡迎給我點鼓勵,讓我知道你來過 :)

1
MUKI says:

如果文章有幫助到你,歡迎分享給更多人知道。文章內容皆為 MUKI 本人的原創文章,我會經常更新文章以及修正錯誤內容,因此轉載時建議保留原出處,避免出現版本不一致或已過時的資訊。

本文地址:https://muki.tw/indexeddb-web-api-store-database/ 已複製

Subscribe
Notify of
guest

0 則留言
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Copyright © since 2008 MUKI space* / omegaSS theme All Rights Reserved.