IndexedDB 是一个为了在客户端中存储结构化数据,也就是我们通常所说的NOSQL。同时也满足一些数据库的特性,比如:大容量、索引检索、事务、安全性、存储类型(包括:二进制对象)、异步API。

基本概念

  • key-values键值对存储:values可以是一个非常复杂的结构体。
  • 事务模式数据库:任何操作都需要一个事务,事务是有生命周期,且只能自动提交。
  • 异步API:虽然也有同步,但是同步目前没有任何一个浏览器版本实现。
  • 同源策略:这里的“源”指的是文档URL所在的协议、域名和端口,在同一个源内的所有数据库都是必须是唯一的。
  • 数据库版本:版本为了区分作数据迁移。
  • object store:同NoSQL中的集合一个意思。

打开数据库

const dbName = 'DemoDb';

let db;
let request = indexedDB.open(dbName, 1);
request.onerror = function  (e) {
    // 错误处理
    alert(e.target.error.message);
}
request.onsuccess = function  (e) {
    // 数据库打开成功处理
    db = request.result;
}
request.onupgradeneeded = function  (e) {
    // 当首次打开数据库或打开的版本号高于当前版本号时执行,同时也是唯一一个最好来创建存储空间、索引、初始数据的地方
}
request.onblocked = function  () {
    // 其他选项卡也打开数据库时
}

可见当我们打开一个数据库时,极为简单。 1是我们的版本号,这里的版本号只允许向上升,而无法对其降级(只能删除数据库)。

onerror IndexedDB被设计来尽可能的减少对错误处理的请求,但是打开数据库会产生错误最有可能会包括:

  • 浏览器会在首次尝试打开一个IndexedDB存储时提醒用户是否允许或拒绝。
  • 隐私模式下访问。
  • 存储设备容量上限。
  • 用户请求清除数据。

而对于错误的处理都是通过事件冒泡方式,从事务再到数据库对象。所以一般如果我们希望处理到所有错误,可以在这里做统一处理。

创建数据库结构

既然前面已经说只能在 onupgradeneeded 事件中创建存储空间,那我们来尝试做一个 customers 存储空间存放客户信息,而每个客户信息包括:唯一编号、姓名、年龄、Email;同时为了加快访问,创建以 name 姓名非唯一索引,以 email 唯一索引(不能重复,否则会收到一个错误消息)。

为了便于查看,我依然放完整代码:

const customerData = [
    { id: 1, name: '苏先生', age: 25, email: 'xx@gmail.com' },
    { id: 2, name: 'asdf', age: 125, email: 'yy@gmail.com' }
];
const dbName = 'DemoDb';

let db;
let request = indexedDB.open(dbName, 1);
request.onerror = function  (e) {
    // 错误处理
    alert(e.target.error.message);
}
request.onsuccess = function  (e) {
    // 数据库打开成功处理
    db = request.result;
}
request.onupgradeneeded = function  (e) {
    // 当首次打开数据库或打开的版本号高于当前版本号时执行
    // 同时也是唯一一个最好来创建存储空间、索引、初始数据的地方

    db = e.target.result;
    // 创建一个存储空间存放customerData
    let objectStore = db.createObjectStore('customers', { keyPath: 'id' });

    // 创建索引
    // 可能会有重复的,因此我们不能使用 unique 索引
    objectStore.createIndex('name', 'name', { unique: false });
    // 希望确保不会有两个相同的email,因此我们使用 unique 索引
    objectStore.createIndex('email', 'email', { unique: true });

    // 初始化存储空间数据
    customerData.forEach(function(item) {
        objectStore.add(item);
    });
}
request.onblocked = function  () {
    // 其他选项卡也打开数据库时
}

增、删、改数据

对象 keypathkey generator

IndexedDB 既然是key-values键值对存储,必然就有键和值才能相关联。然而键的不同会倒置存储值的不同,这取决于对象存储空间是采用 key path 还是 key generator了。

key path

存储的值只能是一个javascript对象,而且对象必须包括和 key path 相同名的属性。比如前面的 let objectStore = db.createObjectStore('customers', { keyPath: 'id' }); 其中是 id 作为 key path,所以在增加对象时必须包含一个叫 id 的属性。

key generator

存储的值就非常自由了,可以是javascript对象或者基础类型(像字符串或数字等等),但是在 put 时就需要指定 optionalKey

事务

如同我们上面说的,每一个操作都需要有一个事务 transaction

db.transaction(['customers'], 'readwrite');

其中第二参数有三个值,代表着不同的事务隔离类型:

readonly:允许读取数据,不改变。
readwrite:允许读取和写入现有数据。
versionchange:允许执行任何操作,包括删除和创建对象存储空间。

使用游标获取所有数据

let listEl = document.getElementById('list');
function loadCustomer() {
    listEl.innerHTML = '';
    let objectStore = db.transaction('customers').objectStore('customers');
    objectStore.openCursor().onsuccess = function(e) {
        let cursor = event.target.result;
        if (cursor) {
            let trEl = document.createElement('tr');
            trEl.innerHTML = `<tr>
                                <td>${cursor.value.id}</td>
                                <td>${cursor.value.name}</td>
                                <td>${cursor.value.age}</td>
                                <td>${cursor.value.email}</td>
                                <td>
                                    <button data-id="${cursor.value.id}" class="view">查看</button>
                                    <button data-id="${cursor.value.id}" class="del">删除</button>
                                </td>
                            </tr>`;
            listEl.appendChild(trEl);
            cursor.continue();
        }
    }
}

openCursor() 包含两个参数:

optionalKeyRange:键的范围,例如:IDBKeyRange.bound("A", "F") 表示 key 的上限为A,下限为F。
optionalDirection:检索的方向,例如:next 表示升序,prev 表示降序。

增改数据

<form action="#" method="post" id="customerForm">
    <p>编号:<input type="number" value="3" id="id" required></p>
    <p>姓名:<input type="text" value="add name" id="name" required></p>
    <p>年龄:<input type="number" value="56" id="age" required></p>
    <p>Email:<input type="email" value="a@qq.com" id="email" required></p>
    <button type="submit" id="save">保存</button>
</form>
let idEl = document.getElementById('id'),
    nameEl = document.getElementById('name'),
    ageEl = document.getElementById('age'),
    emailEl = document.getElementById('email');

document.getElementById('customerForm').onsubmit = function() {
    let idInt = parseInt(idEl.value);
    // 检查id是否存在
    let objectStore = db.transaction(['customers'], 'readwrite').objectStore('customers');

    objectStore.get(idInt).onsuccess = function() {
        var data = this.result || {
            id: idInt,
            name: nameEl.value,
            age: ageEl.value,
            email: emailEl.value
        };
        var updateRequest = objectStore.put(data);
        updateRequest.onsuccess = function() {
            loadCustomer();
        }
        updateRequest.onerror = function(e) {
            alert(this.error.message);
        }
    }
    return false;
}

首先由于我们在创建存储空间时已经指明 id 作为 keypath,所以需要先检索所增加的 id 是否已经存在,存在更新否则新增。

objectStore.put(data) 表示提交一个对象。

注意由于所有API都是异步,因此 updateRequest 需要监听 onsuccess/onerror 事件。而对于 onerror 最有可能出现异步是索引规则,比如我们之前在创建一个email索引是不允许重复,假设插入的数据跟之前的有重复会收到一个错误消息。

删除数据

let req = db.transaction(['customers'], 'readwrite').objectStore('customers').delete(1);
req.onsuccess = function(e) {
    // 删除成功
}

检索数据

根据 key 检索

let id = 1;
let req = db.transaction(['customers'], 'readwrite').objectStore('customers').get(id);

req.onerror = function(e) {
    alert(`未找到 ${id}`);
}
req.onsuccess = function(e) {
    alert(`${id} 的姓名:${req.result.name} `);
}

根据 name 索引检索 name="asdf" 数据

let objectStore = db.transaction('customers').objectStore('customers');
let index = objectStore.index('name');
index.get('asdf').onsuccess = function(e) {
    alert(JSON.stringify(this.result));
}

根据 email 索引检索所有数据

let objectStore = db.transaction('customers').objectStore('customers');
let index = objectStore.index('email');
index.openCursor().onsuccess = function(e) {
    let cursor = e.target.result;
    if (cursor) {
        alert(JSON.stringify(cursor.value));
        cursor.continue();
    }
}

索引的顺序结果和存储对象空间是不一样的。

删除数据库

indexedDB.deleteDatabase("DemoDb");

而相比较 LocalStorage 可存储空间小、同步API。

参考资料