With IndexedDB’s website

Developer.mozilla.org/zh-CN/docs/… This is probably the official Internet cafe, originally in English, now is a Chinese version. Take a look at the official website if you have time.

Introduction to the

IndexedDB is an underlying API for storing large amounts of structured data (also file/binary Large objects (BloBs)) on the client side. The API enables high-performance searches of data using indexes.

Simply say is – can install! IndexedDB is a front-end “transactional object database” that can hold many, many objects (as well as other types). IndexedDB can be used as a front-end cache container for data (objects). Or something else.

It is also very simple to use, there are many tutorials available on the Internet, and several packaged libraries are recommended on the official website. It’s just that I’m lazy and have to look at other people’s libraries (well, I don’t understand them), but I want to encapsulate a library that I’m used to.

Recently, Vue3 is being used, so I want to make a set of indexedDB class library for Vue3 to achieve the function of client caching data.

In fact, the version introduced here should be regarded as the second version. After trying the first version in the project for a period of time, I found several problems, so I want to solve them together in the new version.

IndexedDB operation roadmap

When looking at indexedDB at first, I was confused. I would just read articles written by the big guy, and then I would copy the code, regardless of the principle, so that it could run and read and write data.

Now, after a period of time, I have a little understanding, sorted as follows:

  • Gets the object of indexedDB
  • Open (open/create) the database.
  • If there is no database, or version upgrade:
    • Call onupgradenneeded (build/modify database) and then call onSuccess.
  • If the database already exists and the version is unchanged, call onSuccess directly.
  • After getting the connection object in onSuccess:
    • Start a transaction.
      • Get the object repository.
        • Perform various operations: Add, modify, delete, obtain, etc.
        • Implement queries with indexes and cursors.
      • results

Once we have a clear idea, we can encapsulate it.

Make a help to encapsulate the initialization code

There is a clear difference between a front-end database and a back-end database, where the database is configured, the required tables are created, the initial data is added, and the project is run.

In a project, you don’t have to worry about whether the database is already set up, just use it.

But the front-end database is not good, must first consider the database has been established, the initial data has not been added, and then can start the regular operation.

So the first step is to encapsulate the initialization part of the database.

Let’s create a help.js file and write an ES6 class in it.


** * dbFlag: ** * dbFlag: ** * dbFlag: ** * dbFlag: "' / / database identifier, the difference between different database dbConfig: * * * {* * * * / / connect to the database dbName: 'the name of the database, * * * * ver:' the database version, * * *}, * * * stores: * * * * storeName: {{/ / object warehouse name * * * * * id: 'id', / / the name of the primary key index: * * * * * {/ / * * * * * * can not set the index name: Ture, // key: index name; Value: whether can repeat * * * * * * * * *}}}, * * * * * * init: (help) = > {} / / completely ready callback function * /
export default class IndexedDBHelp {
  constructor (info) {
    this.myIndexedDB = window.indexedDB || window.webkitIndexedDB || window.mozIndexedDB || window.msIndexedDB
    if (!this.myIndexedDB) {
      console.log('Your browser does not support IndexedDB')}// Database name and version
    this._info = {
      dbName: info.dbConfig.dbName,
      ver: info.dbConfig.ver
    }
    // Record the object connected to the database, of type IDBDatabase, not immediately available because open is an asynchronous operation.
    this._db = null

    // Record the status of the warehouse. New: after a new library or version upgrade; Old: There is an object repository.
    this._storeState = 'pending'

    /** * Register a callback event. * * If the component is not ready to read or write indexedDB, * * can register an event and call back when it is ready. * /
    this._regCallback = []

    // Open the database asynchronously, which takes a few milliseconds.
    this.dbRequest = this.myIndexedDB.open(this._info.dbName, this._info.ver)

    For the first time or during a version upgrade, create a table based on the configuration information
    this.dbRequest.onupgradeneeded = (event) = > {
      this._storeState = 'new'
      const db = event.target.result
      Log (' [2] New or upgrade database onupgradeneneeded -- ', DB)

      for (const key in info.stores) {
        const store = info.stores[key]
        if (db.objectStoreNames.contains(key)) {
          // There is already a warehouse, verify whether to delete the original warehouse
          if (store.isClear) {
            // Delete the original object store without saving the data
            db.deleteObjectStore(key)
            // Create a new object repository
            const objectStore = db.createObjectStore(key, { keyPath: store.id })
            // Create an index
            for (const key2 in store.index) {
              const unique = store.index[key2]
              objectStore.createIndex(key2, key2, { unique: unique })
            }
          }
        } else {
          // No object repository is created
          const objectStore = db.createObjectStore(key, { keyPath: store.id }) /* autoIncrement: true */
          // Create an index
          for (const key2 in store.index) {
            const unique = store.index[key2]
            objectStore.createIndex(key2, key2, { unique: unique })
          }
        }
      }
    }

    // The database is opened successfully, and the connection object is recorded
    this.dbRequest.onsuccess = (event) = > {
      this._db = event.target.result // dbRequest.result
      // console.log(' [1] successfully open database onsuccess -- ', this._db)
      // Change the status
      if (this._storeState === 'pending') {
        this._storeState = 'old'
      }
      // Invoke the initialized callback
      if (typeof info.init === 'function') {
        info.init(this)}// Invoke the component registered callback
      this._regCallback.forEach(fn= > {
        if (typeof fn === 'function') {
          fn()
        }
      })
    }

    // Process the error message
    this.dbRequest.onerror = (event) = > {
      / / error
      console.log('Error opening database:', event.target.error)
    }
  }
  // Mount other operations, described later...
}
Copy the code

There are several main things to do here:

  • Determine whether the browser supports indexedDB
  • Open the database
  • Setting up the object Repository
  • Save the connection object for later use

In addition, the use of jsDoc parameter description, sometimes can appear prompt, even if not prompt, but also can explain the role, to avoid a few days of their own can not remember how to use the parameter.

Mount the transaction

Once we have the database connection object, we can (and must) start a transaction before we can do anything else.

So we need to encapsulate the transaction first, so why encapsulate the transaction separately?

This is because it is possible to open a transaction and then pass the transaction instance for continuous operation, although this is not the case too often, but it feels like it should be supported.

begin-tran.js

/** * starts a read-write transaction *@param {*} Help indexedDB help *@param {Array} StoreName an array of strings, the name of the object store *@param {string} Type readwrite: reads and writes transactions. Readonly: read-only transaction; Versionchange: Allows you to perform any operation, including deleting and creating object stores and indexes. *@returns Read and write transactions */
const beginTran = (help, storeName, type = 'readwrite') = > {
  return new Promise((resolve, reject) = > {
    const _tran = () = > {
      const tranRequest = help._db.transaction(storeName, type)
      tranRequest.onerror = (event) = > {
        console.log(type + 'Transaction error:', event.target.error)
        reject(`${type}Transaction error:${event.target.error}`)

      }
      resolve(tranRequest)
      tranRequest.oncomplete = (event) = > {
        // console.log('beginReadonly transaction completed: ', window.performance. Now ())}}if (help._db) {
      _tran() // Execute a transaction
    } else {
      // Register a callback event
      help._regCallback.push(() = > _tran())
    }
  })
}
export default beginTran
Copy the code
  • Support for multiple object repositories

StoreName is an array of strings, so you can start transactions against multiple object repositories at the same time, and then use TranRequest.objectStore (storeName) to get specific object repositories.

  • Mount to help

Since it is written in a separate JS file, we also need to import the js file in help and attach it to help to implement the help.xxx call form, so that we can get help without importing various functions.

import _beginTran from './begin-tran.js' / / transaction. Additional code for help// Read-write transactions
  beginWrite (storeName) {
    return _beginTran(this, storeName, 'readwrite')}// Read-only transaction
  beginReadonly (storeName) {
    return _beginTran(this, storeName, 'readonly')}Copy the code

Is there a sense of “loop call”? Js can be so free. And then we have to be very careful when we write code, because if we’re not careful, it’s easy to write an endless loop.

Mount add delete change check

With the transaction ready, we can proceed to the next step.

Create a js file to add objects to:

addModel.js

import _vueToObject from './_toObject.js'

/** * Add object *@param { IndexedDBHelp } Help accesses an instance of the database *@param { string } StoreName Warehouse name (table name) *@param { Object } The model object *@param { IDBTransaction } If tranRequest uses transactions, it needs to pass the connection object * that was created when the transaction was started@returns ID of the new object */
export default function addModel (help, storeName, model, tranRequest = null) {
  // Take the prototype of the object to save reactive
  const _model = _vueToObject(model)
  // Define an instance of Promise
  return new Promise((resolve, reject) = > {
    // Define a function that is easy to call
    const _add = (__tran) = > {
      __tran
        .objectStore(storeName) / / for the store
        .add(_model) // Add objects
        .onsuccess = (event) = > { // Successful callback
          resolve(event.target.result) // Returns the object ID}}if (tranRequest === null) {
      help.beginWrite([storeName]).then((tran) = > {
        // open a transaction yourself
        _add(tran)
      })
    } else {
      // Use the transaction passed in
      _add(tranRequest)
    }
  })
}

Copy the code

First use Promise to encapsulate the default callback pattern, and then pass in a transaction to enable the ability to add consecutive transactions.

If you don’t pass a transaction, you start a transaction internally, which makes it easier to add a single object.

Then import the js file in help and set a function:

import _addModel from './model-add.js' // Add an object

  /** * Add an object *@param {string} StoreName Warehouse name *@param {object} Model The object to be added *@param {*} TranRequest transaction, can be NULL *@returns * /
  addModel (storeName, model, tranRequest = null) {
    return _addModel(this, storeName, model, tranRequest = null)}Copy the code

So you can mount it. The code is divided into multiple JS files for easy maintenance and extension.

The code for modifying, deleting, and getting is similar, and I won’t list them all.

use

Look at the above code and you may feel dizzy, so complicated? Didn’t it say it was easy?

That’s right, encapsulating the complexity, leaving simple calls. So how do you use it?

Prepare the information for creating the database

Let’s start by defining an object that holds all the information we need

const dbInfo = {
  dbFlag: 'project-meta-db'.// Database identifier to distinguish different databases. If there is only one in the project, you do not need to add this identifier
  dbConfig: {
    dbName: 'nf-project-meta'.// Database name
    ver: 2
  },
  stores: { // Table in database (object repository)
    moduleMeta: { // Module meta {button, list, pagination, query, form several}
      id: 'moduleId'.index: {},
      isClear: false
    },
    menuMeta: { // Meta for menus
      id: 'id'.index: {},
      isClear: false
    },
    serviceMeta: { // Backend API meta, online demo.
      id: 'moduleId'.index: {},
      isClear: false}},init: (help) = > {
    // The database is set up
    console.log('Inti event trigger: indexedDB setup completed ---- help:', help)
  }
}
Copy the code
  • dbFlag

DbFlag is used to distinguish between databases that use multiple indexedDB databases in a project.

  • stores

Description of an object warehouse, used in the onupgradenneeded event to create an object warehouse based on this information.

  • init

IndexedDB is ready for subsequent callbacks.

Direct use of

import IndexedDB from '.. /.. /.. /packages/nf-ws-indexeddb/help.js'

// Create an instance
const help = new IndexedDB(dbInfo)

// Add tests for objects
const add = () = > {
  // Define an object
  const model = {
    id: new Date().valueOf(),
    name: 'test'
  }
  / / add
  help.addModel('menuMeta', model).then((res) = > {
    console.log('Added successfully! ', res) // Returns the object ID})}Copy the code
  • Define a database description
  • Generate an instance of help
  • Add objects using help.addModel

Make a shell for a baby

A review of the code reveals a few minor problems:

  • Do I need to instantiate a help every time I use it? Isn’t that a waste?
  • The object repository name also needs to be a string, what if I make a mistake?
  • Help.xxxmodel (XXX, XXX, XXX) is there some trouble?

So we need a case to make it easier to use.


import IndexedDB from './help.js'

/**
 * 把 indexedDB 的help 做成插件的形式
 */
export default {
  _indexedDBFlag: Symbol('nf-indexedDB-help'),
  _help: {}, // Access an instance of the database
  _store: {}, Foo. AddModel (obj)
  
  createHelp (info) {
    let indexedDBFlag = this._indexedDBFlag
    if (typeof info.dbFlag === 'string') {
      indexedDBFlag = Symbol.for(info.dbFlag)
    } else if (typeof info.dbFlag === 'symbol') {
      indexedDBFlag = info.dbFlag
    }
    // Connect to the database to get the instance.
    const help = new IndexedDB(info)
    // Store static objects so that multiple different instances can be saved.
    this._help[indexedDBFlag] = help // help
    this._store[indexedDBFlag] = {} // The warehouse changes objects

    // Change the repository to an object instead of a string of repository names
    for (const key in info.stores) {
      this._store[indexedDBFlag][key] = {
        put: (obj) = > {
          let _id = obj
          if (typeof obj === 'object') {
            _id = obj[info.stores[key].id]
          }
          return help.updateModel(key, obj, _id)
        },
        del: (obj) = > {
          let _id = obj
          if (typeof obj === 'object') {
            _id = obj[info.stores[key].id]
          }
          return help.deleteModel(key, _id)
        },
        add: (obj) = > help.addModel(key, obj),
        get: (id = null) = > help.getModel(key, id)
      }
    }
  },

  // Get the database instance in the static object
  useDBHelp (_dbFlag) {
    let flag = this._indexedDBFlag
    if (typeof _dbFlag === 'string') {
      flag = Symbol.for(_dbFlag)
    } else if (typeof _dbFlag === 'symbol') {
      flag = _dbFlag
    }
    return this._help[flag]
  },
  useStore (_dbFlag) {
    let flag = this._indexedDBFlag
    if (typeof _dbFlag === 'string') {
      flag = Symbol.for(_dbFlag)
    } else if (typeof _dbFlag === 'symbol') {
      flag = _dbFlag
    }
    return this._store[flag]
  }
}

Copy the code

First, this is a static object that can hold an instance of Help and achieve global access.

Previously, provide/inject was used to save, but found a little inconvenient and not necessary, so changed to static object.

Then, according to the information to build the table, create the warehouse object, the string warehouse name into the object form, which is much more convenient.

“UseDBHelp” is used to distinguish it from webSQL help.

This is what happens when you use it:


// Treat the warehouse as an "object"
const  { menuMeta }  = dbInstall.useStore(dbInfo.dbFlag)

// Add objects
const add = () = > {
  const t1 = window.performance.now()
  console.log('\n -- ready to add object -- : ', t1)
  const model = {
    id: new Date().valueOf(),
    name: 'the test -. '
  }
  menuMeta.add(model).then((res) = > {
    const t2 = window.performance.now()
    console.log('Added successfully! ', res, 'Time:', t2 - t1, '\n')})}Copy the code

In that case, it would be much more convenient. Object repository name. XXX (oo) will do, the code is much simpler.

Further nesting doll

Add, delete, modify, and query object.

Now that we have wrapped this step, we can go further and use js prototype to implement object add, delete, change and check.

// add function to model
for (const key in info.stores) {
  this._store[indexedDBFlag][key] = {
    createModel: (model) = > {
      function MyModel (_model) {
        for (const key in _model) {
          this[key] = _model[key]
        }
      }
      MyModel.prototype.add = function (tran = null) {
        return help.addModel(key, this, tran)
      }
      MyModel.prototype.save = function (tran = null) {
        const _id = this[info.stores[key].id]
        return help.setModel(key, this, _id, tran)
      }
      MyModel.prototype.load = function (tran = null) {
        return new Promise((resolve, reject) = > {
          / / set of Eva
          const _id = this[info.stores[key].id]
          help.getModel(key, _id, tran).then((res) = > {
            Object.assign(this, res)
            resolve(res)
          })
        })
      }
      MyModel.prototype.del = function (tran = null) {
        const _id = this[info.stores[key].id]
        return help.delModel(key, _id, tran)
      }
      const re = new MyModel(model)
      return reactive(re)
    }
  }
}
Copy the code

We first add a “createModel” function to the object repository, then use the prototype to attach the add, delete, change, and query functions, and finally return a new instance.

Usage:


// Create an instance of the object repository, reactive
const testModel = menuMeta.createModel({
  id: 12345.name: 'Object save itself'
})
 
// Save the object directly
const mSave = () = > {
  testModel.name = 'Object save itself' + window.performance.now()
  testModel.save().then((res) = > {
    // Save})}Copy the code

Because of reactive, it’s inherently responsive. Does that sound like a “hyperemic entity”?

It is recommended not to change the ID value, although it can be changed, but it is always uncomfortable to change it.

Unified “exit”

Although several routine operations have been carried out with help, the exit is still not unified enough. Is it convenient to have only one exit like Vue? So let’s also unify:

storage.js

// Introduce various functions to make it easy to make NPM packages
/ / with indexedDB parts
import dbHelp from './nf-ws-indexeddb/help.js'
import dbInstall from './nf-ws-indexeddb/install.js'

/ / with indexedDB parts
const dbCreateHelp = (info) = > dbInstall.createHelp(info)
const useDBHelp = (_dbFlag) = > dbInstall.useDBHelp(_dbFlag)
const useStores = (_dbFlag) = > dbInstall.useStores(_dbFlag)

export {
  / / with indexedDB parts
  dbHelp, / / with indexedDB help
  dbCreateHelp, // Create an instance of help to initialize the Settings
  useDBHelp, // Get an instance of help from the component
  useStores // Add/delete/add/delete/add/delete
}
Copy the code

This also makes it easier for us to package and publish to NPM.

Use in VUE

With all the basic work done, there is only one question left, how to use it in Vue3?

We can imitate the way vuex is used by creating a JS file to achieve uniform Settings.

store-project/db.js

// Introduce indexedDB help
import { dbCreateHelp } from '.. /.. /packages/storage.js'

// Import database data
const db = {
  dbName: 'nf-project-meta'.ver: 5
}

/** * set */
export default function setup (callback) {
  const install = dbCreateHelp({
    // dbFlag: 'project-meta-db',
    dbConfig: db,
    stores: { // Table in the database
      moduleMeta: { // Module meta {button, list, pagination, query, form several}
        id: 'moduleId'.index: {},
        isClear: false
      },
      menuMeta: { // Meta for menus
        id: 'id'.index: {},
        isClear: false
      },
      serviceMeta: { // Backend API meta, online demo.
        id: 'moduleId'.index: {},
        isClear: false
      },
      testIndex: { // Test indexes and queries.
        id: 'moduleId'.index: {
          kind: false.type: false
        },
        isClear: false}},// Add initial data
    init (help) {
      if (typeof callback === 'function') {
        callback(help)
      }
    }
  })
  return install
}

Copy the code

It is then called in main.js, because this is where the code is first executed and the database is first created.


// Introduce indexedDB help
import dbHelp from './store-project/db.js'

dbHelp((help) = > {
  // indexedDB is ready
  console.log('Get indexedDB help from main', help)
})
Copy the code

You can also store an instance of help into a static object.

In fact, we used provide injection at first, but found that it was not suitable because it could not be read by inject in main.js, so that the operation of and state was not convenient.

So just put it in a static object, and you can access it anywhere.

There is no need to mount the App using Use.

Indexes and queries

Due to the limited space, here is not introduced, if you are interested, you can write a supplement.

The source code

Packaged front-end storage gitee.com/naturefw/nf…

The online demo

Naturefw. Gitee. IO/vite2 – vue3 -…

installation

yarn add nf-web-storage
Copy the code