IndexedDB是Indexed Database API的简称,是在浏览器中存储大量结构化数据(包括文件/ blob)的一种数据库,是对Web存储(存储少量数据)的一种补充,属于事务数据库系统。它的API使用索引来对这些数据进行高性能搜索,并且通过API可以方便保存和读取JavaScript对象。它的操作完全是异步进行的,通常要注册onerror或者onsuccess事件处理程序,监听操作的结果。



不同于MySql或Oracle这类用表来保存数据的数据库,IndexedDB采用的是使用对象保存数据。因此IndexedDB可以理解为一组在相同命名空间下的对象的集合。

IndexedDB相关API
  1. 数据库:IDBDatabase对象

数据库是一系列相关数据的容器。每个域名(严格的说,是协议 + 域名 + 端口)都可以新建任意多个数据库。IndexedDB 数据库有版本的概念。同一个时刻,只能有一个版本的数据库存在。如果要修改数据库结构(新增或删除表、索引或者主键),只能通过升级数据库版本完成。

  1. 对象仓库:IDBObjectStore 对象

每个数据库包含若干个对象仓库(object store)。它类似于关系型数据库的表格

  1. 索引:IDBIndex 对象

为了加速数据的检索,可以在对象仓库里面,为不同的属性建立索引。

  1. 事务:IDBTransaction 对象

数据记录的读写和删改,都要通过事务完成。事务对象提供errorabortcomplete三个事件,用来监听操作结果。

  1. 操作请求:IDBRequest 对象

  2. 指针:IDBCursor 对象

  3. 主键集合:IDBKeyRange 对象

操作IndexedDB的基本模式
  1. 打开一个数据库

  2. 在数据库中创建对象存储

  3. 启动事务并请求执行一些数据库操作,例如添加或检索数据

  4. 过侦听事件来等待操作完成

  5. 对结果做一些事情(可以在request object上找到)

API使用及流程详解
let req,db;
let version = 2
/* 
 * 【第一:创建数据库或升级数据库】
 * 发送打开'demo'数据库的请求,若不存在先创建,再打开,返回一个IDBOpenDBRequest请求实例
 * req解答:
 * 原型链: req.__proto__ = IDBOpenDBRequest;IDBOpenDBRequest.__proto__ = IDBRequest
 * IndexedDB中的大多数其他异步函数执行都是相同的操作-返回带有onerror或onsuccess的IDBRequest对象
 * 
 * version解答:
 * 数据库的版本,确定数据库模式-数据库中存储的对象及其结构.
 */ 
req = indexedDB.open('demo',version)
req.onerror = function(e){
	console.log('发生错误',e.target.errorCode)
}
// 成功
req.onsuccess = function(e){
  // 返回的是IDBDatabase数据库实例
	db = e.target.result
  /* 
   * 处理错误事件:
   * 错误事件是冒泡的。错误事件以生成错误的请求为目标,然后事件冒泡到事务中,最后到数据库对象。
   * 如果要避免向每个请求添加错误处理程序,则可以在数据库对象上添加单个错误处理程序。
   */ 
  db.onerror = function(event) {
  	console.error("Database error: " + event.target.errorCode);
	}
}

/*
 * 【第二:监听数据库升级】
 * 创建或更新数据库的版本将触发此事件
 * 是唯一可以更改数据库结构的地方。在其中可以创建和删除对象存储以及构建和删除索引。
 */ 
req.onupgradeneeded = function(e){
	db = e.target.result
  
 	/*
   * 【第三:创建表】
   * 为此数据库创建一个objectStore createObjectStore(存储名,参数对象)
   * 可以理解为:创建一张user的表,主键是id,若无合适主键,可配置自增主键{autoIncrement: true 
   */
  var objectStore 
  if(!db.objectStoreNames.contains('user')){
    // 不存在user表,则创建user表,主键为id
  	objectStore =db.createObjectStore("user", { keyPath: "id" });
  }
  
  /*
   * 【第四:创建索引】
   * (索引名称,索引所在的属性,索引配置对象)
   */
  objectStore.createIndex("id", "id", { unique: true });
  
  /*
   * 【第五:写入数据】
   *  通过IDBTransaction.objectStore(name)方法,拿到 IDBObjectStore 对象,
   *  再通过表格对象的add()方法,向表格写入一条记录
   *  写入过程是异步的,通过监听success/error获取写入结果
   */
  var addUserReq = db.transaction(['user'],'readwrite')
    .objectStore('user').add({id:1,name:'张三',age:18})
  addUserReq.onsuccess = function(){console.log('user表写入数据成功')}
  addUserReq.onerror = function(){console.log('user表写入数据失败')}
  
  /*
   *	【第六:读取数据】		
   * 	读取数据也是通过事务完成
   */
  var readTransaction  = db.transaction(['user'])
  var readUserObjStore = readTransaction.objectStore('user'); 
  // 读取主键为1的数据
  var readReq = readUserObjStore.get(1)
  readReq.onerror = function(){console.log('读取数据失败')}
  readReq.onsuccess = function(event){
  	if (request.result) {
        console.log('Name: ' + request.result.name);
        console.log('Age: ' + request.result.age);
      } else {
        console.log('未获得数据记录');
      }
  }
  
  /*
   *	【第七:更新数据】
   */
   var putUserReq = db.transaction(['user'],'readwrite')
    .objectStore('user').put({id:1,name:'张三',age:20})
  putUserReq.onsuccess = function(){console.log('user表更新数据成功')}
  putUserReq.onerror = function(){console.log('user表更新数据失败')}
}

	/*
 	 * 	【第八:删除数据】
 	 */
   var deleteUserReq = db.transaction(['user'],'readwrite')
    .objectStore('user').delete(1)
  deleteUserReq.onsuccess = function(){console.log('user表删除数据成功')}

	/*
   *  【第九:遍历表格数据】
   *  使用针对对象IDBCursor
   */
	 var cursorStore = db.transaction('user').objectStore('user')
   cursorStore.openCursor().onsuccess = function(event){
   		var cursor = event.target.result
      if(cursor){
      	console.log('id:'+cursor.key)
        console.log('name:'+cursor.value.name)
        console.log('age:'+cursor.value.age)
        cursor.continue()
      }else{console.log('无更多数据')}
   }
   /*
    * 【第十:自定义索引】
    */
	 objectStore.createIndex('name', 'name', { unique: false });
	 var transaction = db.transaction(['user'], 'readonly');
	 var store = transaction.objectStore('user');
	 var index = store.index('name');
	 var request = index.get('张三');
	 request.onsuccess = function (e) {
  	  var result = e.target.result;
      console.log('result',result)	
   }
简易封装IndexedDB
import isString from 'lodash/isString'

/**
 * @param dbName 数据库名称
 * @param version 数据库版本 不传默认为1
 * @param primary 数据库表主键
 * @param indexList Array 数据库表的字段以及字段的配置,每项为Object,结构为{ name, keyPath, options }
 */
class WebDB {
  constructor({dbName, version, primary, indexList}) {
    this.db = null
    this.objectStore = null
    this.request = null
    this.primary = primary
    this.indexList = indexList
    this.version = version
    this.intVersion = parseInt(version.replace(/\./g, ''))
    this.dbName = dbName
    try {
      this.open(dbName, this.intVersion)
    } catch (e) {
      throw e
    }
  }

  open (dbName, version) {
    const indexedDB = window.indexedDB || window.webkitIndexedDB || window.mozIndexedDB || window.msIndexedDB;
    if (!indexedDB) {
      console.error('你的浏览器不支持IndexedDB')
    }
    this.request = indexedDB.open(dbName, version)
    this.request.onsuccess = this.openSuccess.bind(this)
    this.request.onerror = this.openError.bind(this)
    this.request.onupgradeneeded = this.onupgradeneeded.bind(this)
  }

  onupgradeneeded (event) {
    console.log('onupgradeneeded success!')
    this.db = event.target.result
    const names = this.db.objectStoreNames
    if (names.length) {
      for (let i = 0; i< names.length; i++) {
        if (this.compareVersion(this.version, names[i]) !== 0) {
          this.db.deleteObjectStore(names[i])
        }
      }
    }
    if (!names.contains(this.version)) {
      this.objectStore = this.db.createObjectStore(this.version, { keyPath: this.primary })
      this.indexList.forEach(index => {
        const { name, keyPath, options } = index
        this.objectStore.createIndex(name, keyPath, options)
      })
    }
  }

  openSuccess (event) {
    console.log('openSuccess success!')
    this.db = event.target.result
  }

  openError (event) {
    console.error('数据库打开报错', event)
    // 重新链接数据库
    if (event.type === 'error' && event.target.error.name === 'VersionError') {
      indexedDB.deleteDatabase(this.dbName);
      this.open(this.dbName, this.intVersion)
    }
  }

  compareVersion (v1, v2) {
    if (!v1 || !v2 || !isString(v1) || !isString(v2)) {
      throw '版本参数错误'
    }
    const v1Arr = v1.split('.')
    const v2Arr = v2.split('.')
    if (v1 === v2) {
      return 0
    }
    if (v1Arr.length === v2Arr.length) {
      for (let i = 0; i< v1Arr.length; i++) {
        if (+v1Arr[i] > +v2Arr[i]) {
          return 1
        } else if (+v1Arr[i] === +v2Arr[i]) {
          continue
        } else {
          return -1
        }
      }
    }
    throw '版本参数错误'
  }

  /**
   * 添加记录
   * @param record 结构与indexList 定下的index字段相呼应
   * @return Promise
   */
  add (record) {
    if (!record.key) throw '需要添加的key为必传字段!'
    return new Promise((resolve, reject) => {
      let request
      try {
        request = this.db.transaction([this.version], 'readwrite').objectStore(this.version).add(record)
        request.onsuccess = function (event) {
          resolve(event)
        }

        request.onerror = function (event) {
          console.error(`${record.key},数据写入失败`)
          reject(event)
        }
      } catch (e) {
        reject(e)
      }
    })
  }

  /**
   * 读取记录
   * @param key 主键的值
   * @return Promise
   */
  get (key) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        let request
        try {
          request = this.db.transaction([this.version]).objectStore(this.version).get(key)
          request.onerror = function (event) {
            console.error(`${key}, 数据读取失败!`)
            reject(event)
          }

          request.onsuccess = function (event) {
            if (request.result) {
              resolve(request.result)
            } else {
              reject(event)
            }
          }
        } catch (e) {
          reject(e)
        }
      }, 200)
    })
  }

  /**
   * 更新记录
   * @param record 结构与indexList 定下的index字段相呼应
   * @return Promise
   */
  update (record) {
    return new Promise((resolve, reject) => {
      // const request = this.db.transaction([this.version], 'readwrite').objectStore(this.version).put(record)
      let request
      try {
        request = this.db.transaction([this.version], 'readwrite').objectStore(this.version).put(record)
        request.onsuccess = function (event) {
          resolve(event)
        };

        request.onerror = function (event) {
          console.error(`${record.key},数据更新失败`)
          reject(event)
        }
      } catch (e) {
        reject(e)
      }
    })
  }

  /**
   * 删除记录
   * @param key 主键的值
   * @return Promise
   */
  remove (key) {
    return new Promise((resolve, reject) => {
      const request = this.db.transaction([this.version], 'readwrite').objectStore(this.version).delete(key);

      request.onsuccess = function (event) {
        resolve(event)
      }
      request.onerror = function (event) {
        console.error(`${key}, 数据删除失败`)
        reject(event)
      }
    })
  }
}

export default WebDB
总结

至此我们学习的IndexedDB数据库的使用,关于使用场景,可以在做离线缓存等场景中使用,而我当时在做chrome扩展程序热更新方案时候使用到,用于缓存热更新业务代码到本地提升资源效率。