IndexedDB簡介
MDN官網是這樣解釋Indexed DB的:
IndexedDB 是一種底層 API,用於在客戶端儲存大量的結構化資料(也包括檔案/二進位制大型物件(blobs))。該 API 使用索引實現對資料的高效能搜尋。雖然 Web Storage 在儲存較少量的資料很有用,但對於儲存更大量的結構化資料來說力不從心。而 IndexedDB 提供了這種場景的解決方案。
通俗地說,IndexedDB 就是瀏覽器提供的本地資料庫,它可以被網頁尾本建立和操作。IndexedDB 允許儲存大量資料,提供查詢介面,還能建立索引。這些都是 LocalStorage 所不具備的。就資料庫型別而言,IndexedDB 不屬於關係型資料庫(不支援 SQL 查詢語句),更接近 NoSQL 資料庫。
客戶端各儲存方式對比:
IndexedDB使用場景
所有的場景都基於客戶端需要儲存大量資料的前提下:
-
資料視覺化等介面,大量資料,每次請求會消耗很大效能。
-
即時聊天工具,大量訊息需要存在本地。
-
其它儲存方式容量不滿足時,不得已使用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 物件。這個物件透過三種事件error
、success
、upgradeneeded
,處理開啟資料庫的操作結果。
(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