前端大容量存储IndexedDB之Dexie.js

8,489 阅读1分钟

这是我参与11月更文挑战的第1天,活动详情查看:2021最后一次更文挑战

1, 前言

随着浏览器的功能不断增强,越来越多的网站开始考虑将大量的数据存储在客户端,相比后端接口,获取数据更快一些。现有的浏览器存储方案都不适合存储大量的数据。Cookie的大小不超过4KB,而且每次请求都会发送到服务器,LocalStorage在2.5~10MB直接,浏览器不同,存储的大小还不一样,而且不提供搜索功能,也不能建立自定义索引,webSQL大家可以课外了解一下,因为Web SQL Database规范已经被废弃,官方文档也解释的很清楚,webSQL规范底层采用SQLite的SQL方言,而作为一个标准,这是不可接受的,每个浏览器都有自己的实现以后就很能统一标准了,就像IE一样。最后,也是最重要的一个客户端存储大量数据的方案:IndexedDB。

2, webSQL小结

(1) 创建一个数据库

var db = openDatabase('person', 1, 'person', 0)

第一个参数: 数据库的名称
第二个参数: 版本号
第三个参数: 备注
第三个参数: 大小,默认是5M

(2)创建一个表

db.transaction(tx => { 
   tx.executeSql('create table if not exists student(id unique, name)') 
})

(3) 插入两条数据

db.transaction(tx => { 
   tx.executeSql('insert into student (id, name) value(?, ?)', [1, '张三']);         
   tx.executeSql('insert into student (id, name) value (?, ?)', [2, '李四']);
})

(4)查询数据

db.transaction(tx => {
   tx.executeSql('select * from student', [], (tx, res) => {
      let rows = res.rows;
      let len = rows.length
      for (var i=0; i< len, i++) {
         console.log(rows.item(i))
      }
   })
})

总结

  • webSQL标准不再更新,关系型数据库,底层 sqlite
  • chrome中容量5M, 支持同域名不同页面共享
  • 版本参数用于控制,如果打开版本与现有版本不一致,则报错

3, IndexedDB存储

IndexedDB不属于关系型数据库,不支持SQL查询语句,更接近NoSQL数据库,是一个基于事务操作的key-value型前端数据库。它的大部分API都是异步的。

  • 键值对存储

    IndexedDB内部采用对象仓库存放数据,所有类型的数据都可以直接存放,包括JavaScript对象,每一个数据记录都有对应的主键,主键是独一无二的,不能重复

  • 异步

    IndexedDB操作时不会锁死浏览器,用户依然可以进行其他操作,它所有的API都是异步操作,异步设计时为零防止大量数据的读写拖慢网页的表现。

  • 支持事务(transaction)

    支持事务就意味着:在操作(增删改查)的过程中,只要有异步失败了,整个事务就都取消,数据库回滚到事务发生之前的状态,不存在只改写一部分数据的情况。

  • 同源限制

    每一个数据库对应创建它的域名,网页只能访问自身域名下的数据库,而不能访问跨域名的数据库

  • 存储空间大

    IndexedDB的存储空间一般来说不少于250M,甚至没有上限

1,IndexedDB常用概念

IndexedDB是一个比较复杂的API,它把不同的实体抽象成一个个对象接口。

*数据库     IDBDatabase 对象
*对象仓库   IDBObjectStore 对象, 类似于关系型数据库中的表
*索引      IDBIndex 对象
*事务      IDBTransaction 对象
*操作请求   IDBRequest 对象
*指针      IDBCursor 对象
*主键集合   IDBKeyRange 对象

注意: IndexedDB数据库有版本的概念,同一时刻,只能有一个版本的数据库存在,如果要修改数据库结构,只能通过升级数据库版本完成,每一个数据库包含若干个对象仓库,也可以理解为:每一个数据库可以创建多个表, 为了加速数据的检索,可以在对象仓库里面建立索引。

注意:事务

数据记录的读写和删改,都要通过事务完成,事务对象提供了三个回调事件

onerror onsuccess onupgradeneeded

2, 操作流程

(1) 打开数据库

使用IndexedDB的第一步就是打开数据库,使用indexedDB.open()方法

var IDBRequest = window.indexedDB.open(databaseName, version)

第一个参数: 数据库名称, 如果数据库不存在就会创建

第二个参数:表示数据库的版本,如果省略,默认为当前版本,默认为:1

结果indexedDB.open()方法返回一个IDBRequest对象,通过三种事件error, success, upgradeneeded, 处理打开的数据库。

  • onerror 事件表示打开数据库失败

    IDBRequeset.onerror = funciton(event) {
       console.log('数据库打开失败')
    }
    
  • onsuccess事件表示打开数据库成功

    var db
    IDBRequeset.onsuccess = function(event) {
       db = IDBRequeset.result
       console.log('数据库打开成功')
    }
    
  • upgrade needed事件表示:如果指定的版本号,大于数据库的实际版本号,就会发生数据库升级事件upgradeneeded

    var db;
    IDBRequeset.onupgradeneeded = function(event) {
        db = event.target.result
    }
    

(2) 新建数据库

新建数据库和打开数据库是同一个操作, 如果指定打开的数据库不存在,就会新建,因为是新建,这时版本号从无到有,是一个升级过程,所以会触发onupgradeneeded事件,后续的操作都在这个回调里面执行

IDBRequest.onupgradeneeded = function(event) {
  db = event.target.result // 拿到数据库实例
  // 判断表格是否存在
  var objectStore;
  if (!db.objectStoreNames.contains('person')) {
    objectStore = db.createOjbectStore('person', { keyPath: 'id'})
  }
}

没有id,也可以让IndexedDB自动设置主键

objectStore = db.createOjbectStore('person', { autoIncrement: true })

下一步就可以直接创建索引

objectStore.createIndex('name', 'name', { unique: false})

(3)新增数据

新增数据指的是想对象仓库中写入数据记录,需要通过事务完成

function add() {
   var request = db.transaction(['person'], 'readwrite')
           .objectStore('person')
           .add({ id: 1, name: 'zlm', age: 24, email: '2396307772@qq.com'});
       request.onsuccess = function (event) {
            console.log('写入数据成功', event)
        };
       request.onerror = function (event) {
            console.log('写入数据失败',event)
       }
  }

解读: 添加一个数据,需要新建一个事务,通过 transaction()方法。 第一个参数:指定表格名称, 第二个参数:操作模式,只读或者读写。新建事务完成之后,通过objectStore()方法拿到对象仓库,也就是表格对象,一个参数: 表格名称。最后在通过表格对象的add()方法, 向表格中写入一条记录

(4) 读取数据

读取数据也是通过事务完成

function read() {
       var transaction = db.transaction['person'];
       var objectStore = transaction.objectStore('person')
       var request = objectStore.get(1)
           request.onerror = function (event) {
               console.log('读取事务失败',event)
           }
           request.onsuccess = function (event) {
               if (request.result) {
                   console.log('Name', + request.result.name)
                   console.log('Age', + request.result.age)
                   console.log('Email', + request.result.email)
               } else {
                   console.log('未获取数据记录')
               }
               console.log('读取成功事件', event)
           }
    }

(5) 遍历数据

遍历表格中的所有数据,使用指针对象IDBCursor

function readAll() {
     var objectStore = db.transaction('person').objectStore('person')
         objectStore.opneCursor().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)
             console.log('Email: ' + cursor.value.name)
             cursor.continue()
           } else {
             console.log('没有获取到更多的数据')
           }
         }
    } 

(6)更新数据

更新数据要使用IDBObject.put()方法

function update() {
  var request = db.transaction(['person'], 'readwrite')
           .objectStore('person')
           .put({ id: 1, name: '张四', age: 32, email: '239630772@qq.com'})
      request.onsuccess = function (event) {
           console.log('更新数据成功', event)
      }
      request.onerror = function (event) {
           console.log('没有获取到更多的数据', event)
      }
  }

(7) 删除数据

使用IDBObjectStore.delete()方法

function remove() {
        var request = db.transaction(['person'])
                        .objectStore('person')
                        .delete(1)
            request.onsuccess = function (event) {
                console.log('删除数据成功', event)
            } 
    }

(8)索引

索引的意义在于:可以让你搜索任意字段,通过任意字段拿到数据记录,如果不建立索引,只能通过搜索主键

  • 新建表格的时候,对name字段添加索引

    objectStore.createIndex('name', 'name', {unique: false})
    
  • 通过name找到对应的数据记录

    var transaction = db.transaction(['person'], 'readonly')
    var store = transaction.objectStore('person')
    var index = store.index('name')
    var request = index.get('zlm')
    request.onsuccess = function(event) {
       var result = event.target.result
       if(result) {
           console.log('获取到数据')
       } else {
           console.log('没有获取数据')
       }
    }
    

下面是自己的测试Demo.js

/**
 * IDBDatabase 数据库
 * IDBObjectStore 对象仓库
 * IDBIndex 索引
 * IDBTransaction 事务
 * IDBRequest 操作请求
 * IDBCursor 指针
 * IDBKeyRang 主键集合
 */
// ! 打开数据库
var db;
var request =  window.indexedDB.open('myIndexDB');
    request.onerror =  function (event) {
        console.log('打开数据库报错', event)
    }
    request.onsuccess = function (event) {
        db = request.result
        console.log('数据库打开成功', event)
    }
    // 如果制定的版本,大于数据库的实际版本号,机会发生数据库升级事件
    request.onupgradeneeded = function (event) {
        db = event.target.result
        console.log('数据库升级事件')
        var objectStore;
        if (!db.objectStoreName.contains('person')) {
            objectStore = db.createObjectStore('person', { keyPath: 'id'})
            objectStore.createIndex('name', 'name', { unique: true})
            objectStore.createIndex('email', 'email', { unique: true})
        }
        console.log(objectStore)
    }
console.log(db)

// 新增数据
function add() {
        var request = db.transaction(['person'], 'readwrite')
                      .objectStore('person')
                      .add({ id: 1, name: '张三', age: 24, email: 'xx@xx.x.cn'});
            request.onsuccess = function (event) {
                console.log('写入数据成功', event)
            };
            request.onerror = function (event) {
                console.log('写入数据失败',event)
            }
    }

// 读取数据
    function read() {
       var transaction = db.transaction['person'];
       var objectStore = transaction.objectStore('person')
       var request = objectStore.get(1)
           request.onerror = function (event) {
               console.log('读取事务失败',event)
           }
           request.onsuccess = function (event) {
               if (request.result) {
                   console.log('Name', + request.result.name)
               } else {
                   console.log('未获取数据记录')
               }
               console.log('读取成功事件', event)
           }
    }

    // 遍历数据表格中所有的数据,要使用指针对象IDBCursor
    function readAll() {
        var objectStore = db.transaction('person').objectStore('person')
            objectStore.opneCursor().onsuccess = function(event) {
                 var cursor = event.target.result
                 if (cursor) {
                     console.log('ID: ' + cursor.key)
                     console.log("Name: " + cursor.value.name)
                     cursor.continue()
                 } else {
                     console.log('没有获取到更多的数据')
                 }
            }
    } 

// 更新数据
    function update() {
        var request = db.transaction(['person'], 'readwrite')
                        .objectStore('person')
                        .put({ id: 1, name: '张四', age: 32, email: 'xx@qq.com'})
            request.onsuccess = function (event) {
                console.log('更新数据成功', event)
            }
            request.onerror = function (event) {
                console.log('没有获取到更多的数据', event)
            }
    }

// 删除数据
    function remove() {
        var request = db.transaction(['person'])
                        .objectStore('person')
                        .delete(1)
            request.onsuccess = function (event) {
                console.log('删除数据成功', event)
            } 
    }

4, 实战操作

神游一番之后,发现indexedDB很多人进行过封装,对比一番发现:

Dexie.js 对IndexedDB的封装,语法简单,可以快速方便的编写代码

总结:dexie.js是一个对浏览器indexedDB的包装库,让我们操作indexedDB更方便。

(1) 为什么要使用dexie.js

  • 原生的IndexedDB操作都是在回调中进行的
  • 原生所有的操作都需要不断的创建事务,判断表和索引的存在
  • 原生不支持批量操作
  • 原生的错误需要在每一个失败回调中处理

而对比dexie.js, 支持链式调用,索引定义方便,而且支持多值索引和复合索引,而且文档完善

dexie官网文档

(1)安装

npm  install dexie

或者

<script src="https://unpkg.com/dexie@latest/dist/dexie.js"></script>

(2)使用

步骤一:获取一个数据库实例

import Dexie from 'dexie';
var db = new Dexie('数据库名称')

步骤二: 定义表结构

db.version(1).stores({
    users: "++id, name, age, emial",
    students: "++id, &username",
    books: "id, author, name, *categories"
})

注意:1 如果属性前面有++符合, 说明是自增的主键

注意:2 如果字段前面有&符合,说明是唯一索引

注意:3 如果字段前面有*符合,说明该字段是多值索引

符合描述
++自动递增主键
&唯一主键
*多值索引
+复合索引

(3) 查询

所有where 子句运算符都可用于查询多条目索引对象,

  • above() 返回指定字段的值大于某个值的所有记录

  • aboveOrEqual() 返回指定字段值大于等于某个值的所有记录

  • anyOf() 返回数据集中包含在给定数组中的记录

  • below() 返回指定字段的值小于某个值的所有记录

  • belowOrEqual() 返回指定字段值小于等于某个值的所有记录

  • between() 返回介于两者之间的记录

  • equals() 等于

    详细的大家可以参考官网文档

    Dexie官网API文档

下面是自己测试的Demo.html

<!doctype html>
<html>
  <head>
      <script src="https://unpkg.com/dexie@latest/dist/dexie.js"></script>
      <script>
          // 创建数据库病拿到实例化对象
        async function openIDB() {
            var db = new Dexie("MyDatabase");
                db.version(1).stores({
                    tasks: '++id, name, age, email'
                });
                // 增加数据
                db.tasks.add({
                    name: 'zlm',
                    age: 22,
                    email: 'xxx@xx.com.cn'
                })
                db.tasks.add({
                    name: '张三',
                    age: 18,
                    email: 'xxxx@xx.com.cn'
                })
                // 修改数据
                db.tasks.put({
                    id: 2,
                    name: '李四',
                    age: 100,
                    email: ''
                })
                // 查询数据
                console.log('查询第一条数据')
                const oldTask = await db.tasks.get(1)
                console.log(oldTask)
                // 高级查询,查询所有ID大于0的
                const allTask = await db.tasks.where('id').above(0).toArray()
                console.log('查询所有的数据')
                console.log(allTask)
                // 批量获取数据
                const bulkgetData = await db.tasks.bulkGet([1,2,3,4])
                console.log('批量数据')
                console.log(bulkgetData)
                // db.tasks.clear()
        }
        openIDB()  
      </script>
  </head>
</html>