ES6(四)用Promise封裝一下IndexedDB

   

   ES6(四)用Promise封裝一下IndexedDB    

2021-02-04 18:44 金色海洋(jyk)  閱讀(285)  評論(3) 編輯 收藏    html

indexedDB

IndexedDB 是一種底層 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
  }
  • 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: '=', // '>','>=',' {
  *     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()
})
  • dbOpen
    打開數據庫,同時判斷是否須要創建數據庫,若是須要的話,會根據配置信息自動創建數據庫

而後咱們按F12,打開Application標籤,能夠找到咱們創建的數據庫,如圖:
建數據庫.png

咱們能夠看一下索引的狀況,如圖:
02索引.png

添加對象

       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

點右鍵能夠刷新數據,如圖:
03刷新.png

更新後的數據,如圖:
04新數據.png

修改對象

        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看。
貼一個摺疊後的效果圖吧:
05代碼截圖.png

就是先把相關的功能和在一塊兒,寫一個操做類,而後在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

謙行: http://www.javashuo.com/article/p-shnnepjp-e.html

  • 分類            Vue3,            ES6
相關文章
相關標籤/搜索