客户端存储 | IndexedDB

什么是indexedDB

indexedDB是浏览器结构化存储的一种方式,用于代替目前已经废弃的Web SQL Database API

indexedDB是类似于MySQL或者Web SQL Database的数据库,但是**IndexDB使用对象而不是表格保存数据,因此,可以说indexedDB是在一个公共空间下的一组对象存储,类似于NoSQL风格的实现

如何使用IndexDB数据库

使用indexedDB数据库可通过如下步骤(基础):

  • Step1:调用indexedDB.open()方法
  • Step2:对象存储
  • Step3:调用事务进行增删改查

Step1:调用indexedDB.open()方法

indexedDB.open()方法用于打开数据库(如果数据库不存在,则为创建)

参数说明

  • 要打开的数据库名称(必选)
  • 指定版本号(可选)

返回值说明

返回一个IDBRequest的实例,可以在这个实例上添加onerroronsuccess事件处理程序,这两个事件处理程序的event.target都指向这个实例

  • 如果打开的时候发生错误,就会在event.target.errorCode中存储错误码
  • 如果打开成功,就可以通过event.target.result访问数据库
1
2
3
4
5
6
7
8
9
10
11
let db,
request,
version = 1;

request = indexedDB.open('admin', version); // 打开admin这个数据库
request.onerror = (event) => {
console.log(`Failed to open: ${event.target.errorCode}`);
};
request.onsuccess = (event) => {
db = event.target.result;
};

Step2:对象存储

对象存储你需要准备:

  • 指定一个键,必须是全局唯一的
1
2
3
4
5
6
let user = {
username: '007',
firstName: 'Katrina',
lastName: 'Ying',
password: 'foo',
}

假设要存储这样一个对象,很显然username可以用来做主键

  • upgradeneeded事件用于监听更新数据库
1
2
3
4
5
6
7
8
9
10
request.onupgradeneeded = (event) => {
const db = event.target.result;

// 如果已经存在则删除当前的objectStore
if (db.objectStoreNames.contains('users')) {
db.deleteObjectStore('users');
}

db.createObjectStore('users', {keyPath: 'username'});
}

Step3:通过事务进行后序操作

在对象存储之后,剩下的所有操作都是通过事务完成的

  • 插入对象:add put
  • 删除对象:delete (键作为参数)
  • 获取对象:get(键作为参数)
  • 清空对象:clear
1. 创建事务 db.transaction()
  • 没有指定名称——数据库中所有的对象都只有只读权限
  • 访问一个或者多个数据库——传入数据库名称,单个直接传入,多个以数组的形式传入
  • 传入第二个参数以修改访问模式——readonly readwrite versionchange
1
2
3
4
5
6
7
8
9
// 没有指定名称,数据库中所有的对象都只有只读权限
let transaction = db.transaction();

// 指定名称
let transaction = db.transaction('users');
let transaction = db.transaction(['users', 'admin']);

// 指定第二个参数
let transaction = db.transaction('users', 'readwrite');
2. 事务事件处理程序绑定
1
2
3
4
5
6
const transaction = db.transaction('users'),
store = transaction.objectStore('users'),
request = store.get('007');

request.onerror = (event) => alert("Did not get the object!");
request.onsuccess = (event) => alert(event.target.result.firstName);

因为一个事务可以完成任意多个请求,所以事务对象本身也有事件处理程序:onerroroncomplete,这两个事件可以用来获取事务级的状态信息

1
2
3
4
5
6
transaction.onerror = (event) => {
// 整个事务被取消
};
transaction.oncomplete = (event) => {
// 整个事务成功完成
};

注意,不能通过oncomplete事件处理程序的event对象访问get()请求返回的任何数据,因此,仍然需要通过这些请求的onsuccess事件处理程序来获取数据

插入数据

插入数据的方式有:

  • add(增加新值)
  • put(修改,更新)

这两个方法都接收一个参数:要存储的对象

两者的区别在于:传入对象存储已经包含同名键时,add会报错,而put是重写对象

1
2
3
for (let user of users) {
store.add(user);
}

每次调用add()put()都会创建对象存储的新更新请求

如果想验证请求成功与否,可以把请求对象保存到一个变量,然后为它添加onerror onsuccess 事件处理程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let request,
requests = [];

for (let user of users) {
request = store.add(user);

request.onerror = () => {
// 处理错误程序
};

request.onsuccess = () => {
// 处理成功
};

requests.push(request);
}

通过游标查询

使用事务可以通过一个已知键取得一条记录,如果想取得多条数据,则需要在事务中创建一个游标

游标是一个指向结果集的指针

与传统数据库查询不同,游标不会事先收集所有结果,相反,游标指向第一个结果,并在接到指令前不会主动查找下一条数据

创建游标 openCursor()

可以接收两个参数:(后面讲!!!)

  • 键范围
  • 游标方向

openCursor()也返回一个请求,所以必须为它添加onsuccess和onerror事件处理程序

1
2
3
4
5
6
7
8
9
10
11
const transaction = db.transaction('users'),
store = transaction.objectStore('users'),
request = store.openCursor();

request.onerror = () => {
// 处理错误程序
};

request.onsuccess = () => {
// 处理成功
};
  1. 在调用onsuccess事件处理程序时,可以通过event.target.result访问对象存储中的下一条记录,这个属性中保存着IDBCursor的实例(有下一条记录时)或null(没有记录时)

    实例有如下属性:

    • direction:字符串常量,表示游标的前进方向以及是否应该遍历所有重复的值。
      • 可能的值包括:NEXT(“next”)、NEXTUNIQUE(“nextunique”)、PREV(“prev”)、PREVUNIQUE(“prevunique”)
    • key:对象的键
    • value:实际的对象
    • primaryKey:游标使用的键,可能是对象键或索引键
    1
    2
    3
    4
    5
    6
    request.onsuccess = (event) => {
    const cursor = event.target.result;
    if (cursor) { // 永远要检查!
    console.log(`Key:${cursor.key},Value:${JSON.stringify(cursor.value)}`)
    }
    }
  2. 游标可用于更新个别记录——update()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
request.onsuccess = (event) => {
const cursor = event.target.result;
let value,
updateRequest;
if (cursor) { // 永远要检查
if (cursor.key == "foo") {
value = cursor.value; // 取得当前对象
value.password = "magic!"; // 更新密码
updateRequest = cursor.update(value); // 请求保存更新后的对象
updateRequest.onsuccess = () => {
// 处理成功
};
updateRequest.onerror = () => {
// 处理错误
};
}
}
};
  1. 可以删除游标位置的记录_delete()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
request.onsuccess = (event) => {
const cursor = event.target.result;
let value,
deleteRequest;
if (cursor) { // 永远要检查
if (cursor.key == "foo") {
deleteRequest = cursor.delete(); // 请求删除对象
deleteRequest.onsuccess = () => {
// 处理成功
};
deleteRequest.onerror = () => {
// 处理错误
};
}
}
};

注意:如果事务没有修改对象存储的权限,那么update和delete都会报错

游标迭代

默认情况下,每个游标只会创建一个请求

要创建另一个请求,必须调用下列中的一个方法

  • continue(key):移动到结果集中的下一条记录,参数key 是可选的,如果没有指定key,游标就移动到下一条记录;如果指定了,则游标移动到指定的键
  • advance(count):游标向前移动指定的count 条记录,这两个方法都会让游标重用相同的请求,因此也会重用onsuccess 和onerror 处理程序,直至不再需要
1
2
3
4
5
6
7
8
9
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) { // 永远要检查
console.log(`Key: ${cursor.key}, Value: ${JSON.stringify(cursor.value)}`);
cursor.continue(); // 移动到下一条记录
} else {
console.log("Done!");
}
};

键范围

可以使用键范围更容易地管理游标

键范围对应IDBKeyRange的实例

有四种方式指定键范围

  • 方式1:only
1
const onlyRange = IDBKeyRange.only('007');

这个范围保证只获取键为”007”的值

使用这个范围创建的游标类似于直接访问对象存储并调用get(“007”)

  • 方式2:lowerBound

定义结果集的下限,下限表示游标开始的位置

1
const lowerRange = IDBKeyRange.lowerRange('007');   // 保证游标从"007"这个键开始
  • 方式3:upperBound

定义结果集的上限,通过调用upperBound()方法可以指定游标不会越过的记录,如果不想包含指定的键,可以在第二个参数传入true

1
2
3
4
5
// 从头开始,到"ace"记录为止
const upperRange = IDBKeyRange.upperBound("ace");

// 从头开始,到"ace"的前一条记录为止
const upperRange = IDBKeyRange.upperBound("ace", true);
  • 方式4:bound

同时指定下限和上限,这个方法接收四个参数:下限的键、上限的键、可选的布尔值表示是否跳过下限和可选的布尔值表示是否跳过上限

1
2
3
4
5
6
7
8
// 从"007"记录开始,到"ace"记录停止
const boundRange = IDBKeyRange.bound("007", "ace");
// 从"007"的下一条记录开始,到"ace"记录停止
const boundRange = IDBKeyRange.bound("007", "ace", true);
// 从"007"的下一条记录开始,到"ace"的前一条记录停止
const boundRange = IDBKeyRange.bound("007", "ace", true, true);
// 从"007"记录开始,到"ace"的前一条记录停止
const boundRange = IDBKeyRange.bound("007", "ace", false, true);

游标范围设置——传入openCursor

1
2
3
4
5
6
7
8
9
10
11
12
13
const store = db.transaction("users").objectStore("users"),
range = IDBKeyRange.bound("007", "ace");

request = store.openCursor(range);
request.onsuccess = function(event){
const cursor = event.target.result;
if (cursor) { // 永远要检查
console.log(`Key: ${cursor.key}, Value: ${JSON.stringify(cursor.value)}`);
cursor.continue(); // 移动到下一条记录
} else {
console.log("Done!");
}
};

设置游标方向

游标方向:

  • next(默认):从第一条到最后一条,不跳过重复项
  • nextunique:从第一条到最后一条,跳过重复项
  • prev:从最后一条到第一条,不跳过重复项
  • prevunique:从最后一条到第一条,跳过重复项

索引

创建新索引 createIndex()

参数说明

  • 索引名称
  • 索引属性名称
  • 包含键unique的options对象
    • unique必须指定,表示这个键是否在所在记录里面唯一
1
2
3
const transaction = db.transaction("users"),
store = transaction.objectStore("users"),
index = store.createIndex("username", "username", { unique: true });

返回值说明

返回IDBIndex实例

1
2
3
const transaction = db.transaction("users"),
store = transaction.objectStore("users"),
index = store.index("username"); // 使用索引
  1. 可以在索引上使用openCursor()方法创建新游标,这个游标与在对象存储上调用openCursor()创建的游标完全一样,只是其result.key 属性中保存的是索引键,而不是主键

    1
    2
    3
    4
    5
    6
    7
    8
    const transaction = db.transaction("users"),
    store = transaction.objectStore("users"),
    index = store.index("username"),
    request = index.openCursor();

    request.onsuccess = (event) => {
    // 处理成功
    };
  2. 使用openKeyCursor()方法也可以在索引上创建特殊游标,只返回每条记录的主键

    这个方法接收的参数与openCursor()一样

    最大的不同在于,event.result.key 是索引键,且event.result.value是主键而不是整个记录

    1
    2
    3
    4
    5
    6
    7
    8
    9
    const transaction = db.transaction("users"),
    store = transaction.objectStore("users"),
    index = store.index("username"),
    request = index.openKeyCursor();

    request.onsuccess = (event) => {
    // 处理成功
    // event.result.key 是索引键,event.result.value 是主键
    };
  3. 如果想只取得给定索引键的主键,可以使用getKey()方法,这样也会创建一个新请求,但result.value 等于主键而不是整个记录

    1
    2
    3
    4
    5
    6
    7
    8
    9
    const transaction = db.transaction("users"),
    store = transaction.objectStore("users"),
    index = store.index("username"),
    request = index.getKey("007");

    request.onsuccess = (event) => {
    // 处理成功
    // event.target.result.key 是索引键,event.target.result.value 是主键
    };

    在这个onsuccess 事件处理程序中,event.target.result.value 中应该是用户ID

    任何时候,都可以使用IDBIndex 对象的下列属性取得索引的相关信息

    • name:索引的名称
    • keyPath:调用createIndex()时传入的属性路径
    • objectStore:索引对应的对象存储
    • unique:表示索引键是否唯一的布尔值

    对象存储自身也有一个indexNames 属性,保存着与之相关索引的名称

    使用如下代码可以方便地了解对象存储上已存在哪些索引

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    const transaction = db.transaction("users"),
    store = transaction.objectStore("users"),
    indexNames = store.indexNames

    for (let indexName in indexNames) {
    const index = store.index(indexName);
    console.log(`Index name: ${index.name}
    KeyPath: ${index.keyPath}
    Unique: ${index.unique}`);
    }
  4. 在对象存储上调用deleteIndex()方法并传入索引的名称可以删除索引

    1
    2
    3
    const transaction = db.transaction("users"),
    store = transaction.objectStore("users"),
    store.deleteIndex("username");

    因为删除索引不会影响对象存储中的数据,所以这个操作没有回调

并发问题

IndexedDB虽然是网页中的异步API,但仍存在并发问题

如果两个不同的浏览器标签页同时打开了同一个网页,则有可能出现一个网页尝试升级数据库而另一个尚未就绪的情形

有问题的操作是设置数据库为新版本,而版本变化只能在浏览器只有一个标签页使用数据库时才能完成

第一次打开数据库时,添加onversionchange 事件处理程序非常重要,另一个同源标签页将数据库打开到新版本时,将执行此回调,对这个事件最好的回应是立即关闭数据库,以便完成版本升级

1
2
3
4
5
6
let request, database;
request = indexedDB.open("admin", 1);
request.onsuccess = (event) => {
database = event.target.result;
database.onversionchange = () => database.close();
};

应该在每次成功打开数据库后都指定onversionchange事件处理程序

记住,onversionchange有可能会被其他标签页触发

通过始终都指定这些事件处理程序,可以保证Web应用程序能够更好地处理与IndexedDB相关的并发问题

限制

  • IndexedDB 数据库是与页面源(协议、域和端口)绑定的,因此信息不能跨域共享。这意味着www.wrox.com 和p2p.wrox.com 会对应不同的数据存储
  • 每个源都有可以存储的空间限制。当前Firefox 的限制是每个源50MB,而Chrome 是5MB,移动版Firefox 有5MB 限制,如果用度超出配额则会请求用户许可
  • Firefox 还有一个限制——本地文本不能访问IndexedDB 数据库,Chrome 没有这个限制。