How to use IndexedDB to build Progressive Web Apps

阅读原文,并写下相关要点。

你需要有在Service Workers 中的 IndexedDB 的实现的 Service Workers 相关概念及知识。

开始

单页应用基本需要 web service 装在数据。数据通过控制器注入 DOM 中。大多数前端框架都这么做。

你可以缓存静态网页和资源文件,尤其是浏览的数据,但没有网络的时候,用户可以从当地数据库缓存中进行读取。

IndexedDB 是被废弃的 Web SQL 数据库的代替品。一个键值对的 NoSQL 且支持超大储存(上浮20-50%硬盘空间)的数据库。支持数据类型 number, string, JSON, blob 等等。

IndexedDB遵守同源策略。这意味着不同应用不能相互访问数据库。这是事件驱动,具有事务性,原子操作,API 异步的数据库。被所有主流浏览器支持,不支持的以后也会支持。非常适合储存离线数据。

IndexedDB 2.0 是一个值得一看的。虽然所有浏览器都不支持。

我用几个小 API 吧,文档

介绍

IndexedDB 由多个 object store 组成。像 MySQL 的表、 MongoDB 的集合。每个储存都有多个对象。像是 MySQL 表里的行。

object store 可以记录多行,是键值对型的。每行靠 key 上升排序。数据库里只能有一个独一无二的 object store 名字。
一个数据库创建的时候,版本为1。 数据库不能有多版本。

可以被多个客户端连接,读写操作具有事务性,

key 的类型:string, date, float, a binary blob, or an array。

value 跟 JavaScript 表达的一样:boolean, number, string, date, object, array, regexp, undefined and null。

用 IndexedDB 的基本操作:

  • 打开数据库。
  • 创建一个 object store。
  • 事务性执行数据库操作,如添加或检索数据。
  • 等操作完成,通过监听事件。
  • 用读出结果坐点东西。

window 和 service worker 的全局作用域里都可以使用。
检查一下支持不,window.indexedDB or self.IndexedDB。

1
2
3
if(self.IndexedDB){
console.log('IndexedDB is supported');
}

.open(dbName, versionInt) 打开数据库。传入名字和版本号。

如果数据库不存在,就创建一个。如果版本号高,数据库会用新版本,不用旧数据。
事件驱动,你懂吧。open 方法触发 success 、 error 、 upgradeneeded。像我一样:

1
2
3
4
5
6
7
8
9
var request = self.IndexedDB.open('EXAMPLE_DB', 1);
var db;
request.onsuccess = function(event) {
console.log('[onsuccess]', request.result);
db = event.target.result; // === request.result
};
request.onerror = function(event) {
console.log('[onerror]', request.error);
};

在回调里,event.target === request。request.result 就是结果。

onerror 里记得处理错误。

开发者工具里, Application > IndexedDB 里可以看情况。

打开新数据库或者新版本的时候,onupgradeneeded 会被触发。

1
2
3
request.onupgradeneeded = function(event) {
// create object store from db or event.target.result
};

createObjectStore() 创建 object store 。

1
db.createObjectStore(storeName, options);

storeName 也是唯一性。options 有两种属性。options.keyPath 是字段名称。(类似 Key

1
2
3
4
request.onupgradeneeded = function(event) {
var db = event.target.result;
var store = db.createObjectStore('products', {keyPath: 'id'});
};

作用如图:

如果需要 Key 自增,使用 options.autoIncrement = true
各选项组合效果如下:

IndexedDB 具有索引。Indexes 且可具有唯一约束。

1
store.createIndex(indexName, keyPath, options);

indexName 索引名称, keyPath 在哪个 Key 上建立。options 可选。可配置 {unique: true}
如上配置,则无法添加重复Key值得条目。

1
2
3
4
5
6
request.onupgradeneeded = function(event) {
var db = event.target.result;
var store = db.createObjectStore('products', {keyPath: 'id'});
// create unique index on keyPath === 'id'
store.createIndex('products_id_unqiue, 'id', {unique: true});
};

一旦 onupgradeneeded 事件完成, success 事件被触发。

1
2
var transaction = db.transaction(storeName, mode);
var transaction = db.transaction(storeNamesArray, mode);

根据 store 返回操作权限数量。

mode 可选 参数为 readonly readwrite versionchange。

1
var objectStore = transaction.objectStore(storeName);

操作成功后,complete 事件被触发。
abort() 可以回滚事务。

关于事务

objectStore 是 IDBObjectStore 接口实例,提供了 get, add, clear, count, put, delete 等操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
var request = self.IndexedDB.open('EXAMPLE_DB', 1);
request.onsuccess = function(event) {
// some sample products data
var products = [
{id: 1, name: 'Red Men T-Shirt', price: '$3.99'},
{id: 2, name: 'Pink Women Shorts', price: '$5.99'},
{id: 3, name: 'Nike white Shoes', price: '$300'}
];
// get database from event
var db = event.target.result;
// create transaction from database
var transaction = db.transaction('products', 'readwrite');
// add success event handleer for transaction
// you should also add onerror, onabort event handlers
transaction.onsuccess = function(event) {
console.log('[Transaction] ALL DONE!');
};

// get store from transaction
// returns IDBObjectStore instance
var productsStore = transaction.objectStore('products');
// put products data in productsStore
products.forEach(function(product){
var db_op_req = productsStore.add(product); // IDBRequest
});
};

for the sake of simplicity === 简单起见

1
2
3
4
5
6
7
var db_op_req = productsStore.add(product);
db_op_req.onsuccess = function(event) {
console.log(event.target.result == product.id); // true
};
db_op_req.onerror = function(event) {
// handle error
};

CRUD:

  • objectStore.add(data)
  • objectStore.get(key)
  • objectStore.getAll()
  • objectStore.count(key?)
  • objectStore.getAllKeys()
  • objectStore.put(data, key?)
  • objectStore.delete(key)
  • objectStore.clear()

详情见 IDBObjectStore

close 可关闭数据库连接。除非事务都完成,数据库才关闭。但提前关闭,则不会有新的事务产生。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
var request = self.indexedDB.open('EXAMPLE_DB', 1);

request.onsuccess = function(event) {
// some sample products data
var products = [
{id: 1, name: 'Red Men T-Shirt', price: '$3.99'},
{id: 2, name: 'Pink Women Shorts', price: '$5.99'},
{id: 3, name: 'Nike white Shoes', price: '$300'}
];

// get database from event
var db = event.target.result;

// create transaction from database
var transaction = db.transaction('products', 'readwrite');

// add success event handleer for transaction
// you should also add onerror, onabort event handlers
transaction.onsuccess = function(event) {
console.log('[Transaction] ALL DONE!');
};

// get store from transaction
var productsStore = transaction.objectStore('products');

/*************************************/

// put products data in productsStore
products.forEach(function(product){
var db_op_req = productsStore.add(product);

db_op_req.onsuccess = function(event) {
console.log(event.target.result == product.id); // true
}
});

// count number of objects in store
productsStore.count().onsuccess = function(event) {
console.log('[Transaction - COUNT] number of products in store', event.target.result);
};

// get product with id 1
productsStore.get(1).onsuccess = function(event) {
console.log('[Transaction - GET] product with id 1', event.target.result);
};

// update product with id 1
products[0].name = 'Blue Men T-shirt';
productsStore.put(products[0]).onsuccess = function(event) {
console.log('[Transaction - PUT] product with id 1', event.target.result);
};

// delete product with id 2
productsStore.delete(2).onsuccess = function(event) {
console.log('[Transaction - DELETE] deleted with id 2');
};
};

request.onerror = function(event) {
console.log('[onerror]', request.error);
};

request.onupgradeneeded = function(event) {
var db = event.target.result;
var productsStore = db.createObjectStore('products', {keyPath: 'id'});
};

输出:

检查结果:

理解更多 IndexedDB 概念,如游标,数据库迁移,数据库版本控制。

学习如何正确使用是一个大问题。遵从下面的建议去让你更好使用离线数据库:

  • service worker 的 install 事件中去缓存静态文件。
  • 在 service 的 activate 事件中初始化数据库比 worker 的 install 事件好。新数据库与 old service worker 混合使用可能会产生冲突。用 keyPath 作为 URL 储存点。
  • 无论在线或离线,你的 app 会使用缓存文件。但获取数据的请求依然会发出。
  • 当线上请求出现错误的时候,设计一个 offline-api 的地址,进入 service worker 获取数据库数据。
  • 请求以 /offline-api 开头,那么使用等于请求 keyPath 从数据库提取数据, 在 application/json 之类的响应上设置适当的头并将响应返回给浏览器。 你可以使用 Response 构造函数。

用上面的方法,可以构造一个完全离线使用的应用。作者准备写一系列的文章去细讲 Progress Web Apps。

这篇文章可能漏讲一些东西,去仔细审阅文档吧。

与缓存API不同,IndexedDB API是事件驱动的,而不是基于 Promise 的。使用一些indexeddb包装库,它允许你编写基于promise的代码。

  • localForage (~8KB, promises, good legacy browser support)
  • IDB-keyval (500 byte alternative to localForage, for modern browsers)
  • IDB-promised (~2k, same IndexedDB API, but with promises)
  • Dexie (~16KB, promises, complex queries, secondary indices)
  • PouchDB (~45KB (supports custom builds), synchronization)
  • Lovefield (relational)
  • LokiJS (in-memory)
  • ydn-db (dexie-like, works with WebSQL)