1. 概述
1.1 浏览器存储选项
渐进式 web应用程序(PWA)运行在浏览器中,必须能够离线工作,因此需要一种存储选项。目前有几种工具可以在浏览器中存储数据,使其可以在线和离线访问。
Web标准为你提供了三种主要的API用于在浏览器中存储数据:
-
Cookies:这些数据存储在浏览器中,
Cookies的大小限制为4KB。通常,当服务器响应请求时,他们可能会包含一个SET-COOKIE头,给浏览器一个要存储的键和值。客户端应该在未来的请求头中包含此Cookie,这将允许服务器识别浏览器会话等。这些Cookies通常具有HTTP-Only属性,这意味着客户端脚本无法访问Cookie。因此,Cookies不适合用于存储离线数据。 -
LocalStorage/SessionStorage:
LocalStorage/SessionStorage是内置于浏览器中的键值存储,每个键的大小限制为5MB。LocalStorage会存储数据直到被删除,而SessionStorage会在浏览器关闭时清除数据。除此之外,它们的API是相同的。你可以使用window.localStorage.setItem("Key", "Value")添加键值对,并使用window.localStorage.getItem("Key")检索值。注意,LocalStorage API是同步的,因此使用它会阻塞浏览器中的其他活动。 -
IndexedDB:这是一个内置于浏览器中的完整文档数据库,没有存储限制,允许异步访问数据,因此非常适合防止复杂操作阻塞渲染和其他活动。就数据库类型而言,
IndexedDB不属于关系型数据库(不支持 SQL 查询语句),更接近NoSQL数据库。
1.2 IndexedDB使用场景
所有的场景都基于客户端需要存储大量数据的前提下:
-
数据可视化等界面,大量数据,每次请求会消耗很大性能。
-
即时聊天工具,大量消息需要存在本地。
-
其它存储方式容量不满足时,不得已使用
IndexedDB。
1.3 IndexedDB的优势
-
IndexedDB提供了类似数据库风格的数据储存和使用方式. -
Cookie只能是字符串,储存空间有限,每次HTTP接收和发送都会传递Cookies数据,它会占用额外的流量. -
LocalStorage是用key-value键值模式储存数据,想让localstorage存储对象,你需要借助JSON.stringify()能将对象变成字符串形式,再用JSON.parse()将字符串还原成对象,当存储的数据庞大时,这就不是最佳的方案了,LocalStorage就是专门为小数量数据设计的,它的api设计为同步的. -
IndexedDB很适合存储大量数据,它的API是异步调用的,IndexedDB使用索引存储数据,各种数据库操作放在事务中执行,IndexedDB支持简单的数据类型,它比LocalStorage强大,API也相对复杂,对于简单的数据,还是使用LocalStorage。 -
IndexedDB能提供更为复杂的查询数据的方式。
1.4 IndexedDB的特性
-
键值对储存
-
IndexedDB没有表的概念,内部采用对象仓库objectstore存放数据。 -
一个数据库中可以包含多个
objectStore -
objectStore是一个灵活的数据结构,可以存放多种类型数据,也就是说一个objectStore相当于一张表,里面储存的每条数据和一个键相关。 -
我们可以使用每条记录中的某个字段作为键值
keyPath,也可以使用自动生成的递增数字作为键值keyGenerator,也可以不指定。 -
选择键的类型不同,
objectStore可以存储的数据结构也有差异
-
-
事务性
-
在
IndexedDB中,每一个对数据库操作是在一个事务的上下文中执行的。这意味着一系列操作步骤之中,只要有一步失败,整个事务就都取消,数据库回滚到事务发生之前的状态,不存在只改写一部分数据的情况。 -
事务范围一次影响一个或多个
objectstores。 -
你通过传入一个
objectstores名字的数组到创建事务范围的函数来定义。- 比如:
db.transaction( storeName, ' readwrite' ),创建事务的第二个参数是事务模式,当请求一个事务时,必须决定是按照只读还是读写模式模式请求访问
- 比如:
-
-
基于请求
-
对
IndexedDB数据库的每次操作,描述为通过一个请求打开数据库,访问一个objectstore,再继续。 -
IndexedDBAPI天生是基于请求的,这也是API异步本性指示,对于你在数据库执行的每次操作,你必须首先为这个操作创建一个请求,当请求完成,你可以响应由请求结果产生的事件和错误
-
-
异步
- 在
IndexedDB大部分操作并不是我们常用的调用方法,返回结果的模式,而是请求 ----> 响应的模式 - 所谓异步API是指并不是这条指令执行完毕,我们就可以使用
request.result来获取IndexedDB对象了。 - 类似于 我们使用
ajax一样,语句执行完并不代表已经获取到了对象,所以我们一般在其回调函数中处理。
- 在
2. IndexedDB重要概念
IndexedDB是一个比较复杂的 API,它把不同的实体,抽象成一个个对象接口:
- 数据库:IDBDatabase对象
- 对象仓库:IDBObjectStore对象
- 索引: IDBIndex对象
- 事务: IDBTransaction对象
- 操作请求:IDBRequest对象
- 指针: IDBCursor对象
- 主键集合:IDBKeyRange对象
下面是一些主要的概念。
- 数据库
数据库是一系列相关数据的容器。每个域名(严格的说,是协议 + 域名 + 端口)都可以新建任意多个数据库。
IndexedDB数据库有版本的概念。同一个时刻,只能有一个版本的数据库存在。如果要修改数据库结构(新增或删除表、索引或者主键),只能通过升级数据库版本完成。
- 对象仓库
每个数据库包含若干个对象仓库objectstore。它类似于关系型数据库的表格。
- 数据记录
对象仓库保存的是数据记录。每条记录类似于关系型数据库的行,但是只有主键和数据体两部分。主键用来建立默认的索引,必须是不同的,否则会报错。主键可以是数据记录里面的一个属性,也可以指定为一个递增的整数编号。
{ id: 1, text: 'foo' }
上面的对象中,id属性可以当作主键。
数据体可以是任意数据类型,不限于对象。
- 索引
为了加速数据的检索,可以在对象仓库里面,为不同的属性建立索引。
- 事务
数据记录的读写和删改,都要通过事务完成。事务对象提供error、abort和complete三个事件,用来监听操作结果。
3. IndexedDB流程
IndexedDB 数据库的各种操作,一般是按照下面的流程进行的
3.1 打开数据库
使用 IndexedDB 的第一步是打开数据库,使用indexedDB.open()方法。
var request = window.indexedDB.open(databaseName, version);
这个方法接受两个参数,第一个参数是字符串,表示数据库的名字。如果指定的数据库不存在,就会新建数据库。第二个参数是整数,表示数据库的版本。如果省略,打开已有数据库时,默认为当前版本;新建数据库时,默认为1。
indexedDB.open()方法返回一个 IDBRequest 对象。这个对象通过三种事件error、success、upgradeneeded,处理打开数据库的操作结果。
1. error 事件
error事件表示打开数据库失败。
request.onerror = function (event) {
console.log('数据库打开报错');
};
2. success 事件
success事件表示成功打开数据库。
var db;
request.onsuccess = function (event) {
db = request.result;
console.log('数据库打开成功');
};
这时,通过request对象的result属性拿到数据库对象。
3. upgradeneeded 事件
如果指定的版本号,大于数据库的实际版本号,就会发生数据库升级事件upgradeneeded。
var db;
request.onupgradeneeded = function (event) {
db = event.target.result;
}
这时通过事件对象的target.result属性,拿到数据库实例。
3.2 新建数据库
新建数据库与打开数据库是同一个操作。如果指定的数据库不存在,就会新建。不同之处在于,后续的操作主要在upgradeneeded事件的监听函数里面完成,因为这时版本从无到有,所以会触发这个事件。
通常,新建数据库以后,第一件事是新建对象仓库(即新建表)。
request.onupgradeneeded = function(event) {
db = event.target.result;
var objectStore = db.createObjectStore('person', { keyPath: 'id' });
}
上面代码中,数据库新建成功以后,新增一张叫做person的表格,主键是id。
更好的写法是先判断一下,这张表格是否存在,如果不存在再新建。
request.onupgradeneeded = function (event) {
db = event.target.result;
var objectStore;
if (!db.objectStoreNames.contains('person')) {
objectStore = db.createObjectStore('person', { keyPath: 'id' });
}
}
主键(key)是默认建立索引的属性。比如,数据记录是{ id: 1, name: '张三' },那么id属性可以作为主键。主键也可以指定为下一层对象的属性,比如{ foo: { bar: 'baz' } }的foo.bar也可以指定为主键。
如果数据记录里面没有合适作为主键的属性,那么可以让 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()的三个参数分别为索引名称、索引所在的属性、配置对象(说明该属性是否包含重复的值)。
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 读取数据
读取数据也是通过事务完成。
function read() {
var transaction = db.transaction(['person']);
var objectStore = transaction.objectStore('person');
var request = objectStore.get(1);
request.onerror = function(event) {
console.log('事务失败');
};
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('未获得数据记录');
}
};
}
read();
上面代码中,objectStore.get()方法用于读取数据,参数是主键的值。
3.5 遍历数据
遍历数据表格的所有记录,要使用指针对象 IDBCursor。
function readAll() {
var objectStore = db.transaction('person').objectStore('person');
objectStore.openCursor().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.email);
cursor.continue();
} else {
console.log('没有更多数据了!');
}
};
}
readAll();
上面代码中,新建指针对象的openCursor()方法是一个异步操作,所以要监听success事件。
3.6 更新数据
更新数据要使用IDBObject.put()方法。
function update() {
var request = db.transaction(['person'], 'readwrite')
.objectStore('person')
.put({ id: 1, name: '李四', age: 35, email: 'lisi@example.com' });
request.onsuccess = function (event) {
console.log('数据更新成功');
};
request.onerror = function (event) {
console.log('数据更新失败');
}
}
update();
上面代码中,put()方法自动更新了主键为1的记录。
3.7 删除数据
IDBObjectStore.delete()方法用于删除记录。
function remove() {
var request = db.transaction(['person'], 'readwrite')
.objectStore('person')
.delete(1);
request.onsuccess = function (event) {
console.log('数据删除成功');
};
}
remove();
3.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('李四');
request.onsuccess = function (e) {
var result = e.target.result;
if (result) {
// ...
} else {
// ...
}
}