indexedDB
IndexedDB 是一種底層 API,用於在客戶端存儲大量的結構化數據,它可以被網頁腳本創建和操作。
IndexedDB 允許儲存大量數據,提供查找接口,還能建立索引,這些都是 LocalStorage 所不具備的。
就數據庫類型而言,IndexedDB 不屬於關系型數據庫(不支持 SQL 查詢語句),更接近 NoSQL 數據庫。
其他的介紹就不搬運了,大家可以自行百度,后面有參考資料。
需求
我想更好的實現文檔驅動的想法,發現需要實現前端存儲的功能,於是打算采用 IndexedDB 來實現前端存儲的功能。但是看了一下其操作方式比較繁瑣,所以打算封裝一下。
官網給了幾個第三方的封裝庫,我也點過去看了看,結果沒看懂。想了想還是自己動手豐衣足食吧。
關於重復制造輪子的想法:
- 首先要有制造輪子能能力。
- 自己造的輪子,操控性更好。
功能設計
按照官網的功能介紹,把功能整理了一下:
如圖:
就是建庫、增刪改查那一套。看到有些第三方的封裝庫,可以實現支持sql語句方式的查詢,真的很厲害。目前沒有這種需求,好吧,能力有限實現不了。
總之,先滿足自己的需求,以后在慢慢改進。
代碼實現
還是簡單粗暴,直接上代碼吧,基礎知識的介紹,網上有很多了,可以看后面的參考資料。官網介紹的也比較詳細,還有中文版的。
配置文件
nf-indexedDB.config
const config = {
dbName: 'dbTest',
ver: 1,
debug: true,
objectStores: [ // 建庫依據
{
objectStoreName: 'blog',
index: [ // 索引 , unique 是否可以重復
{ name: 'groupId', unique: false }
]
}
],
objects: { // 初始化數據
blog: [
{
id: 1,
groupId: 1,
title: '這是一個博客',
addTime: '2020-10-15',
introduction: '這是博客簡介',
concent: '這是博客的詳細內容<br>第二行',
viewCount: 1,
agreeCount: 1
},
{
id: 2,
groupId: 2,
title: '這是兩個博客',
addTime: '2020-10-15',
introduction: '這是博客簡介',
concent: '這是博客的詳細內容<br>第二行',
viewCount: 10,
agreeCount: 10
}
]
}
}
export default config
-
dbName
指定數據庫名稱 -
ver
指定數據庫版本 -
debug
指定是否要打印狀態 -
objectStores
對象倉庫的描述,庫名、索引等。 -
objects
初始化數據,如果建庫后需要添加默認數據的話,可以在這里設置。
這里的設置不太完善,有些小問題現在還沒想好解決方法。以后想好了再改。
內部成員
/**
* IndexedDB 數據庫對象
* 判斷瀏覽器是否支持
* */
const myIndexedDB = window.indexedDB || window.webkitIndexedDB || window.mozIndexedDB || window.msIndexedDB
if (!myIndexedDB) {
console.log('你的瀏覽器不支持IndexedDB')
}
let _db // 內部保存的 indexed 數據庫 的實例
/**
* 把vue的ref、reactive轉換成原始對象
*/
const _vueToObject = (vueObject) => {
let _object = vueObject
// 針對Vue3做的類型判斷
if (Vue.isRef(_object)) {
// 如果是 vue 的 ref 類型,替換成 ref.value
_object = _object.value
}
if (Vue.isReactive(_object)) {
// 如果是 vue 的 reactive 類型,那么獲取原型,否則會報錯
_object = Vue.toRaw(_object)
}
return _object
}
-
myIndexedDB
兼容瀏覽器的寫法,適應不同的瀏覽器。 -
_db 內部的 IDBOpenDBRequest 用於檢查是否打開數據庫,以及數據庫的相關操作。
-
_vueToObject
這是一個兼容Vue的對象轉換函數。vue的reactive直接存入的話會報錯,需要獲取原型才能存入,我又不想每次保存的時候都多一步操作,所以就寫了這個轉換函數。
如果非vue3環境,可以直接返回參數,不影響其他功能。
建立對象庫以及打開數據庫
// ======== 數據庫操作 ================
/**
* 打開 indexedDB 數據庫。
* dbName:數據庫名稱;
* version:數據庫版本。
* 可以不傳值。
*/
const dbOpen = (dbName, version) => {
// 創建數據庫,並且打開
const name = config.dbName || dbName
const ver = config.ver || version
const dbRequest = myIndexedDB.open(name, ver)
// 記錄數據庫版本是否變更
let isChange = false
/* 該域中的數據庫myIndex */
if (config.debug) {
console.log('dbRequest - 打開indexedDb數據庫:', dbRequest)
}
// 打開數據庫的 promise
const dbPromise = new Promise((resolve, reject) => {
// 數據庫打開成功的回調
dbRequest.onsuccess = (event) => {
// _db = event.target.result
// 數據庫成功打開后,記錄數據庫對象
_db = dbRequest.result
if (isChange) { // 如果變更,則設置初始數據
setup().then(() => {
resolve(_db)
})
} else {
resolve(_db)
}
}
dbRequest.onerror = (event) => {
reject(event) // 返回參數
}
})
// 創建表
// 第一次打開成功后或者版本有變化自動執行以下事件,一般用於初始化數據庫。
dbRequest.onupgradeneeded = (event) => {
isChange = true
_db = event.target.result /* 數據庫對象 */
// 建立對象表
for (let i = 0; i < config.objectStores.length; i++) {
const object = config.objectStores[i]
// 驗證有沒有,沒有的話建立一個對象表
if (!_db.objectStoreNames.contains(object.objectStoreName)) {
const objectStore = _db.createObjectStore(object.objectStoreName, { keyPath: 'id' }) /* 創建person倉庫(表) 主鍵 */
// objectStore = _db.createObjectStore('person',{autoIncrement:true});/*自動創建主鍵*/
// 建立索引
for (let i = 0; i < object.index.length; i++) {
const index = object.index[i]
objectStore.createIndex(index.name, index.name, { unique: index.unique })
}
if (config.debug) {
console.log('onupgradeneeded - 建立了一個新的對象倉庫:', objectStore)
}
}
}
}
// 返回 Promise 實例 —— 打開Indexed庫
return dbPromise
}
這段代碼有點長,因為有兩個功能,一個是打開數據庫,一個是創建數據庫。
indexedDB 的邏輯是這樣的,在open數據庫的時候判斷本地有沒有數據庫,如果沒有數據庫則觸發 onupgradeneeded 事件,創建數據庫,然后打開數據庫。
如果有數據庫的話,判斷版本號,如果高於本地數據庫,那么也會觸發 onupgradeneeded 事件。所以open和 onupgradeneeded 就聯系在了一起。
初始化對象
/**
* 設置初始數據
*/
const setup = () => {
// 定義一個 Promise 的實例
const objectPromise = new Promise((resolve, reject) => {
const arrStore = []
// 遍歷,獲取表名集合,便於打開事務
for (const key in config.objects) {
arrStore.push(key)
}
const tranRequest = _db.transaction(arrStore, 'readwrite')
// 遍歷,添加數據(對象)
for (const key in config.objects) {
const objectArror = config.objects[key]
const store = tranRequest.objectStore(key)
// 清空數據
store.clear().onsuccess = (event) => {
// 遍歷添加數據
for (let i = 0; i < objectArror.length; i++) {
store
.add(objectArror[i])
.onsuccess = (event) => {
if (config.debug) {
console.log(`添加成功!key:${key}-i:${i}`)
}
}
}
}
}
// 遍歷后統一返回
tranRequest.oncomplete = (event) => {
// tranRequest.commit()
if (config.debug) {
console.log('setup - oncomplete')
}
resolve()
}
tranRequest.onerror = (event) => {
reject(event)
}
})
return objectPromise
}
有的時候需要在建庫之后設置一些初始化的數據,於是設計了這個函數。
setup會依據 nf-indexedDB.config 里的配置,把默認對象添加到數據庫里面。
添加對象
基礎的增刪改查系列,不管是數據庫還是對象庫,都躲不開。
// ======== 增刪改操作 ===================================
/**
* 添加對象。
* storeName:對象倉庫名;
* object:要添加的對象
*/
const addObject = (storeName, object) => {
const _object = _vueToObject(object)
// 定義一個 Promise 的實例
const objectPromise = new Promise((resolve, reject) => {
// 定義個函數,便於調用
const _addObject = () => {
const tranRequest = _db.transaction(storeName, 'readwrite')
tranRequest
.objectStore(storeName) // 獲取store
.add(_object) // 添加對象
.onsuccess = (event) => { // 成功后的回調
resolve(event.target.result) // 返回對象的ID
}
tranRequest.onerror = (event) => {
reject(event)
}
}
// 判斷數據庫是否打開
if (typeof _db === 'undefined') {
dbOpen().then(() => {
_addObject()
})
} else {
_addObject()
}
})
return objectPromise
}
這么長的代碼,只是實現了把一個對象填到數據庫里的操作,可見原本的操作是多么的繁瑣。
好吧,不開玩笑了,其實原本的想法是這樣的,想要添加對象要這么寫:
dbOpen().then(() =>{
addObject('blog',{
id: 3,
groupId: 1,
title: '這是三個博客',
addTime: '2020-10-15',
introduction: '這是博客簡介',
concent: '這是博客的詳細內容<br>第二行',
viewCount: 1,
agreeCount: 1
})
})
就是說,每次操作的時候先開庫,然后才能進行操作,但是想想這么做是不是有點麻煩?
能不能不管開不開庫的,直接開魯呢?
於是內部實現代碼就變得復雜了一點。
修改對象
/**
* 修改對象。
* storeName:對象倉庫名;
* object:要修改的對象
*/
const updateObject = (storeName, object) => {
const _object = _vueToObject(object)
// 定義一個 Promise 的實例
const objectPromise = new Promise((resolve, reject) => {
// 定義個函數,便於調用
const _updateObject = () => {
const tranRequest = _db.transaction(storeName, 'readwrite')
// 按照id獲取對象
tranRequest
.objectStore(storeName) // 獲取store
.get(_object.id) // 獲取對象
.onsuccess = (event) => { // 成功后的回調
// 從倉庫里提取對象,把修改值合並到對象里面。
const newObject = { ...event.target.result, ..._object }
// 修改數據
tranRequest
.objectStore(storeName) // 獲取store
.put(newObject) // 修改對象
.onsuccess = (event) => { // 成功后的回調
if (config.debug) {
console.log('updateObject -- onsuccess- event:', event)
}
resolve(event.target.result)
}
}
tranRequest.onerror = (event) => {
reject(event)
}
}
// 判斷數據庫是否打開
if (typeof _db === 'undefined') {
dbOpen().then(() => {
_updateObject()
})
} else {
_updateObject()
}
})
return objectPromise
}
修改對象,是新的對象覆蓋掉原來的對象,一開始是想直接put,但是后來實踐的時候發現,可能修改的時候只是修改其中的一部分屬性,而不是全部屬性,那么直接覆蓋的話,豈不是造成參數不全的事情了嗎?
於是只好先把對象拿出來,然后和新對象合並一下,然后再put回去,於是代碼就又變得這么長了。
刪除對象
/**
* 依據id刪除對象。
* storeName:對象倉庫名;
* id:要刪除的對象的key值,注意類型要准確。
*/
const deleteObject = (storeName, id) => {
// 定義一個 Promise 的實例
const objectPromise = new Promise((resolve, reject) => {
// 定義個函數,便於調用
const _deleteObject = () => {
const tranRequest = _db.transaction(storeName, 'readwrite')
tranRequest
.objectStore(storeName) // 獲取store
.delete(id) // 刪除一個對象
.onsuccess = (event) => { // 成功后的回調
resolve(event.target.result)
}
tranRequest.onerror = (event) => {
reject(event)
}
}
// 判斷數據庫是否打開
if (typeof _db === 'undefined') {
dbOpen().then(() => {
_deleteObject()
})
} else {
_deleteObject()
}
})
return objectPromise
}
其實吧刪除對象,一個 delete 就可以了,但是還是要先判斷一下是否打開數據庫,於是代碼還是短不了。
清空倉庫里的對象
/**
* 清空store里的所有對象。
* storeName:對象倉庫名;
*/
const clearStore = (storeName) => {
// 定義一個 Promise 的實例
const objectPromise = new Promise((resolve, reject) => {
// 定義個函數,便於調用
const _clearStore = () => {
const tranRequest = _db.transaction(storeName, 'readwrite')
tranRequest
.objectStore(storeName) // 獲取store
.clear() // 清空對象倉庫里的對象
.onsuccess = (event) => { // 成功后的回調
resolve(event)
}
tranRequest.onerror = (event) => {
reject(event)
}
}
// 判斷數據庫是否打開
if (typeof _db === 'undefined') {
dbOpen().then(() => {
_clearStore()
})
} else {
_clearStore()
}
})
return objectPromise
}
- clear()
清空指定對象倉庫里的所有對象,請謹慎操作。
刪除對象倉庫
/**
* 刪除整個store。
* storeName:對象倉庫名;
*/
const deleteStore = (storeName) => {
// 定義一個 Promise 的實例
const objectPromise = new Promise((resolve, reject) => {
// 定義個函數,便於調用
const _deleteStore = () => {
const tranRequest = _db.transaction(storeName, 'readwrite')
tranRequest
.objectStore(storeName) // 獲取store
.delete() // 清空對象倉庫里的對象
.onsuccess = (event) => { // 成功后的回調
resolve(event)
}
tranRequest.onerror = (event) => {
reject(event) // 失敗后的回調
}
}
// 判斷數據庫是否打開
if (typeof _db === 'undefined') {
dbOpen().then(() => {
_deleteStore()
})
} else {
_deleteStore()
}
})
return objectPromise
}
這個就更厲害了,可以把對象倉庫給刪掉。更要謹慎。
刪除數據庫
/**
* 刪除數據庫。
* dbName:數據庫名;
*/
const deleteDB = (dbName) => {
// 定義一個 Promise 的實例
const objectPromise = new Promise((resolve, reject) => {
// 刪掉整個數據庫
myIndexedDB.deleteDatabase(dbName).onsuccess = (event) => {
resolve(event)
}
})
return objectPromise
}
能建立數據庫,那么就應該能刪除數據庫,這個就是。
這個就非常簡單了,不用判斷是否打開數據庫,直接刪除就好。
不過前端數據庫應該具備這樣的功能:整個庫刪掉后,可以自動恢復狀態才行。
按主鍵獲取對象,或者獲取全部
/**
* 獲取對象。
* storeName:對象倉庫名;
* id:要獲取的對象的key值,注意類型要准確,只能取一個。
* 如果不設置id,會返回store里的全部對象
*/
const getObject = (storeName, id) => {
const objectPromise = new Promise((resolve, reject) => {
const _getObject = () => {
const tranRequest = _db.transaction(storeName, 'readonly')
const store = tranRequest.objectStore(storeName) // 獲取store
let dbRequest
// 判斷是獲取一個,還是獲取全部
if (typeof id === 'undefined') {
dbRequest = store.getAll()
} else {
dbRequest = store.get(id)
}
dbRequest.onsuccess = (event) => { // 成功后的回調
if (config.debug) {
console.log('getObject -- onsuccess- event:', id, event)
}
resolve(event.target.result) // 返回對象
}
tranRequest.onerror = (event) => {
reject(event)
}
}
// 判斷數據庫是否打開
if (typeof _db === 'undefined') {
dbOpen().then(() => {
_getObject()
})
} else {
_getObject()
}
})
return objectPromise
}
這里有兩個功能
- 依據ID獲取對應的對象
- 獲取對象倉庫里的所有對象
不想取兩個函數名,於是就依據參數來區分了,傳遞ID就獲取ID的對象,沒有傳遞ID就返回全部。
查詢對象倉庫
/**
* 依據 索引+游標,獲取對象,可以獲取多條。
* storeName:對象倉庫名。
* page:{
* start:開始,
* count:數量,
* description:'next'
* // next 升序
* // prev 降序
* // nextunique 升序,只取一
* // prevunique 降序,只取一
* }
* findInfo = {
* indexName: 'groupId',
* indexKind: '=', // '>','>=','<','<=','between',
* indexValue: 1,
* betweenInfo: {
* v1:1,
* v2:2,
* v1isClose:true,
* v2isClose:true,
* },
* where:(object) => {
* reutrn true/false
* }
* }
*/
const findObject = (storeName, findInfo = {}, page = {}) => {
const _start = page.start || 0
const _count = page.count || 0
const _end = _start + _count
const _description = page.description || 'prev' // 默認倒序
// 查詢條件,按照主鍵或者索引查詢
let keyRange = null
if (typeof findInfo.indexName !== "undefined") {
if (typeof findInfo.indexKind !== "undefined") {
const id = findInfo.indexValue
const dicRange = {
"=":IDBKeyRange.only(id),
">":IDBKeyRange.lowerBound(id, true),
">=":IDBKeyRange.lowerBound(id),
"<":IDBKeyRange.upperBound(id, true),
"<=":IDBKeyRange.upperBound(id)
}
switch (findInfo.indexKind) {
case '=':
case '>':
case '>=':
case '<':
case '<=':
keyRange = dicRange[findInfo.indexKind]
break
case 'between':
const betweenInfo = findInfo.betweenInfo
keyRange = IDBKeyRange.bound(betweenInfo.v1,betweenInfo.v2,betweenInfo.v1isClose,betweenInfo.v2isClose)
break
}
}
}
console.log('findObject - keyRange', keyRange)
const objectPromise = new Promise((resolve, reject) => {
// 定義個函數,便於調用
const _findObjectByIndex = () => {
const dataList = []
let cursorIndex = 0
const tranRequest = _db.transaction(storeName, 'readonly')
const store = tranRequest.objectStore(storeName)
let cursorRequest
// 判斷是否索引查詢
if (typeof findInfo.indexName === "undefined") {
cursorRequest = store.openCursor(keyRange, _description)
} else {
cursorRequest = store
.index(findInfo.indexName)
.openCursor(keyRange, _description)
}
cursorRequest.onsuccess = (event) => {
const cursor = event.target.result
if (cursor) {
if (_end === 0 || (cursorIndex >= _start && cursorIndex < _end)) {
// 判斷鈎子函數
if (typeof findInfo.where === 'function') {
if (findInfo.where(cursor.value, cursorIndex)) {
dataList.push(cursor.value)
cursorIndex++
}
} else { // 沒有設置查詢條件
dataList.push(cursor.value)
cursorIndex++
}
}
cursor.continue()
}
// tranRequest.commit()
}
tranRequest.oncomplete = (event) => {
if (config.debug) {
console.log('findObjectByIndex - dataList', dataList)
}
resolve(dataList)
}
tranRequest.onerror = (event) => {
console.log('findObjectByIndex - onerror', event)
reject(event)
}
}
// 判斷數據庫是否打開
if (typeof _db === 'undefined') {
dbOpen().then(() => {
_findObjectByIndex()
})
} else {
_findObjectByIndex()
}
})
return objectPromise
}
打開指定的對象倉庫,然后判斷是否設置了索引查詢,沒有的話打開倉庫的游標,如果設置了,打開索引的游標。
可以用鈎子實現其他屬性的查詢。
可以分頁獲取數據,方法類似於mySQL的 limit。
功能測試
封裝完畢,要寫個測試代碼來跑一跑,否則怎么知道到底好不好用呢。
於是寫了一個比較簡單的測試代碼。
建立對象庫
dbOpen().then(() =>{
// 建表初始化之后,獲取全部對象
getAll()
})
- dbOpen
打開數據庫,同時判斷是否需要建立數據庫,如果需要的話,會根據配置信息自動建立數據庫
然后我們按F12,打開Application標簽,可以找到我們建立的數據庫,如圖:
我們可以看一下索引的情況,如圖:
添加對象
addObject('blog',{
id: new Date().valueOf(),
groupId: 1,
title: '這是三個博客',
addTime: '2020-10-15',
introduction: '這是博客簡介',
concent: '這是博客的詳細內容<br>第二行',
viewCount: 1,
agreeCount: 1
}).then((data) => {
re.value = data
getAll()
})
-
倉庫名
第一個參數是對象倉庫的名稱,目前暫時采用字符串的形式。 -
對象
第二個參數是要添加的對象,其屬性必須有主鍵和索引,其他隨意。 -
返回值
成功后會返回對象ID
點右鍵可以刷新數據,如圖:
更新后的數據,如圖:
修改對象
updateObject('blog',blog).then((data) => {
re.value = data
getAll()
})
-
倉庫名
第一個參數是對象倉庫的名稱,目前暫時采用字符串的形式。 -
對象
第二個參數是要修改的對象,屬性可以不全。 -
返回值
成功后會返回對象ID
刪除對象
deleteObject('blog',id).then((data) => {
re.value = data
getAll()
})
-
倉庫名
第一個參數是對象倉庫的名稱,目前暫時采用字符串的形式。 -
對象
第二個參數是要刪除的對象的ID。 -
返回值
成功后會返回對象ID
清空倉庫里的對象
clearStore('blog').then((data) => {
re.value = data
getAll()
})
-
倉庫名
第一個參數是對象倉庫的名稱,目前暫時采用字符串的形式。 -
返回值
成功后會返回對象ID
刪除對象倉庫
deleteStore('blog').then((data) => {
re.value = data
getAll()
})
-
倉庫名
第一個參數是對象倉庫的名稱,目前暫時采用字符串的形式。 -
返回值
成功后會返回對象ID
刪除數據庫
deleteDB('dbTest').then((data) => {
re.value = data
getAll()
})
- 數據庫名稱
第一個參數是數據庫的名稱
查詢功能
// 查詢條件
const findInfo = {
indexName: 'groupId',
indexKind: '=', // '>','>=','<','<=','between',
indexValue: 1,
betweenInfo: {
v1:1,
v2:2,
v1isClose:true,
v2isClose:true,
},
where: (object) => {
if (findKey.value == '') return true
let re = false
if (object.title.indexOf(findKey.value) >= 0) {
re = true
}
if (object.introduction.indexOf(findKey.value) >= 0) {
re = true
}
if (object.concent.indexOf(findKey.value) >= 0) {
re = true
}
return re
}
}
const find = () => {
findObject('blog', findInfo).then((data) => {
findRe.value = data
})
}
-
findInfo
查詢信息的對象,把需要查詢的信息都放在這里 -
indexName
索引名稱,可以不設置。 -
indexKind
索引屬性的查詢方式,如果設置indexName,則必須設置。 -
indexValue
索引字段的查詢值 -
betweenInfo
如果 indexKind = 'between' 的話,需要設置。 -
v1
開始值 -
v2
結束值 -
v1isClose
是否閉合區間 -
v2isClose
是否閉合區間 -
where
鈎子函數,可以不設置。
內部打開游標后,會把對象返回來,然后我們就可以在這里進行各種條件判斷。
全部代碼就不貼了,感興趣的話可以去GitHub看。
貼一個折疊后的效果圖吧:
就是先把相關的功能和在一起,寫一個操作類,然后在setup里面應用這個類就可以了,然后寫點代碼把各個類關聯起來即可。
這樣代碼好維護多了。
小結
功能不是很完善,目前是自己夠用的程度。
本來想用純js來寫個使用方式的,但是發現還是用vue寫着方便,於是測試代碼就變成了vue的形式。
源碼
https://github.com/naturefwvue/nf-vue-cnd/tree/main/cnd/LocalStore/IndexedDB
在線演示
https://naturefwvue.github.io/nf-vue-cnd/cnd/LocalStore/IndexedDB/
參考資料
官網:https://developer.mozilla.org/zh-CN/docs/Web/API/IndexedDB_API
阮一峰的網絡日志:http://www.ruanyifeng.com/blog/2018/07/indexeddb.html