IndexedDB介绍
IndexedDB标准是HTML5官方认可的本地数据库解决方案。其目的不是取代服务器端数据库,它在一些特定场景下很有用:
-
创建自给自足的离线应用
比如页面可以在有网络连接的时候从服务器端数据库获取所需要的数据,然后将数据保存到本地数据库,以便离线时访问。
-
优化性能
一些应用使用大量的数据,如果持续地在需要时获取同样的数据,网页性能会下降。同样的,创建一个通过复杂计算生成数据的应用,如果不想浪费时间重复做同样的计算,我们可以将所有需要的数据保存到一个IndexedDB数据库中。让它成为超级定制缓存。
-
改进本地存储
要保存浏览器会话间的数据,并在页面之间传递。如果数据结构简单并且不大,使用本地存储就能胜任。但如果数据非常巨大或者结构非常复杂,使用IndexedDB数据库来管理就会更加简单快捷。
IndexedDB特点
-
键值对储
IndexedDB内部采用对象仓库(object store)存放数据。所有类型的数据都可以直接存入,包括JavaScript对象。在对象仓库中,数据以“键值对”的形式保存,每一个数据都有对应的键名,键名是独一无二的,不能有重复,否则会抛出一个错误。
-
异步
IndexedDB操作时不会锁死浏览器,用户依然可以进行其他操作,这与localStorage形成对比,后者的操作是同步的。异步设计是为了防止大量数据的读写,拖慢网页的表现。
-
支持事务
IndexedDB支持事务(transaction),这意味着一系列操作步骤之中,只要有一步失败,整个事务就都取消,数据库回到事务发生之前的状态,不存在只改写一部分数据的情况。
-
同域限制
IndexedDB也受到同域限制,每一个数据库对应创建该数据库的域名。来自不同域名的网页,只能访问自身域名下的数据库,而不能访问其他域名下的数据库。
-
储存空间大
IndexedDB的储存空间比localStorage大得多,一般来说不少于250MB。IE的储存上限是250MB,Chrome和Opera是剩余空间的某个百分比,Firefox则没有上限。
-
支持二进制储存
IndexedDB不仅可以储存字符串,还可以储存二进制数据。
使用数据库
一个数据库一次只能有一个版本。在首次创建数据库时,它的初始版本编号为 0。创建数据库之后,数据库(和它的对象存储)只能通过一种称为 versionchange 的特殊类型的事务来更改。要在创建数据库后更改它,必须打开具有更高版本的数据库。此操作会触发 upgradeneeded 事件。修改数据库或对象存储的代码必须位于 upgradeneeded 事件处理函数中。
-
创建数据库
function createDatabase() {
var openRequest = indexedDB.open(dbName);
openRequest.onerror = function(e) {
console.log("Database error: " + e.target.errorCode);
};
openRequest.onsuccess = function(event) {
console.log("Database created");
localDatabase.db = openRequest.result;
};
openRequest.onupgradeneeded = function (evt) {
...
};
}
-
删除数据库
function deleteDatabase() {
var deleteDbRequest = indexedDB.deleteDatabase(dbName);
deleteDbRequest.onsuccess = function (event) {
// database deleted successfully
};
deleteDbRequest.onerror = function (e) {
console.log("Database error: " + e.target.errorCode);
};
}
-
打开数据库
function openDatabase() {
var openRequest = indexedDB.open("dbName");
openRequest.onerror = function(e) {
console.log("Database error: " + e.target.errorCode);
};
openRequest.onsuccess = function(event) {
localDatabase.db = openRequest.result;
};
}
使用对象存储
对象存储是一个数据记录集合。要在现有数据库中创建一个新对象存储,则需要对现有数据库进行版本控制。为此,请打开要进行版本控制的数据库。除了数据库名称之外,open 方法还接受版本号作为第二个参数。如果希望创建数据库的一个新版本(也就是说,要创建或修改一个对象存储),只需打开具有现有数据库版本更高的数据库。这会调用 onupgradeneeded 事件处理函数。
创建对象存储
function createObjectStore() {
var openRequest = indexedDB.open(dbName, 2);
openRequest.onerror = function(e) {
console.log("Database error: " + e.target.errorCode);
};
openRequest.onsuccess = function(event) {
localDatabase.db = openRequest.result;
};
openRequest.onupgradeneeded = function (evt) {
var employeeStore = evt.currentTarget.result.createObjectStore
("employees", {keyPath: "id"});
};
}
使用索引
除了使用键来检索对象存储中的记录,还可使用代索引的字段来检索记录。对象存储可具有一个或多个索引。索引是一种特殊的对象存储,它引用包含数据的对象存储,在更改所引用的对象存储时(也就是添加、修改或删除记录时)自动更新。
function createIndex() {
var openRequest = indexedDB.open(dbName, 2);
openRequest.onerror = function(e) {
console.log("Database error: " + e.target.errorCode);
};
openRequest.onsuccess = function(event) {
db = openRequest.result;
};
openRequest.onupgradeneeded = function (evt) {
var employeeStore = evt.currentTarget.result.objectStore("employees");
employeeStore.createIndex("stateIndex", "state", { unique: false });
employeeStore.createIndex("emailIndex", "email", { unique: true });
employeeStore.createIndex("zipCodeIndex", "zip_code", { unique: false })
};
}
使用事务
您需要使用事务在对象存储上执行所有读取和写入操作。类似于关系数据库中的事务的工作原理,IndexedDB 事务提供了数据库写入操作的一个原子集合,这个集合要么完全提交,要么完全不提交。IndexedDB 事务还拥有数据库操作的一个中止和提交工具。
IndexedDB 事务模式:
readonly:提供对某个对象存储的只读访问,在查询对象存储时使用。
readwrite:提供对某个对象存储的读取和写入访问权。
versionchange:提供读取和写入访问权来修改对象存储定义,或者创建一个新的对象存储。
默认的事务模式为 readonly。您可在任何给定时刻打开多个并发的 readonly 事务,但只能打开一个 readwrite 事务。出于此原因,只有在数据更新时才考虑使用 readwrite 事务。单独的(表示不能打开任何其他并发事务)versionchange 事务操作一个数据库或对象存储。可以在 onupgradeneeded 事件处理函数中使用 versionchange 事务创建、修改或删除一个对象存储,或者将一个索引添加到对象存储。
-
使用键获取一个特定的记录
function fetchEmployee() {
try {
var result = document.getElementById("result");
result.innerHTML = "";
if (localDatabase != null && localDatabase.db != null) {
var store = localDatabase.db.transaction("employees").objectStore("employees");
store.get("E3").onsuccess = function(event) {
var employee = event.target.result;
if (employee == null) {
result.value = "employee not found";
}
else {
var jsonStr = JSON.stringify(employee);
result.innerHTML = jsonStr;
}
};
}
}
catch(e){
console.log(e);
}
}
-
使用索引获取特定的记录
function fetchEmployeeByEmail() {
try {
var result = document.getElementById("result");
result.innerHTML = "";
if (localDatabase != null && localDatabase.db != null) {
var range = IDBKeyRange.only("john.adams@somedomain.com");
var store = localDatabase.db.transaction("employees")
.objectStore("employees");
var index = store.index("emailIndex");
index.get(range).onsuccess = function(evt) {
var employee = evt.target.result;
var jsonStr = JSON.stringify(employee);
result.innerHTML = jsonStr;
};
}
}
catch(e){
}
-
创建一条新员工记录
function addEmployee() {
try {
var result = document.getElementById("result");
result.innerHTML = "";
var transaction = localDatabase.db.transaction("employees", "readwrite");
var store = transaction.objectStore("employees");
if (localDatabase != null && localDatabase.db != null) {
var request = store.add({
"id": "E5",
"first_name" : "Jane",
"last_name" : "Doh",
"email" : "jane.doh@somedomain.com",
"street" : "123 Pennsylvania Avenue",
"city" : "Washington D.C.",
"state" : "DC",
"zip_code" : "20500",
});
request.onsuccess = function(e) {
result.innerHTML = "Employee record was added successfully.";
};
request.onerror = function(e) {
console.log(e.value);
result.innerHTML = "Employee record was not added.";
};
}
}
catch(e){
console.log(e);
}
}
-
更新现有的员工记录
function updateEmployee() {
try {
var result = document.getElementById("result");
result.innerHTML = "";
var transaction = localDatabase.db.transaction("employees", "readwrite");
var store = transaction.objectStore("employees");
var jsonStr;
var employee;
if (localDatabase != null && localDatabase.db != null) {
store.get("E3").onsuccess = function(event) {
employee = event.target.result;
// save old value
jsonStr = "OLD: " + JSON.stringify(employee);
result.innerHTML = jsonStr;
// update record
employee.email = "john.adams@anotherdomain.com";
var request = store.put(employee);
request.onsuccess = function(e) {
console.log("Added Employee");
};
request.onerror = function(e) {
console.log(e.value);
};
// fetch record again
store.get("E3").onsuccess = function(event) {
employee = event.target.result;
jsonStr = "
NEW: " + JSON.stringify(employee);
result.innerHTML = result.innerHTML + jsonStr;
}; // fetch employee again
}; // fetch employee first time
}
}
catch(e){
console.log(e);
}
}
-
清除对象存储事务
function clearAllEmployees() {
try {
var result = document.getElementById("result");
result.innerHTML = "";
if (localDatabase != null && localDatabase.db != null) {
var store = localDatabase.db.transaction("employees", "readwrite")
.objectStore("employees");
store.clear().onsuccess = function(event) {
result.innerHTML = "'Employees' object store cleared";
};
}
}
catch(e){
console.log(e);
}
}
使用游标
类似于关系数据库中游标的工作方式,IndexedDB 中的游标使您能够迭代一个对象存储中的记录。您还可以使用对象存储的索引来迭代记录。IndexedDB 中的游标是双向的,所以您可向前和向后迭代记录,还可以跳过非惟一索引中的重复记录。openCursor 方法可以打开一个游标。它接受两个可选的参数,其中包括范围和方向。
IndexedDB API 在为索引打开游标时提供的范围类型或过滤器
IDBKeyRange.bound:返回指定范围内的所有记录。这个范围有一个下边界和上边界。它还有两个可选的参数:lowerOpen 和 upperOpen,这两个参数表明下边界或上边界上的记录是否应包含在范围内。
IDBKeyRange.lowerBound:超过指定的边界值范围的所有记录。此范围有一个可选的参数 lowerOpen,表明下边界上的记录是否应包含在范围中。
IDBKeyRange.upperBound:返回指定的边界值之前的所有记录。它也有一个可选的 upperOpen 参数。
IDBKeyRange.only:仅返回与指定值匹配的记录。
-
迭代所有员工记录
function fetchAllEmployees() {
try {
var result = document.getElementById("result");
result.innerHTML = "";
if (localDatabase != null && localDatabase.db != null) {
var store = localDatabase.db.transaction("employees")
.objectStore("employees");
var request = store.openCursor();
request.onsuccess = function(evt) {
var cursor = evt.target.result;
if (cursor) {
var employee = cursor.value;
var jsonStr = JSON.stringify(employee);
result.innerHTML = result.innerHTML + "
" + jsonStr;
cursor.continue();
}
};
}
}
catch(e){
console.log(e);
}
}
-
迭代纽约市内的所有员工记录
function fetchNewYorkEmployees() {
try {
var result = document.getElementById("result");
result.innerHTML = "";
if (localDatabase != null && localDatabase.db != null) {
var range = IDBKeyRange.only("New York");
var store = localDatabase.db.transaction("employees")
.objectStore("employees");
var index = store.index("stateIndex");
index.openCursor(range).onsuccess = function(evt) {
var cursor = evt.target.result;
if (cursor) {
var employee = cursor.value;
var jsonStr = JSON.stringify(employee);
result.innerHTML = result.innerHTML + "
" + jsonStr;
cursor.continue();
}
};
}
}
catch(e){
console.log(e);
}
}
-
使用 IDBKeyRange.lowerBound
function fetchEmployeeByZipCode1() {
try {
var result = document.getElementById("result");
result.innerHTML = "";
if (localDatabase != null && localDatabase.db != null) {
var store = localDatabase.db.transaction("employees").objectStore("employees");
var index = store.index("zipIndex");
var range = IDBKeyRange.lowerBound("92000");
index.openCursor(range).onsuccess = function(evt) {
var cursor = evt.target.result;
if (cursor) {
var employee = cursor.value;
var jsonStr = JSON.stringify(employee);
result.innerHTML = result.innerHTML + "
" + jsonStr;
cursor.continue();
}
};
}
}
catch(e){
console.log(e);
}
}
-
使用 IDBKeyRange.upperBound
function fetchEmployeeByZipCode2() {
try {
var result = document.getElementById("result");
result.innerHTML = "";
if (localDatabase != null && localDatabase.db != null) {
var store = localDatabase.db.transaction("employees").objectStore("employees");
var index = store.index("zipIndex");
var range = IDBKeyRange.upperBound("93000");
index.openCursor(range).onsuccess = function(evt) {
var cursor = evt.target.result;
if (cursor) {
var employee = cursor.value;
var jsonStr = JSON.stringify(employee);
result.innerHTML = result.innerHTML + "
" + jsonStr;
cursor.continue();
}
};
}
}
catch(e){
console.log(e);
}
}
-
使用 IDBKeyRange.bound
function fetchEmployeeByZipCode3() {
try {
var result = document.getElementById("result");
result.innerHTML = "";
if (localDatabase != null && localDatabase.db != null) {
var store = localDatabase.db.transaction("employees").objectStore("employees");
var index = store.index("zipIndex");
var range = IDBKeyRange.bound("92000", "92999", true, true);
index.openCursor(range).onsuccess = function(evt) {
var cursor = evt.target.result;
if (cursor) {
var employee = cursor.value;
var jsonStr = JSON.stringify(employee);
result.innerHTML = result.innerHTML + "
" + jsonStr;
cursor.continue();
}
};
}
}
catch(e){
console.log(e);
}
}
这些示例表明,IndexedDB 中的游标功能与关系数据库中的游标功能类似。使用 IndexedDB 游标,可迭代一个对象存储中的记录和对象存储的某个索引的记录。IndexedDB 中的游标是双向的,这提供了额外的灵活性。
idb:IndexedDB的封装库,简化IndexedDB的使用
const dbPromise = idb.open('keyval-store', 1, upgradeDB => {
upgradeDB.createObjectStore('keyval');
});
const idbKeyval = {
get(key) {
return dbPromise.then(db => {
return db.transaction('keyval')
.objectStore('keyval').get(key);
});
},
set(key, val) {
return dbPromise.then(db => {
const tx = db.transaction('keyval', 'readwrite');
tx.objectStore('keyval').put(val, key);
return tx.complete;
});
},
delete(key) {
return dbPromise.then(db => {
const tx = db.transaction('keyval', 'readwrite');
tx.objectStore('keyval').delete(key);
return tx.complete;
});
},
clear() {
return dbPromise.then(db => {
const tx = db.transaction('keyval', 'readwrite');
tx.objectStore('keyval').clear();
return tx.complete;
});
},
keys() {
return dbPromise.then(db => {
const tx = db.transaction('keyval');
const keys = [];
const store = tx.objectStore('keyval');
// This would be store.getAllKeys(), but it isn't supported by Edge or Safari.
// openKeyCursor isn't supported by Safari, so we fall back
(store.iterateKeyCursor || store.iterateCursor).call(store, cursor => {
if (!cursor) return;
keys.push(cursor.key);
cursor.continue();
});
return tx.complete.then(() => keys);
});
}
};
具体用法请参考https://github.com/jakearchibald/idb
浏览器对IndexedDB的支持情况
-
IE10起支持
-
Firefox 10、Chrome 23、Opera 15
-
最新版的桌面版或移动版的Safari也支持IndexedDB