首頁>技術>

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(域名轉發錯誤, 域名轉發錯誤, { 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  }
清空倉庫裡的物件
/**  * 清空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

清空倉庫裡的物件
       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

謙行: https://www.cnblogs.com/dolphinX/p/3416889.html

14
最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • google視覺化程式設計工具blockly入門