2021-02-04 18:44 金色海洋(jyk) 閱讀(285) 評論(3) 編輯 收藏 html
indexedDBIndexedDB 是一種底層 API,用於在客戶端存儲大量的結構化數據,它能夠被網頁腳本建立和操做。
IndexedDB 容許儲存大量數據,提供查找接口,還能創建索引,這些都是 LocalStorage 所不具有的。
就數據庫類型而言,IndexedDB 不屬於關係型數據庫(不支持 SQL 查詢語句),更接近 NoSQL 數據庫。
其餘的介紹就不搬運了,你們能夠自行百度,後面有參考資料。前端
我想更好的實現文檔驅動的想法,發現須要實現前端存儲的功能,因而打算採用 IndexedDB 來實現前端存儲的功能。可是看了一下其操做方式比較繁瑣,因此打算封裝一下。vue
官網給了幾個第三方的封裝庫,我也點過去看了看,結果沒看懂。想了想仍是本身動手豐衣足食吧。react
關於重複製造輪子的想法:git
按照官網的功能介紹,把功能整理了一下:
如圖:
github
就是建庫、增刪改查那一套。看到有些第三方的封裝庫,能夠實現支持sql語句方式的查詢,真的很厲害。目前沒有這種需求,好吧,能力有限實現不了。
總之,先知足本身的需求,之後在慢慢改進。web
仍是簡單粗暴,直接上代碼吧,基礎知識的介紹,網上有不少了,能夠看後面的參考資料。官網介紹的也比較詳細,還有中文版的。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: '這是博客的詳細內容
第二行', viewCount: 1, agreeCount: 1 }, { id: 2, groupId: 2, title: '這是兩個博客', addTime: '2020-10-15', introduction: '這是博客簡介', concent: '這是博客的詳細內容
第二行', viewCount: 10, agreeCount: 10 } ] } } export default config
dbName
指定數據庫名稱promise
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: '這是博客的詳細內容
第二行', 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 }
/** * 刪除整個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就返回所有。
/** * 依據 索引+遊標,獲取對象,能夠獲取多條。 * storeName:對象倉庫名。 * page:{ * start:開始, * count:數量, * description:'next' * // next 升序 * // prev 降序 * // nextunique 升序,只取一 * // prevunique 降序,只取一 * } * findInfo = { * indexName: 'groupId', * indexKind: '=', // '>','>=',' { * 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), " { // 定義個函數,便於調用 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() })
而後咱們按F12,打開Application標籤,能夠找到咱們創建的數據庫,如圖:
咱們能夠看一下索引的狀況,如圖:
addObject('blog',{ id: new Date().valueOf(), groupId: 1, title: '這是三個博客', addTime: '2020-10-15', introduction: '這是博客簡介', concent: '這是博客的詳細內容
第二行', 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: '=', // '>','>=',' { 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