前端技术分享之Dexie.js

3,518 阅读7分钟

前言

大家好,今天给大家分享一个好用的第三方库,它叫 dexie.js

dexie.js是什么

dexie.js 是一个对浏览器 indexexDB 的包装库,使得我们可以更方便地操作 indexedDB。

dexie.js 官网

传送门

为什么使用dexie.js

由于原生 indexedDB 具有几下缺点

  • 原生所有操作都是在回调中进行的
  • 原生所有操作都需要不断地创建事务,判断表和索引的存在性
  • 原生为表建立索引很繁琐
  • 原生查询支持的较为简单,复杂的查询需要自己去实现
  • 原生不支持批量操作
  • 原生的错误需要在每个失败回调中接收处理

基于以上原因,出现了很多对原生接口的包装,而相比于其它包装库,dexie.js 具有以下明显的优点:

  • 几乎所有接口都返回 promise,即符合 indexedDB 异步操作的特性,对开发者又直观友好,可以使用 promise 链,错误可以在 catch 中统一处理,且有丰富的错误类型返回。

  • 即支持与原生一致的接口,比如 open、get、put、add、delete、transcation 等等,又支持扩展的非常丰富的更加便捷的接口,如 db.storeName.get。

  • 类似于后端数据库的高级查询,并且支持链式调用。

  • 更丰富的索引定义,建立索引变得非常简单,并且支持多值索引和复合索引

  • 接近原生的性能。

使用

获取数据库实例

获取一个数据库实例

const db = new Dexie(dbname);
  • dbname:数据库的名称

这里只是获得数据库实例,与传入的数据库是否已经存在没有关系,如果已经存在,就会返回已经存在的数据库的一个实例,如果不存在,就会新建一个数据库,并返回该数据库的一个实例。

定义数据库结构

db.version(lastVersion).stores(
  {
  	localVersions: 'matadataid, content, lastversionid, date, time',
    users: "++id, name, &username, *email, address.city",
    relations: "++rid, userId1, userId2, [userId1+userId2], relation",
    books: 'id, author, name, *categories'
  }
);
  • lastVersion : 当前数据库最新版本,只有需要修改数据库结构时才更改这个值。

由于 dexiejs 需要兼容 IE 的一个 BUG,所以在实际建库的时候版本号都会乘以 10,如果这里 lastVersion 传 0.1,实际建的库的版本就是 1

  • 上例中的 localVersions,users,relations 都是数据库要包含的 objectStore 的名称,而他们的值则是要定义的索引,如果某个字段不需要索引,则不要写入这个索引列表,另外,如果某个字段存储的是图片数据(imageData),视频(arrayBuffer),或者特别大的字符串,不建议加入索引列表。

  • 可以定义四种索引:

    • 主键(自增):索引列表的第一个总会被当做主键,如上例中的 matadataid,id,rid,如果主键前有++ 符号,说明这个字段是自增的。

    • 唯一索引。如果某个索引的字段的值在所有记录中是唯一的,那么可以在它前面加& 号,比如上例中 users 仓库中的 username 字段。

    • 多值索引。 如果某个字段具有多个值,那么可以在它前面加*号将其设置为多值索引,如上例中的 books 仓库中的 categories 字段,用户可以根据它多个值的任何一个值来查询它,如:

db.books.put({
  id: 1,
  name: 'Under the Dome',
  author: 'Stephen King',
  categories: ['sci-fi', 'thriller']
});

这里面的 categores 是个数组,有多个值,那么我们就可以将其设置为多值索引

然后我们查询时便可以用其中一个值为查询条件去查询:

function getSciFiBooks() {
  return db.books
    .where('categories').equals('sci-fi')
    .toArray ();
}

这里便会查询到所有类型为 sci-fi 的书籍,即使这些书还可能同时属于其他分类。

复合索引。如果某个索引是基于多个键路径的,就可以将其设置为复合索引,格式为[A+B],如上例中 relations 中的[userId1+userId2] 索引。下面是一个例子:

// Store relations
    await db.relations.put({
        rid: 1,
        userId1: '1',
        userId2: '2'
    });

    // Query relations:
    const fooBar = await db.relations.where({
      userId1: '1',
      userId2: '2'
    }).first();

当你定义了复合索引后,就可以在 where 查询子句中传入一个复合条件对象,该示例就将查询出 userId1 为 1,userId2 为 2 的记录,但同时,你也可以只以其中一个字段为索引条件进行查询:

db.relations
  .where('userId1')
  .equals("1")
  .toArray();

  • 每次页面刷新都会重新获取一遍实例,重新运行一遍数据库定义逻辑,不会有问题吗?

答案是,不会有问题。如果你传入的 lastVersion 与数据库当前版本一致,则即使重新运行一遍数据库定义逻辑,也不会覆盖你第一次运行时定义的结构(即使你修改了数据库结构),在这种情况下,你已经存入的数据不会受任何影响。只有当 lastVersion 版本高于当前数据库版本时,才会去更新数据库结构(即使结构没有任何变化),这时如果定义中的仓库被删除了,那对应的仓库会被删除,如果定义中是索引被删除了,那仓库中对应的索引也会被删除。

【只有执行完 version().stores()方法之后再至少进行一次数据库操作(比如 open(), get(),put()等),这个才可以生效,因为 versions().store()只是定义结构,并不立即生效,而是等到进行数据库操作时才会打开数据库进行更新】

在具体的实践中,建议将获取数据库实例和定义表结构的代码封装在一起,然后返回一个单例,整个应用中需要这个数据库的地方都从这个方法获取这个单例,这样可以保证所有对数据库结构的改动都在一个地方进行,从而保证数据库版本的一致。

官方的 vue 版本 TODO 应用的示例如下:

// database.js

import Dexie from 'dexie';
export class Database extends Dexie {
  constructor() {
    super('database');

    this.version(1).stores({
      todos: '++id,done',
    });

    this.todos = this.table('todos');
  }

  async getTodos(order) {
    let todos = [];
    switch (order) {
      case forwardOrder:
        todos = await this.todos.orderBy('id').toArray();
        break;
      case reverseOrder:
        todos = await this.todos.orderBy('id').reverse().toArray();
        break;
      case unfinishedFirstOrder:
        todos = await this.todos.orderBy('done').toArray();
        break;
      default:
        // as a default just fall back to forward order
        todos = await this.todos.orderBy('id').toArray();
    }

    return todos.map((t) => {
      t.done = !!t.done;
      return t;
    });
  }

  setTodoDone(id, done) {
    return this.todos.update(id, { done: done ? 1 : 0 })
  }
  addTodo(text) {
    // add a todo by passing in an object using Table.add.
    return this.todos.add({ text: text, done: 0 })
  }

  deleteTodo(todoID) {
    // delete a todo by passing in the ID of that todo.
    return this.todos.delete(todoID);
  }
}

export const forwardOrder = 'forward';

export const reverseOrder = 'reverse';

export const unfinishedFirstOrder = 'unfinished-first';

// App.vue

<template>
  <div class="app">
    <div class="app-header">
      <h2>Vue + Dexie Todo Example</h2>
    </div>
    <AddTodo @add-todo="addTodo" />
    <TodoList
      :todos="todos"
      @toggle-todo="toggleTodo"
      @delete-todo="deleteTodo"
      @sort-todos="updateTodoOrder"
    />
  </div>
</template>

<script>
import AddTodo from './components/AddTodo.vue';
import TodoList from './components/TodoList.vue';

import { Database, forwardOrder } from './database.js';

export default {
  name: 'App',
  components: {
    AddTodo,
    TodoList,
  },
  data() {
    return {
      todos: [],
      order: forwardOrder,
    }
  },

  created() {
    this.db = new Database();
    this.updateTodos();
  },

  methods: {
    async addTodo(todo) {
      await this.db.addTodo(todo.text);
      this.updateTodos(false);
    },
    async toggleTodo(togglePayload) {
      await this.db.setTodoDone(togglePayload.id, togglePayload.done);
      this.updateTodos(false);
    },

    async deleteTodo(deletePayload) {
      await this.db.deleteTodo(deletePayload.id);
      this.updateTodos(false);
    },

    updateTodoOrder(sortTodosPayload) {
      this.order = sortTodosPayload.order;
      this.updateTodos(true);
    },


    async updateTodos(orderUpdated) {
      let todos = await this.db.getTodos(this.order);

      if (orderUpdated) {
        this.todos = todos;
        return
      }
      let idToIndex = {};
      for (let i = 0; i < this.todos.length; i++) {
        idToIndex[this.todos[i].id] = i;
      }
      this.todos = todos.sort((a, b) => {
        // handle new items
        if (idToIndex[a.id] == undefined) {
          return 1;
        } else if (idToIndex[b.id] == undefined) {
          return -1;
        }

        return idToIndex[a.id] < idToIndex[b.id] ? -1 : 1;
      })
    },
  },
}
</script>

<style>
/* ... */
</style>

可以看到,这个示例里,是把数据库操作的所有定义和操作逻辑都封装在了 database.js 的 Database 类中,然后在页面的 created 生命周期中新建了一个数据库实例,下面所有的操作都是用这个实例进行的。

这样做的好处是当数据库的结构需要修改时,只需要在 database.js 中去进行修改和版本号的升级就行了,禁止在应用其它地方修改数据库结构和进行数据库版本升级。

来一波增删改查(CRUD)的操作

1.向表中新增一条记录

db.tableName.add(recordObject);

// 如
db.users.add({
  name: 'zhang san',
  age: '23'
});

2.更新表中的某条记录

db.tableName.put(recordObject);

// 如
db.users.put({
  name: 'zhang san',
  age: '25'
});

此时,如果该表主键是 name,并且表中已经存在 name 为 zhang san 的数据,就会将这条数据的 age 改为 25。

如果该表中没有 name 为 zhang san 的记录,则会将传入的记录作为一条新的记录插入到表中,同 add()行为一致。

所以,鉴于 add()方法执行时如果已经存在主键一样的数据,就会报错,我们推荐总是使用 put()操作来新增和更新记录,而尽量不用 add()操作。

3.获取表中的记录

db.tableName.get(primaryKeyValue);

// 如
db.users.get('zhang san')
  .then((user) => {
  console.log(user.age);
});


如果 user 表的主键是 name,这条查询将获得 name 等于 zhang san 的记录。

注意,dexiejs 的所有操作都会返回 promise,所以要在 promise 的 then 方法里获取查询到的记录。

4.删除表中的记录

db.tableName.delete(primaryKeyValue);

// 如
db.users.delete('zhang san');

其它

其它详细与高级 API 见官网,这里就不做赘述啦!