IndexedDB 是浏览器提供的本地数据库,js原生支持创建和操作 IndexedDB。IndexedDB 允许储存大量数据,提供查找接口,还能建立索引。就数据库类型而言,IndexedDB 不属于关系型数据库,更接近 NoSQL 数据库(存储key-value)。
它具有如下特点:
短暂存储
情况下,一组域名受到组限制
,共享最小10MB,最大2GB的存储空间。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。
使用 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 作为版本号。
新建数据库与打开数据库是同一个操作。如果指定的数据库不存在,就会新建。不同之处在于,后续的操作主要在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属性表示是否包含重复的值)。
这里只介绍新增数据,其余操作与此类似,具体使用参考下一章节在项目中封装使用
新增数据指的是向对象仓库写入数据记录。这需要通过事务完成。
function add() {
var request = db.transaction('person', 'readwrite')
.objectStore('person')
.add({ id: 1, name: '张三', age: 24, email: '[email protected]' });
request.onsuccess = function (event) {
console.log('数据写入成功');
};
request.onerror = function (event) {
console.log('数据写入失败');
}
}
add();
上面代码中,写入数据需要新建一个事务。新建时必须指定表格名称和操作模式(“只读"或"读写”)。新建事务以后,通过IDBTransaction.objectStore(name)
方法,拿到 IDBObjectStore
对象,再通过表格对象的add()
方法,向表格写入一条记录。
写入操作是一个异步操作,通过监听连接对象的success
事件和error
事件,了解是否写入成功。
索引的意义在于,可以让你按任意字段搜索数据,也就是说从任意字段拿到数据记录。如果不建立索引,默认只能按主键搜索。
假定新建表格的时候,对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 {
// ...
}
}
使用数据库的第一步就是打开或新建一个数据库
创建一个学生数据库,其中有两个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);
下面是openDatabase
函数的函数体。
openDatabase
函数返回一个 Promise 对象,可异步获取打开的数据库对象实例。如果打开的对象不存在或者数据库版本号比实际版本大,将会触发upgradeneeded
事件,在该事件中,首先删除旧有版本的所有object store
,然后建立新版本的数据库。
dbName:string
数据库名。
dbVersion:number
数据库版本号。
dbStores:Array
数据库信息,可以有多个object store
,单个object store
可声明多个索引。
type DBStoreType = {
dbStore: DBStoreParameter;
dbIndexs?: Array;
}
type DBStoreParameter = {
dbStoreName: string;
options?: IDBObjectStoreParameters;
}
type DBIndexParameter = {
dbIndexName: string;
keyPath: string | string[];
options?: IDBIndexParameters;
}
上面代码中,可以看到DBStoreType
由dbStore
和dbIndexs
属性组成。
实例化DBObjectStore
,对studentDatabase
中的student
表进行各种数据操作,包括增删改查,迭代。
type StudentInfo = {
id: number;
name: string;
age: number;
}
const StudentStore = DBObjectStore<[number],StudentInfo>
DBObjectStore
类初始化时,获得了数据库对象实例database
以及要操作的object store
名。
DBObjectStore
是一个泛型类,需要传递类型作为参数,以约束object store
中存储数据的key
,value
的类型。
注意:当不设置主键时,使用默认递增的主键,它的类型为 number。设置主键后,value 的类型必须为 object 类型,不能是 number, string 等类型。
getStore(storeName: string, mode? IDBTransactionMode)
返回 object store,准备开始操作数据,数据操作在指定模式下的事务中进行。不知道事务模式,默认为 readonly。
put(value: V, key? K): Promise
增加或修改数据,返回一个 Promise 对象,增加或修改成功传递 Event 事件参数回调。
增加还是修改取决于object store
中是否含有该键指向的数据。
如果设置了指定字段作为主键,那么就不需要使用第二个参数,value 中已包含该主键值。未指定主键,按默认主键,如果设置为主键递增增加,则第二参数可要可不要。
putBulk(value: Array): Promise
批量一次性添加多条数据。返回一个 Promise 对象,成功则传递事件参数 Event 回调。
delete(query: K | IDBKeyRange): Promise
删除一条数据或多条数据。返回一个 Promise 对象,成功则传递事件参数 Event 回调。
get(query: K): Promise
按主键查询一条数据。返回一个 Promise 对象,成功则传递查询结果回调。
getRange(query: IDBKeyRange): Promise>
按主键范围返回查询数据。返回一个 Promise 对象,成功则传递查询结果回调。
getAll(): Promise>
查询当前object store
的所有数据。返回一个 Promise 对象,成功则传递查询结果回调。
getByIndex(indexName: string, key: IDBValidKey): Promise
按索引中的 key 查询一条数据。返回一个 Promise 对象,成功则传递查询结果回调。
getRangeByIndex(indexName: string, key: IDBKeyRange): Promise>
按索引中的 key范围查询多条数据。返回一个 Promise 对象,成功则传递查询结果回调。
iterate(iterateCall: (value: V) => void,query?:K | IDBValidKey | IDBKeyRange | null, indexName?: string, direction?: IDBCursorDirection): Promise>
迭代object store
或索引,可指定迭代范围,不指定迭代所有,也可指定迭代方向。迭代操作,通过传入的iterateCall
完成
clearData(): Promise
清空当前object store
中的数据。返回一个 Promise 对象,成功则传递事件参数 Event 回调。
IndexedDB遵守同源原则,这意味着某个源创建的数据库只能在该源里访问。但是也有特殊情况,下面这句话来自mdn
文档:
Third party window content (e.g.
这段英文不太能理解,另外我又在微软的技术文档里找到了另一种表述。
经过实验,发现是页面能访问
indexedDB 创建的数据库内容始终是保留在用户端的,每当我们想改变数据库的结构,就要考虑用户端本地原来保存的是什么样的数据库。
最简单的方法,就是在数据库版本提升时,清空旧的 object store。这样当用户再次访问我们网站时,就会执行这段脚本,清空旧数据。
当某个数据库弃用时也需要,写一段脚本去删除。
(window as any)._indexedDB.deleteDatabase('databaseName');
数据库结构对应 openDatabase 函数的 dbStores:Array
参数,当其改变时,提升打开的数据库版本号以触发 onupgradeneeded 事件。