前言
大家好,今天给大家分享一个好用的第三方库,它叫 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 见官网,这里就不做赘述啦!