IndexedDB教程
一、概述
IndexedDB 是浏览器提供的本地数据库,js原生支持创建和操作 IndexedDB。IndexedDB 允许储存大量数据,提供查找接口,还能建立索引。就数据库类型而言,IndexedDB 不属于关系型数据库,更接近 NoSQL 数据库(存储key-value)。
它具有如下特点:
-
键值对储存
。 -
异步
。IndexedDB 操作时不会锁死浏览器,用户依然可以进行其他操作。 -
支持事务
。 IndexedDB 支持事务(transaction),这意味着一系列操作步骤之中,只要有一步失败,整个事务就都取消,数据库回滚到事务发生之前的状态,不存在只改写一部分数据的情况。 -
同源限制
。网页只能访问自身域名下的数据库,而不能访问跨域的数据库。 -
存储空间大
。在默认使用
短暂存储
情况下,一组域名受到
组限制
,共享最小10MB,最大2GB的存储空间。 -
支持二进制存储
。如 ArrayBuffer 对象和 Blob 对象
二、基本概念
IndexedDB 是一个比较复杂的 API,涉及不少概念。它把不同的实体,抽象成一个个对象接口。学习这个 API,就是学习它的各种对象接口。
* 数据库:IDBDatabase 对象
* 对象仓库:IDBObjectStore 对象
* 索引: IDBIndex 对象
* 事务: IDBTransaction 对象
* 操作请求:IDBRequest 对象
* 指针: IDBCursor 对象
* 主键集合:IDBKeyRange 对象
下面是一些主要的概念。
(1) 数据库
数据库是一系列相关数据的容器。每个域名(严格的说,是协议 + 域名 + 端口)都可以新建任意多个数据库。
IndexedDB 数据库有版本的概念。同一个时刻,只能有一个版本的数据库存在。如果要修改数据库结构(新增或删除
object store
、索引或者主键),只能通过升级数据库版本完成。
(2) 对象仓库
每个数据库包含若干个对象仓库(object store)。它类似于关系型数据库的表。
(3) 数据记录
对象仓库保存的是数据记录。每条记录类似于关系型数据库的行,但是只有主键和数据体两部分。主键用来建立默认的索引,必须是不同的,否则会报错。主键可以是数据记录里面的一个属性,也可以指定为一个递增的整数编号。
{ id: 1, text: 'foo' }
上面是一个
object store
中的一条数据,我们可以指定
id
属性作为该
object store
的主键,这条数据的数据体就是
{ id: 1, text: 'foo' }
(4) 索引
为了加速数据的检索,可以在对象仓库里面,为不同的属性建立索引。
(5) 事务
数据的增删改查、数据库版本的升级都要通过事务完成,总共有三种事务模式:
readwrite
、
readonly
和
versionchange
。
三、API 介绍
下面通过具体操作数据库时的流程,介绍相关 API。
3.1 打开数据库
使用 IndexedDB 的第一步是打开数据库,使用indexedDB.open()方法。
var request = window.indexedDB.open(databaseName, version);
这个方法接受两个参数,第一个参数是字符串,表示数据库的名字。如果指定的数据库不存在,就会新建数据库。第二个参数是整数,表示数据库的版本。如果省略,打开已有数据库时,默认为当前版本;新建数据库时,默认为
1
indexedDB.open()
方法返回一个 IDBRequest 对象。这个对象通过三种事件
error
、
success
、
upgradeneeded
,处理打开数据库的操作结果。
error
、
success
事件表示打开数据库失败、成功,如果
数据库不存在
或者指定打开的版本大于实际的数据库版本,就会触发数据库升级事件
upgradeneeded
。这时通过事件对象的
target.result
属性,拿到数据库实例。
var db;
request.onsuccess = function (event) {
db = request.result;
console.log('数据库打开成功');
};
request.onerror = function (event) {
console.log('数据库打开报错');
};
request.onupgradeneeded = function (event) {
db = event.target.result;
}
注意
:版本号是 unsigned long long 类型,不是浮点型,不能使用 2.4 作为版本号。
3.2 新建数据库
新建数据库与打开数据库是同一个操作。如果指定的数据库不存在,就会新建。不同之处在于,后续的操作主要在upgradeneeded事件的监听函数里面完成,因为这时版本从无到有,所以会触发这个事件。
通常,新建数据库以后,第一件事是新建对象仓库(即新建表)。
request.onupgradeneeded = function(event) {
db = event.target.result;
var objectStore = db.createObjectStore('person', { keyPath: 'id' });
}
上面代码中,数据库新建成功以后,新增一张叫做person的表格,主键是id。如果数据记录里面没有合适作为主键的属性,那么可以让 IndexedDB 自动生成主键。
var objectStore = db.createObjectStore(
'person',
{ autoIncrement: true }
);
上面代码中,指定主键为一个递增的整数。
新建对象仓库以后,下一步可以新建索引。
request.onupgradeneeded = function(event) {
db = event.target.result;
var objectStore = db.createObjectStore('person', { keyPath: 'id' });
objectStore.createIndex('name', 'name', { unique: false });
objectStore.createIndex('email', 'email', { unique: true });
}
上面代码中,
IDBObject.createIndex()
的三个参数分别为索引名称、索引所在的属性、配置对象(unique属性表示是否包含重复的值)。
3.3 数据操作
这里只介绍新增数据,其余操作与此类似,具体使用参考下一章节
在项目中封装使用
新增数据指的是向对象仓库写入数据记录。这需要通过事务完成。
function add() {
var request = db.transaction('person', 'readwrite')
.objectStore('person')
.add({ id: 1, name: '张三', age: 24, email: 'zhangsan@example.com' });
request.onsuccess = function (event) {
console.log('数据写入成功');
};
request.onerror = function (event) {
console.log('数据写入失败');
}
}
add();
上面代码中,写入数据需要新建一个事务。新建时必须指定表格名称和操作模式(“只读”或”读写”)。新建事务以后,通过
IDBTransaction.objectStore(name)
方法,拿到
IDBObjectStore
对象,再通过表格对象的
add()
方法,向表格写入一条记录。
写入操作是一个异步操作,通过监听连接对象的
success
事件和
error
事件,了解是否写入成功。
3.4 使用索引
索引的意义在于,可以让你按任意字段搜索数据,也就是说从任意字段拿到数据记录。如果不建立索引,默认只能按主键搜索。
假定新建表格的时候,对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('李四');
request.onsuccess = function (e) {
var result = e.target.result;
if (result) {
// ...
} else {
// ...
}
}
四、在项目中封装使用
4.1 打开(新建)数据库,DBInstance.ts: openDatabase
使用数据库的第一步就是打开或新建一个数据库
4.1.1 实例
创建一个学生数据库,其中有两个
object store
。一个名为
studentInfo
的
object store
记录学生的身份信息,以学生的
id
属性作为主键,同时对
age
、
name
字段建立索引。另一个名为
score
的
object store
记录学生的分数信息,自动递增生成主键。
enum StoreName {
Student = 'studentInfo',
Score = 'score'
}
const studentSchema: Array<DBStoreType> = [
{
dbStore: {
dbStoreName: StoreName.Student,
},
dbIndex: [
{
dbIndexName: 'age',
keyPath: 'age',
},
{
dbIndexName: 'name',
keyPath: 'name'
}
]
},
{
dbStore: {
dbStoreName: StoreName.Score,
options: {autoIncrement: true}
}
}
]
const studentDatabase = await openDatabase('student', 1, studentSchema);
4.1.2 定义和用法
下面是
openDatabase
函数的函数体。
openDatabase
函数返回一个 Promise 对象,可异步获取打开的数据库对象实例。如果打开的对象不存在或者数据库版本号比实际版本大,将会触发
upgradeneeded
事件,在该事件中,首先删除旧有版本的所有
object store
,然后建立新版本的数据库。
4.1.3 参数说明
-
dbName:string
数据库名。
-
dbVersion:number
数据库版本号。
-
dbStores:Array<DBStoreType>
数据库信息,可以有多个
object store
,单个
object store
可声明多个索引。
type DBStoreType = {
dbStore: DBStoreParameter;
dbIndexs?: Array<DBIndexParameter>;
}
type DBStoreParameter = {
dbStoreName: string;
options?: IDBObjectStoreParameters;
}
type DBIndexParameter = {
dbIndexName: string;
keyPath: string | string[];
options?: IDBIndexParameters;
}
上面代码中,可以看到
DBStoreType
由
dbStore
和
dbIndexs
属性组成。
4.2 Object Store 封装类,DBObjectStore.ts: DBObjectStore
4.2.1 实例
实例化
DBObjectStore
,对
studentDatabase
中的
student
表进行各种数据操作,包括增删改查,迭代。
type StudentInfo = {
id: number;
name: string;
age: number;
}
const StudentStore = DBObjectStore<[number],StudentInfo>
4.2.2 定义和用法
DBObjectStore
类初始化时,获得了数据库对象实例
database
以及要操作的
object store
名。
DBObjectStore
是一个泛型类,需要传递类型作为参数,以约束
object store
中存储数据的
key
,
value
的类型。
注意
:当不设置主键时,使用默认递增的主键,它的类型为 number。设置主键后,value 的类型必须为 object 类型,不能是 number, string 等类型。
4.2.3 DBObjectStore 方法介绍
-
getStore(storeName: string, mode? IDBTransactionMode)
返回 object store,准备开始操作数据,数据操作在指定模式下的事务中进行。不知道事务模式,默认为 readonly。
-
put(value: V, key? K): Promise<Event>
增加或修改数据,返回一个 Promise 对象,增加或修改成功传递 Event 事件参数回调。
增加还是修改取决于
object store
中是否含有该键指向的数据。
如果设置了指定字段作为主键,那么就不需要使用第二个参数,value 中已包含该主键值。未指定主键,按默认主键,如果设置为主键递增增加,则第二参数可要可不要。
-
putBulk(value: Array<V>): Promise<Event>
批量一次性添加多条数据。返回一个 Promise 对象,成功则传递事件参数 Event 回调。
-
delete(query: K | IDBKeyRange): Promise<Event>
删除一条数据或多条数据。返回一个 Promise 对象,成功则传递事件参数 Event 回调。
-
get(query: K): Promise<V>
按主键查询一条数据。返回一个 Promise 对象,成功则传递查询结果回调。
-
getRange(query: IDBKeyRange): Promise<Array<V>>
按主键范围返回查询数据。返回一个 Promise 对象,成功则传递查询结果回调。
-
getAll(): Promise<Array<V>>
查询当前
object store
的所有数据。返回一个 Promise 对象,成功则传递查询结果回调。
-
getByIndex(indexName: string, key: IDBValidKey): Promise<V>
按索引中的 key 查询一条数据。返回一个 Promise 对象,成功则传递查询结果回调。
-
getRangeByIndex(indexName: string, key: IDBKeyRange): Promise<Array<V>>
按索引中的 key范围查询多条数据。返回一个 Promise 对象,成功则传递查询结果回调。
-
iterate(iterateCall: (value: V) => void,query?:K | IDBValidKey | IDBKeyRange | null, indexName?: string, direction?: IDBCursorDirection): Promise<Array<V>>
迭代
object store
或索引,可指定迭代范围,不指定迭代所有,也可指定迭代方向。迭代操作,通过传入的
iterateCall
完成
-
clearData(): Promise<Event>
清空当前
object store
中的数据。返回一个 Promise 对象,成功则传递事件参数 Event 回调。
五、数据库安全与管理
5.1 安全
IndexedDB遵守同源原则,这意味着某个源创建的数据库只能在该源里访问。但是也有特殊情况,下面这句话来自
mdn
文档:
Third party window content (e.g. <iframe> content) cannot access IndexedDB if the browser is set to
never accept third party cookies
(see
bug 1147821
.)
这段英文不太能理解,另外我又在微软的技术文档里找到了另一种表述。
经过实验,发现是页面能访问 <iframe> 中的 indexedDB,但是<iframe>无法访问页面的 indexedDB。
5.2 数据库管理
indexedDB 创建的数据库内容始终是保留在用户端的,每当我们想改变数据库的结构,就要考虑用户端本地原来保存的是什么样的数据库。
最简单的方法,就是在数据库版本提升时,清空旧的 object store。这样当用户再次访问我们网站时,就会执行这段脚本,清空旧数据。
当某个数据库弃用时也需要,写一段脚本去删除。
(window as any)._indexedDB.deleteDatabase('databaseName');
数据库结构对应 openDatabase 函数的 dbStores:Array<DBStoreType> 参数,当其改变时,提升打开的数据库版本号以触发 onupgradeneeded 事件。
参考链接
- 浏览器数据库 IndexedDB 入门教程,阮一峰,https://www.ruanyifeng.com/blog/2018/07/indexeddb.html
- IndexedDB API,mdn web docs,https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API