indexedDB

IndexedDB is a low-level API for storing large amounts of structured data on the client side that can be created and manipulated by web scripts. IndexedDB allows you to store large amounts of data, provide a lookup interface, and create indexes that LocalStorage does not have. In terms of database type, IndexedDB is not a relational database (does not support SQL queries) and is closer to a NoSQL database. Other introduction does not carry, you can baidu, there are reference materials behind.

demand

I wanted to better implement the document-driven idea and realized that I needed front-end storage, so I decided to use IndexedDB to do that. But look at its operation is cumbersome, so I plan to encapsulate it.

The official website gave several third party encapsulation library, I also point in the past to see, the result did not understand. Thought about it or do their own food and clothing.

The idea of making wheels over and over again:

  • The first is the ability to make wheels.
  • You build your own wheels. You handle them better.

Functional design

According to the function introduction on the official website, the functions are sorted out: as shown in the figure:

It is to build a database, add, delete, change and check that set. It’s great to see some third-party encapsulated libraries that support SQL statements. There’s no need for that right now, well, the capacity is limited. In short, meet their own needs first, later in the slow improvement.

Code implementation

Or simple and rough, directly on the code, the introduction of basic knowledge, there are a lot of online, you can see the back of the reference. The official website is also more detailed, and the Chinese version.

The configuration file

nf-indexedDB.config

const config = {
  dbName: 'dbTest'.ver: 1.debug: true.objectStores: [ // Base for database construction
    {
      objectStoreName: 'blog'.index: [ // Index, whether unique can be repeated
        { name: 'groupId'.unique: false}}]].objects: { // Initialize the data
    blog: [{id: 1.groupId: 1.title: 'This is a blog'.addTime: '2020-10-15'.introduction: 'This is the blog introduction'.concent: 'Here's the blog in detail 

Line 2'
.viewCount: 1.agreeCount: 1 }, { id: 2.groupId: 2.title: 'These are two blogs'.addTime: '2020-10-15'.introduction: 'This is the blog introduction'.concent: 'Here's the blog in detail

Line 2'
.viewCount: 10.agreeCount: 10}}}]export default config Copy the code
  • dbName

Specify the database name

  • ver

Specifying the database version

  • debug

Specifies whether to print the state

  • objectStores

Description of object repository, library name, index, etc.

  • objects

Initialize data. If you need to add default data after the database is created, you can set it here.

The setup here is not perfect, and there are some small problems that I haven’t figured out how to solve yet. You can change it later when you think about it.

The internal members

 /** * IndexedDB database object * determines whether the browser supports ** /
  const myIndexedDB = window.indexedDB || window.webkitIndexedDB || window.mozIndexedDB || window.msIndexedDB
  if(! myIndexedDB) {console.log('Your browser does not support IndexedDB')}let _db // Holds an instance of indexed database internally

  Var var var var var var var var var var var var var var var var var var var var var
  const _vueToObject = (vueObject) = > {
    let _object = vueObject
    // The type judgment made for Vue3
    if (Vue.isRef(_object)) {
      // If vue is ref type, replace with ref. Value
      _object = _object.value
    }
    if (Vue.isReactive(_object)) {
      Reactive type of vue; reactive type of vue
      _object = Vue.toRaw(_object)
    }
    return _object
  }
Copy the code
  • myIndexedDB

Compatible browser writing, adapt to different browsers.

  • The IDBOpenDBRequest inside _db is used to check whether the database is open, and for database-related operations.

  • _vueToObject

This is a Vue compatible object conversion function. If Vue’s reactive is directly stored, an error will be reported, and the prototype needs to be obtained before it can be stored. I don’t want to make extra steps every time I save it, so I wrote this transformation function. If the environment is not VUE3, the parameter can be returned directly without affecting other functions.

Create the object library and open the database

  // ======== Database operation ================
/** * Open indexedDB. * dbName: database name; * version: indicates the database version. * No value can be passed. * /
  const dbOpen = (dbName, version) = > {
    // Create a database and open it
    const name = config.dbName || dbName
    const ver = config.ver || version
    const dbRequest = myIndexedDB.open(name, ver)
    // Record whether the database version has changed
    let isChange = false
    /* The database myIndex */ in this domain
    if (config.debug) {
      console.log('dbRequest - Open indexedDb database: ', dbRequest)
    }
    // Open the database promise
    const dbPromise = new Promise((resolve, reject) = > {
      // The database opened successfully callback
      dbRequest.onsuccess = (event) = > {
        // _db = event.target.result
        // After the database is successfully opened, record the database object
        _db = dbRequest.result
        if (isChange) { // If changed, the initial data is set
          setup().then(() = > {
            resolve(_db)
          })
        } else {
          resolve(_db)
        }
      }

      dbRequest.onerror = (event) = > {
        reject(event) // Return parameters}})/ / create a table
    // The following events are automatically executed after the first successful opening or the version changes, generally used to initialize the database.
    dbRequest.onupgradeneeded = (event) = > {
      isChange = true
      _db = event.target.result /* Database object */
      // Create the object table
      for (let i = 0; i < config.objectStores.length; i++) {
        const object = config.objectStores[i]
        // Create a table of objects
        if(! _db.objectStoreNames.contains(object.objectStoreName)) {const objectStore = _db.createObjectStore(object.objectStoreName, { keyPath: 'id' }) /* Create the person repository primary key */
          // objectStore = _db.createObjectStore('person',{autoIncrement:true}); /* Automatically create primary keys */
          // Create index
          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('Onupgradenneeded - which has built a new object repository:', objectStore)
          }
        }
      }
    }

    // Return the Promise instance -- open the Indexed library
    return dbPromise
  }
Copy the code

This code is a bit long because it has two functions, one is to open the database, the other is to create the database.

IndexedDB logic is needed to determine if there is a local database when it is opened and if there is no database it triggers onUpgradenneeded events, creates a database and then opens the database. If there is a database, determine the version number which is higher than the local database which also triggers onUpgradenneeded events. So open needed and onupgradenneeded are connected.

Initialize an object

  /** * Set initial data */
  const setup = () = > {
    // Define an instance of a Promise
    const objectPromise = new Promise((resolve, reject) = > {
      const arrStore = []
      // Iterates to get a set of table names for opening transactions
      for (const key in config.objects) {
        arrStore.push(key)
      }
      const tranRequest = _db.transaction(arrStore, 'readwrite')

      // Iterate, add data (object)
      for (const key in config.objects) {
        const objectArror = config.objects[key]
        const store = tranRequest.objectStore(key)
        // Clear the data
        store.clear().onsuccess = (event) = > {
          // Iterate to add data
          for (let i = 0; i < objectArror.length; i++) {
            store
              .add(objectArror[i])
              .onsuccess = (event) = > {
                if (config.debug) {
                  console.log('Added successfully! key:${key}-i:${i}`)}}}}}// Return after traversal
      tranRequest.oncomplete = (event) = > {
        // tranRequest.commit()
        if (config.debug) {
          console.log('setup - oncomplete')
        }
        resolve()
      }
      tranRequest.onerror = (event) = > {
        reject(event)
      }
    })
    return objectPromise
  }

Copy the code

Sometimes you need to set up some initialization data after building a library, so this function is designed. Setup will add default objects to the database based on the configuration in NF-Indexeddb.config.

Add the object

The basic series of additions, additions, modifications, and queries, whether databases or object libraries, are unavoidable.

/ / = = = = = = = = add deletion operation = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
  /** * Add the object. * storeName: Name of the object repository; * object: Object to be added */
  const addObject = (storeName, object) = > {
    const _object = _vueToObject(object)
    // Define an instance of a Promise
    const objectPromise = new Promise((resolve, reject) = > {
      // Define a function that is easy to call
      const _addObject = () = > {
        const tranRequest = _db.transaction(storeName, 'readwrite')
        tranRequest
          .objectStore(storeName) / / for the store
          .add(_object) // Add the object
          .onsuccess = (event) = > { // Callback after success
            resolve(event.target.result) // Return the ID of the object
          }
        tranRequest.onerror = (event) = > {
          reject(event)
        }
      }

      // Check whether the database is open
      if (typeof _db === 'undefined') {
        dbOpen().then(() = > {
          _addObject()
        })
      } else {
        _addObject()
      }
    })
    return objectPromise
  }
Copy the code

Such a long code, only the implementation of an object to fill in the database operation, visible the original operation is how tedious.

All right, joking aside, the original idea was to add an object like this:

dbOpen().then(() = >{
  addObject('blog', {id: 3.groupId: 1.title: 'These are three blogs'.addTime: '2020-10-15'.introduction: 'This is the blog introduction'.concent: 'Here's the blog in detail 

Line 2'
.viewCount: 1.agreeCount: 1})})Copy the code

That is to say, every time the operation first open the library, and then the operation, but think about this is a little troublesome? Can we not open the library, directly kailu? So the internal implementation code gets a little more complicated.

Modify the object

  /** * Modify object. * storeName: Name of the object repository; * object: Object to be modified */
  const updateObject = (storeName, object) = > {
    const _object = _vueToObject(object)
    // Define an instance of a Promise
    const objectPromise = new Promise((resolve, reject) = > {
      // Define a function that is easy to call
      const _updateObject = () = > {
        const tranRequest = _db.transaction(storeName, 'readwrite')
        // Obtain the object by id
        tranRequest
          .objectStore(storeName) / / for the store
          .get(_object.id) // Get the object
          .onsuccess = (event) = > { // Callback after success
            // Retrieve the object from the repository and merge the modified values into the object.
            constnewObject = { ... event.target.result, ... _object }// Modify the data
            tranRequest
              .objectStore(storeName) / / for the store
              .put(newObject) // Modify the object
              .onsuccess = (event) = > { // Callback after success
                if (config.debug) {
                  console.log('updateObject -- onsuccess- event:', event)
                }
                resolve(event.target.result)
              }
          }

        tranRequest.onerror = (event) = > {
          reject(event)
        }
      }
      // Check whether the database is open
      if (typeof _db === 'undefined') {
        dbOpen().then(() = > {
          _updateObject()
        })
      } else {
        _updateObject()
      }
    })
    return objectPromise
  }

Copy the code

When you modify an object, it means that the new object overwrites the original object. At the beginning, you want to put it directly, but later in practice, you find that you may only modify part of the attributes, but not all the attributes. If you directly overwrite an object, won’t it cause incomplete parameters?

So you have to take the object out, merge it with the new object, and then put it back, and the code gets this long again.

Delete the object

 /** * Delete object based on id. * storeName: Name of the object repository; * id: specifies the key value of the object to be deleted. * /
  const deleteObject = (storeName, id) = > {
    // Define an instance of a Promise
    const objectPromise = new Promise((resolve, reject) = > {
      // Define a function that is easy to call
      const _deleteObject = () = > {
        const tranRequest = _db.transaction(storeName, 'readwrite')
        tranRequest
          .objectStore(storeName) / / for the store
          .delete(id) // Delete an object
          .onsuccess = (event) = > { // Callback after success
            resolve(event.target.result)
          }
        tranRequest.onerror = (event) = > {
          reject(event)
        }
      }
      // Check whether the database is open
      if (typeof _db === 'undefined') {
        dbOpen().then(() = > {
          _deleteObject()
        })
      } else {
        _deleteObject()
      }
    })
    return objectPromise
  }

Copy the code

In fact, delete the object, a delete can be, but still to determine whether to open the database first, so the code is still short.

Empty the warehouse of objects

 /** * Empty all objects in the store. * storeName: Name of the object repository; * /
  const clearStore = (storeName) = > {
    // Define an instance of a Promise
    const objectPromise = new Promise((resolve, reject) = > {
      // Define a function that is easy to call
      const _clearStore = () = > {
        const tranRequest = _db.transaction(storeName, 'readwrite')
        tranRequest
          .objectStore(storeName) / / for the store
          .clear() // Empty the object repository
          .onsuccess = (event) = > { // Callback after success
            resolve(event)
          }
        tranRequest.onerror = (event) = > {
          reject(event)
        }
      }
      // Check whether the database is open
      if (typeof _db === 'undefined') {
        dbOpen().then(() = > {
          _clearStore()
        })
      } else {
        _clearStore()
      }
    })
    return objectPromise
  }
Copy the code
  • clear()

Clear all objects in the specified object repository. Exercise caution when performing this operation.

Deleting an object repository

  /** * Delete the entire store. * storeName: Name of the object repository; * /
  const deleteStore = (storeName) = > {
    // Define an instance of a Promise
    const objectPromise = new Promise((resolve, reject) = > {
      // Define a function that is easy to call
      const _deleteStore = () = > {
        const tranRequest = _db.transaction(storeName, 'readwrite')
        tranRequest
          .objectStore(storeName) / / for the store
          .delete() // Empty the object repository
          .onsuccess = (event) = > { // Callback after success
            resolve(event)
          }
        tranRequest.onerror = (event) = > {
          reject(event) // Callback after failure}}// Check whether the database is open
      if (typeof _db === 'undefined') {
        dbOpen().then(() = > {
          _deleteStore()
        })
      } else {
        _deleteStore()
      }
    })
    return objectPromise
  }
Copy the code

This is even better, because you can delete the object store. Be more cautious.

Deleting a database

  /** * Delete database. * dbName: database name; * /
  const deleteDB = (dbName) = > {
    // Define an instance of a Promise
    const objectPromise = new Promise((resolve, reject) = > {
      // Delete the entire database
      myIndexedDB.deleteDatabase(dbName).onsuccess = (event) = > {
        resolve(event)
      }
    })
    return objectPromise
  }
Copy the code

If you can create a database, you should be able to delete a database, which is what this is. This is very simple, do not judge whether open database, directly delete good. However, the front-end database should have such a function: the entire library deleted, can automatically restore the state of the line.

Press the primary key to get the object, or all of it

  /** * Get the object. * storeName: Name of the object repository; * id: specifies the key value of the object to be obtained. * If no id is set, all objects in the store are returned */
  const getObject = (storeName, id) = > {
    const objectPromise = new Promise((resolve, reject) = > {
      const _getObject = () = > {
        const tranRequest = _db.transaction(storeName, 'readonly')
        const store = tranRequest.objectStore(storeName) / / for the store
        let dbRequest
        // Decide whether to get one or all
        if (typeof id === 'undefined') {
          dbRequest = store.getAll()
        } else {
          dbRequest = store.get(id)
        }

        dbRequest.onsuccess = (event) = > { // Callback after success
          if (config.debug) {
            console.log('getObject -- onsuccess- event:', id, event)
          }
          resolve(event.target.result) // Return the object
        }
    
        tranRequest.onerror = (event) = > {
          reject(event)
        }
      }
      // Check whether the database is open
      if (typeof _db === 'undefined') {
        dbOpen().then(() = > {
          _getObject()
        })
      } else {
        _getObject()
      }
    })

    return objectPromise
  }
Copy the code

There are two functions here

  • Obtain the corresponding object based on the ID
  • Gets all objects in the object repository

We don’t want to have two function names, so we’re going to separate them by argument, get an object with an ID if we pass it, and return all of it if we don’t pass an ID.

Querying the Object Repository

  /** * Obtain multiple objects based on the index + cursor. * storeName: Specifies the name of the object repository. * page: {* start: start, * count: count, * description:'next' * // next * // prev * // nextUnique * // PrevUnique * // PrevUnique * // prevUnique * // prevUnique * // prevUnique * // prevUnique * / Take only one *} * 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' // Default reverse order

    // The primary key or index is used for the query
    let keyRange = null
    if (typeoffindInfo.indexName ! = ="undefined") {
      if (typeoffindInfo.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) = > {
      // Define a function that is easy to call
      const _findObjectByIndex = () = > {
        const dataList = []
        let cursorIndex = 0
        const tranRequest = _db.transaction(storeName, 'readonly')
        const store = tranRequest.objectStore(storeName)
        let cursorRequest 
        // Determine whether to index the query
        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)) {
              // Judge the hook function
              if (typeof findInfo.where === 'function') {
                if (findInfo.where(cursor.value, cursorIndex)) {
                  dataList.push(cursor.value)
                  cursorIndex++
                }
              } else { // No search criteria are set
                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)
        }
      }

      // Check whether the database is open
      if (typeof _db === 'undefined') {
        dbOpen().then(() = > {
          _findObjectByIndex()
        })
      } else {
        _findObjectByIndex()
      }
    })
    return objectPromise
  }

Copy the code

Open the specified object repository and check whether index query is set. If not, open the repository cursor. If yes, open the index cursor. You can use hooks to query other properties. You can get data in pages, similar to mySQL limit.

A functional test

After encapsulation, write a test code to run, otherwise how to know whether good or not. So I wrote a relatively simple test code.

Building an object library

dbOpen().then(() = >{
    // After creating a table, get all objects
    getAll()
})
Copy the code
  • dbOpen

Open the database and determine whether to create a database. If yes, the database is automatically created based on the configuration information

Then press F12, open the Application TAB, and you can find the database we created, as shown in the figure below:

We can take a look at the index, as shown here:

Add the object

        addObject('blog', {id: new Date().valueOf(),
          groupId: 1.title: 'These are three blogs'.addTime: '2020-10-15'.introduction: 'This is the blog introduction'.concent: 'Here's the blog in detail 

Line 2'
.viewCount: 1.agreeCount: 1 }).then((data) = > { re.value = data getAll() }) Copy the code
  • Warehouse,

The first argument is the name of the object repository, which is a string for now.

  • object

The second parameter is the object to be added, whose properties must have a primary key and index, and so on.

  • The return value

The object ID is returned upon success

Click the right button to refresh data, as shown in the figure:

The updated data is shown as follows:

Modify the object

        updateObject('blog',blog).then((data) = > {
          re.value = data
          getAll()
        })
Copy the code
  • Warehouse,

The first argument is the name of the object repository, which is a string for now.

  • object

The second parameter is the object to be modified, and the property can be incomplete.

  • The return value

The object ID is returned upon success

Delete the object

        deleteObject('blog',id).then((data) = > {
          re.value = data
          getAll()
        })
Copy the code
  • Warehouse,

The first argument is the name of the object repository, which is a string for now.

  • object

The second argument is the ID of the object to delete.

  • The return value

The object ID is returned upon success

Empty the warehouse of objects

        clearStore('blog').then((data) = > {
          re.value = data
          getAll()
        })
Copy the code
  • Warehouse,

The first argument is the name of the object repository, which is a string for now.

  • The return value

The object ID is returned upon success

Deleting an object repository

        deleteStore('blog').then((data) = > {
          re.value = data
          getAll()
        })
Copy the code
  • Warehouse,

The first argument is the name of the object repository, which is a string for now.

  • The return value

The object ID is returned upon success

Deleting a database

         deleteDB('dbTest').then((data) = > {
          re.value = data
          getAll()
        })
Copy the code
  • Database name

The first parameter is the name of the database

Query function

       // Query conditions
      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
        })
      }
Copy the code
  • findInfo

Query information object, put all the information you need to query here

  • indexName

Index name. This parameter is optional.

  • indexKind

Query mode of the index attribute. If indexName is set, it must be set.

  • indexValue

The query value of the index field

  • betweenInfo

If indexKind = ‘between’, it needs to be set.

  • v1

Begin to value

  • v2

The end value

  • v1isClose

Closed interval

  • v2isClose

Closed interval

  • where

Hook function, which may not be set. When we open the cursor inside, we return the object, and then we can make all kinds of criteria here.

I will not post the entire code, if you are interested, you can go to GitHub to see. Paste a picture of the folded effect:

It’s just a matter of putting the related functions together, writing an action class, then applying that class in setup, and then writing some code to associate the classes together.

The code is much easier to maintain.

The source code

Github.com/naturefwvue…

The online demo

Naturefwvue. Making. IO/nf – vue – CND /…

The resources

Liverpoolfc.tv: developer.mozilla.org/zh-CN/docs/…

Nguyen other blog: www.ruanyifeng.com/blog/2018/0…

Ken: www.cnblogs.com/dolphinX/p/…