IndexedDB

代码里的诗發表於2024-11-28

IndexedDB簡介

MDN官網是這樣解釋Indexed DB的:

IndexedDB 是一種底層 API,用於在客戶端儲存大量的結構化資料(也包括檔案/二進位制大型物件(blobs))。該 API 使用索引實現對資料的高效能搜尋。雖然 Web Storage 在儲存較少量的資料很有用,但對於儲存更大量的結構化資料來說力不從心。而 IndexedDB 提供了這種場景的解決方案。

通俗地說,IndexedDB 就是瀏覽器提供的本地資料庫,它可以被網頁尾本建立和操作。IndexedDB 允許儲存大量資料,提供查詢介面,還能建立索引。這些都是 LocalStorage 所不具備的。就資料庫型別而言,IndexedDB 不屬於關係型資料庫(不支援 SQL 查詢語句),更接近 NoSQL 資料庫。

客戶端各儲存方式對比:

IndexedDB使用場景

所有的場景都基於客戶端需要儲存大量資料的前提下:

  1. 資料視覺化等介面,大量資料,每次請求會消耗很大效能。

  2. 即時聊天工具,大量訊息需要存在本地。

  3. 其它儲存方式容量不滿足時,不得已使用IndexedDB。

IndexedDB特點

(1) 非關係型資料庫(NoSql)

我們都知道MySQL等資料庫都是關係型資料庫,它們的主要特點就是資料都以一張二維表的形式儲存,而Indexed DB是非關係型資料庫,主要以鍵值對的形式儲存資料。

(2)持久化儲存

cookie、localStorage、sessionStorage等方式儲存的資料當我們清楚瀏覽器快取後,這些資料都會被清除掉的,而使用IndexedDB儲存的資料則不會,除非手動刪除該資料庫。

(3)非同步操作

IndexedDB操作時不會鎖死瀏覽器,使用者依然可以進行其他的操作,這與localstorage形成鮮明的對比,後者是同步的。

(4)支援事務

IndexedDB支援事務(transaction),這意味著一系列的操作步驟之中,只要有一步失敗了,整個事務都會取消,資料庫回滾的事務發生之前的狀態,這和MySQL等資料庫的事務類似。

(6)同源策略

IndexedDB同樣存在同源限制,每個資料庫對應建立它的域名。網頁只能訪問自身域名下的資料庫,而不能訪問跨域的資料庫。

(7)儲存容量大

這也是IndexedDB最顯著的特點之一了,這也是不用localStorage等儲存方式的最好理由。

IndexedDB重要概念講解

倉庫objectStore

IndexedDB沒有表的概念,它只有倉庫store的概念,大家可以把倉庫理解為表即可,即一個store是一張表。

索引index

在關係型資料庫當中也有索引的概念,我們可以給對應的表欄位新增索引,以便加快查詢速率。

遊標cursor

遊標是IndexedDB資料庫新的概念,大家可以把遊標想象為一個指標,比如我們要查詢滿足某一條件的所有資料時,就需要用到遊標,我們讓遊標一行一行的往下走,遊標走到的地方便會返回這一行資料,此時我們便可對此行資料進行判斷,是否滿足條件。

【注意】:IndexedDB查詢不像MySQL等資料庫方便,它只能透過主鍵、索引、遊標方式查詢資料。

事務

資料記錄的讀寫和刪改,都要透過事務完成。即對資料庫進行操作時,只要失敗了,都會回滾到最初始的狀態,確保資料的一致性。

IndexedDB實操

IndexedDB所有針對倉庫的操作都是基於事務的。

新建/開啟資料庫

使用 IndexedDB 的第一步是開啟資料庫,使用indexedDB.open()方法。

var request = window.indexedDB.open(databaseName, version);

這個方法接受兩個引數,第一個引數是字串,表示資料庫的名字。如果指定的資料庫不存在,就會新建資料庫。第二個引數是整數,表示資料庫的版本。如果省略,開啟已有資料庫時,預設為當前版本;新建資料庫時,預設為1。

indexedDB.open()方法返回一個 IDBRequest 物件。這個物件透過三種事件errorsuccessupgradeneeded,處理開啟資料庫的操作結果。

(1)error 事件

error事件表示開啟資料庫失敗。

request.onerror = (event) => {
  console.log('資料庫開啟報錯');
};

(2)success 事件

success事件表示成功開啟資料庫。

let db;
request.onsuccess = (event) => {
  db = request.result;
  console.log('資料庫開啟成功');
};

這時,透過request物件的result屬性拿到資料庫物件。

(3)upgradeneeded 事件

當資料庫版本有變化的時候會執行該函式,比如我們想建立新的儲存庫(表),就可以在該函式里面操作,更新資料庫版本即可。

let db;
request.onupgradeneeded = (event) => {
  db = event.target.result;
}

這時透過事件物件的target.result屬性,拿到資料庫例項。

新建資料庫與開啟資料庫是同一個操作。如果指定的資料庫不存在,就會新建。不同之處在於,後續的操作主要在upgradeneeded事件的監聽函式里面完成,因為這時版本從無到有,所以會觸發這個事件。

通常,新建資料庫以後,第一件事是新建物件倉庫(即新建表)。

request.onupgradeneeded = function (event: any) {
  // 資料庫建立或升級的時候會觸發
  console.log("onupgradeneeded");
  db = event.target.result; // 資料庫物件
  var objectStore;
  // 新建物件倉庫(即新建表)
  objectStore = db.createObjectStore("users", {
    keyPath: "uid", // 主鍵
    // autoIncrement: true // 如果資料記錄裡面沒有合適作為主鍵的屬性,那麼可以讓 IndexedDB 自動生成主鍵
  });
};

主鍵(key)是預設建立索引的屬性。

透過IDBObject.createIndex()新建索引。

IDBObject.createIndex()的三個引數分別為索引名稱、索引所在的屬性、配置物件(說明該屬性是否包含重複的值)。

request.onupgradeneeded = function (event: any) {
  // 資料庫建立或升級的時候會觸發
  console.log("onupgradeneeded");
  db = event.target.result; // 資料庫物件
  var objectStore;
  // 新建物件倉庫(即新建表)
  objectStore = db.createObjectStore("users", {
    keyPath: "uid", // 主鍵
    // autoIncrement: true // 如果資料記錄裡面沒有合適作為主鍵的屬性,那麼可以讓 IndexedDB 自動生成主鍵
  });
  // 建立索引,在後面查詢資料的時候可以根據索引查
  objectStore.createIndex("name", "name", { unique: true });
  objectStore.createIndex("name", "name", { unique: false });
  objectStore.createIndex("age", "age", { unique: false });
};

新增資料

新增資料指的是向物件倉庫寫入資料記錄。這需要透過事務完成。

/**
 * 新增資料
 * @param {object} db 資料庫例項
 * @param {string} storeName 倉庫名稱
 * @param {string} data 資料
 */
  addData(db: any, storeName: string, data: any) {
    return new Promise<void>((resolve, reject) => {
      const request = db
        .transaction([storeName], "readwrite") // 事務物件 指定表格名稱和操作模式("只讀"或"讀寫")
        .objectStore(storeName) // 倉庫物件
        .add(data);
      request.onsuccess = function () {
        console.log("資料寫入成功");
        resolve()
      };
      request.onerror = function () {
        console.log("資料寫入失敗");
        reject()
      };
    })
  }

上面程式碼中,寫入資料需要新建一個事務。新建時必須指定表格名稱和操作模式("只讀"或"讀寫")。新建事務以後,透過IDBTransaction.objectStore(name)方法,拿到 IDBObjectStore 物件,再透過表格物件的add()方法,向表格寫入一條記錄。

add()接收三個引數,分別如下:

  • db:在建立或連線資料庫時,返回的db例項,需要那個時候儲存下來。

  • storeName:倉庫名稱(或者表名),在建立或連線資料庫時我們就已經建立好了倉庫。

  • data:需要插入的資料,通常是一個物件。

透過主鍵讀取資料

讀取資料也是透過事務完成。

/**
 * 透過主鍵讀取資料
 * @param {object} db 資料庫例項
 * @param {string} storeName 倉庫名稱
 * @param {string} key 主鍵值
 */
getDataByKey(db: any, storeName: string, key: number) {
    return new Promise((resolve, reject) => {
      const transaction = db.transaction([storeName]); // 事務
      const objectStore = transaction.objectStore(storeName); // 倉庫物件
      const request = objectStore.get(key); // 透過主鍵獲取資料
      request.onerror = () => {
        console.log("事務失敗");
        reject();
      };
      request.onsuccess = () => {
        console.log("主鍵查詢結果: ", request.result);
        resolve(request.result);
      };
    });
  }

上面程式碼中,objectStore.get()方法用於讀取資料,引數是主鍵的值。

透過遊標查詢資料

/**
 * 透過遊標讀取資料
 * @param {object} db 資料庫例項
 * @param {string} storeName 倉庫名稱
 */
cursorGetData(db: any, storeName: string) {
    return new Promise((resolve, reject) => {
      const list:any[] = [];
      const store = db
        .transaction(storeName, "readwrite") // 事務
        .objectStore(storeName); // 倉庫物件
      const request = store.openCursor(); // 指標物件
      // 遊標開啟成功,逐行讀資料
      request.onsuccess = (e: any) => {
        const cursor = e.target.result;
        if (cursor) {
          // 必須要檢查
          list.push(cursor.value);
          cursor.continue(); // 遍歷了儲存物件中的所有內容
        } else {
          console.log("遊標讀取的資料:", list);
          resolve(list);
        }
      };
      request.onerror = () => {
        console.log("遊標讀取失敗");
        reject();
      }
    })
  }

上面函式開啟了一個遊標,然後逐行讀取資料,存入陣列,最終得到整個倉庫的所有資料。

透過索引查詢資料

索引的意義在於,可以讓你搜尋任意欄位,也就是說從任意欄位拿到資料記錄。如果不建立索引,預設只能搜尋主鍵(即從主鍵取值)。

/**
 * 透過索引讀取資料
 * @param {object} db 資料庫例項
 * @param {string} storeName 倉庫名稱
 * @param {string} indexName 索引名稱
 * @param {string} indexValue 索引值
 */
getDataByIndex(db: IDBDatabase, storeName: string, indexName: string, indexValue: number) {
    return new Promise((resolve, reject) => {
      const store = db.transaction(storeName, "readonly").objectStore(storeName);
      const request = store.index(indexName).get(indexValue);
      request.onerror = function () {
        console.log("事務失敗");
        reject();
      };
      request.onsuccess = function (e) {
        const result = (e.target as IDBOpenDBRequest).result;
        console.log("索引查詢結果:", result);
        resolve(result);
      };
    })
 }

索引名稱即我們建立倉庫的時候建立的索引名稱,也就是鍵值對中的鍵,最終會查詢出所有滿足我們傳入函式索引值的資料。

單獨透過索引或者遊標查詢出的資料都是部分或者所有資料,如果我們想要查詢出索引中滿足某些條件的所有資料,那麼單獨使用索引或遊標是無法實現的。當然,你也可以查詢出所有資料之後在迴圈陣列篩選出合適的資料,但是這不是最好的實現方式,最好的方式當然是將索引和遊標結合起來。

值得注意的是使用了IDBKeyRange.only()API,該API代表只能當兩個值相等時,具體API解釋可參考MDN官網。

/**
 * 透過索引和遊標查詢記錄
 * @param {object} db 資料庫例項
 * @param {string} storeName 倉庫名稱
 * @param {string} indexName 索引名稱
 * @param {string} indexValue 索引值
 */
cursorGetDataByIndex(db: IDBDatabase, storeName: string, indexName: string, indexValue: number) {
    return new Promise((resolve, reject) => {
      const list: any = [];
      const store = db.transaction(storeName, "readwrite").objectStore(storeName); // 倉庫物件
      const request = store
        .index(indexName) // 索引物件
        .openCursor(IDBKeyRange.only(indexValue)); // 指標物件
      request.onsuccess = function (e) {
        const cursor: any = (e.target as IDBOpenDBRequest).result;
        if (cursor) {
          list.push(cursor.value);
          cursor.continue();
        } else {
          console.log("遊標索引查詢結果:", list);
          resolve(list);
        }
      };
      request.onerror = function (e) {
        reject();
      };
    })
  }

更新資料

IndexedDB更新資料較為簡單,直接使用put方法,值得注意的是如果資料庫中沒有該條資料,則會預設增加該條資料,否則更新。

/**
 * 更新資料
 * @param {object} db 資料庫例項
 * @param {string} storeName 倉庫名稱
 * @param {object} data 資料
 */
updateDB(db: IDBDatabase, storeName: string, data: any): Promise<void> {
    return new Promise((resolve, reject) => {
      const request = db
        .transaction([storeName], "readwrite") // 事務物件
        .objectStore(storeName) // 倉庫物件
        .put(data);
    
      request.onsuccess = function () {
        console.log("資料更新成功");
        resolve();
      };
    
      request.onerror = function () {
        console.log("資料更新失敗");
        reject();
      };
    })
  }

刪除資料

IDBObjectStore.delete()方法用於刪除記錄。

 /**
 * 透過主鍵刪除資料
 * @param {object} db 資料庫例項
 * @param {string} storeName 倉庫名稱
 * @param {object} id 主鍵值
 */
 deleteDB(db: IDBDatabase, storeName: string, id: number) {
    return new Promise(() => {
      const request = db
        .transaction([storeName], "readwrite")
        .objectStore(storeName)
        .delete(id); 
      request.onsuccess = function () {
        console.log("資料刪除成功");
      };
      request.onerror = function () {
        console.log("資料刪除失敗");
      };
    })
  }

該種刪除只能刪除一條資料,必須傳入主鍵。

有時候我們拿不到主鍵值,只能只能透過索引值來刪除,透過這種方式,我們可以刪除一條資料(索引值唯一)或者所有滿足條件的資料(索引值不唯一)。

/**  
* 透過索引和遊標刪除指定的資料  
* @param {object} db 資料庫例項  
* @param {string} storeName 倉庫名稱  
* @param {string} indexName 索引名  
* @param {object} indexValue 索引值  
*/
cursorDelete(db: IDBDatabase, storeName: string, indexName: string, indexValue: number | string): Promise<void> {
    return new Promise((resolve, reject) => {
      const store = db.transaction(storeName, "readwrite").objectStore(storeName);
      const request = store
        .index(indexName) // 索引物件
        .openCursor(IDBKeyRange.only(indexValue)); // 指標物件
      request.onsuccess = function (e) {
        const cursor: any  = (e.target as IDBOpenDBRequest).result;
        let deleteRequest;
        if (cursor) {
          deleteRequest = cursor.delete(); // 請求刪除當前項
          deleteRequest.onerror = function () {
            console.log("遊標刪除該記錄失敗");
            reject();
          };
          deleteRequest.onsuccess = function () {
            console.log("遊標刪除該記錄成功");
            resolve();
          };
          cursor.continue();
        }
      };
      request.onerror = function (e) {
        console.log(e);
        reject();
      };
    })
  }

關閉資料庫

當我們資料庫操作完畢後,建議關閉它,節約資源。

closeDB(db: IDBDatabase) {
  db.close();
  console.log("資料庫已關閉");
}

刪除資料庫

最後我們需要刪庫跑路,刪除操作也很簡單。

deleteDBAll(dbName: string): Promise<void> {
    return new Promise((resolve, reject) => {
      const deleteRequest = window.indexedDB.deleteDatabase(dbName);
      deleteRequest.onerror = function (event) {
        console.log("刪除失敗");
        reject();
      };
      deleteRequest.onsuccess = function (event) {
        console.log("刪除成功");
        resolve();
      };
    })
  }

參考文件

https://developer.mozilla.org/zh-CN/docs/Web/API/IndexedDB_API

相關文章