注:如果你只是想快速体验一下 IndexedDB,建议直接去使用 JsStore 。它基本上完全隐藏了 IndexedDB 繁琐的技术细节,提供的接口也更加人性化,同时还有更高级的特性(如 sql join、upsert、webworker 等)。
作为前端开发工程师,你可能已经习惯了使用一些前端的存储方案,如 LocalStorage, Session Storage, Cookie 之类等等。而这些存储在存储结构简单、数据量小的场景中非常好用,但是遇到大规模结构化存储的场景中就不那么好用了。为此,诞生了一种新的存储标准 IndexedDB,用于在前端解决大规模量结构化数据存储的场景。如服务端数据缓存至前端加速,前端日志缓存记录等。
本文将介绍 IndexedDB 快速入门的相关内容,至于高级使用,仍建议详细阅读 IndexedDB 的官方文档。
如今 IndexedDB 几乎在现代化的浏览器都已经实现,因此可以放心大胆的在产品环境使用。截至本文撰写时间,当前 IndexedDB 的浏览器兼容性参考如下:
IndexedDB 的最大存储容量通常是动态的,通常全局限制为当前磁盘空间的 50%,如浏览器安装分区容量为 500GB,则 IndexedDB 最大可能占用空间为 250GB —— 这对于前端存储来几乎是足够的
还有另一个限制称为组限制——这被定义为全局限制的 20%,但它至少有 10 MB,最大为 2GB。 每个源都是一组(源组)的一部分。 每个 eTLD+1 域都有一个组。 例如:
mozilla.org
——组 1,源 1www.mozilla.org
——组 1,源 2joe.blogs.mozilla.org
——组 1,源 3firefox.com
——组 2,源 4在这个组中,mozilla.org、www.mozilla.org和joe.blogs.mozilla.org可以聚合使用最多20%的全局限制。 firefox.com 单独最多使用 20%。
达到限制后有两种不同的反应:
组限制也称为“硬限制”:它不会触发源回收。 全局限制是一个“软限制”,因为其有可能释放一些空间并且这个操作可能持续。
IndexedDB 使用同源原则,这意味着它把存储空间绑定到了创建它的站点的源(典型情况下,就是站点的域或是子域),所以它不能被任何其他源访问。
IndexedDB 的 API 是基于事件响应全异步性的,使用的时候需要监听对应的事件完成操作。一个基本的创建数据库的操作实例如下:
// 如果被打开的数据库不存在,则会自动创建
var request = window.indexedDB.open("MyTestDatabase");
request.onerror = function (event) {
alert("Why didn't you allow my web app to use IndexedDB?!");
};
request.onsuccess = function (event) {
const db = event.target.result;
// 在不使用 db 的时候尽可能 close掉,否则无法打开一个更高版本的 db,表现形式为 hang 死,无法接收到 onsuccess 事件
// 注意 close() 函数不会立即中断已经在运行中的 transaction
// 但是不能再用这个实例创建新的transaction,否则会抛异常
db.close();
};
open()
函数可以传一个可选的 version
参数指定版本号,当 version 比当前 db.version
高时,会触发 onupgradeneeded
事件,同时,会更新 db.version
属性:
var request = window.indexedDB.open("MyTestDatabase", 2);
// 该事件仅在较新的浏览器中实现了
request.onupgradeneeded = function (event) {
// 保存 IDBDataBase 接口
var db = event.target.result;
// 为该数据库创建一个对象仓库
// 注意 createObjectStore() 只能在 onupgradeneeded 事务中运行
var objectStore = db.createObjectStore("name", { keyPath: "myKey" });
};
为了防止在其他标签页中打开高版本的数据库 hang 住的情况,一种做法可以参考如下:
var openReq = mozIndexedDB.open("MyTestDatabase", 2);
openReq.onblocked = function (event) {
// 如果其他的一些页签加载了该数据库,在我们继续之前需要关闭它们
alert("请关闭其他由该站点打开的页签!");
};
openReq.onupgradeneeded = function (event) {
// 其他的数据已经被关闭,一切就绪
db.createObjectStore(/* ... */);
useDatabase(db);
};
openReq.onsuccess = function (event) {
var db = event.target.result;
useDatabase(db);
return;
};
function useDatabase(db) {
// 当由其他页签请求了版本变更时,确认添加了一个会被通知的事件处理程序。
// 这里允许其他页签来更新数据库,如果不这样做,版本升级将不会发生知道用户关闭了这些页签。
db.onversionchange = function (event) {
db.close();
alert("A new version of this page is ready. Please reload or close this tab!");
};
// 处理数据库
}
const dbName = "the_name";
var request = indexedDB.open(dbName, 2);
request.onerror = function (event) {
// 错误处理
};
request.onupgradeneeded = function (event) {
var db = event.target.result;
// 建立一个对象仓库来存储我们客户的相关信息,我们选择 ssn 作为键路径(key path)
// 因为 ssn 可以保证是不重复的
var objectStore = db.createObjectStore("customers", { keyPath: "ssn" });
// 建立一个索引来通过姓名来搜索客户。名字可能会重复,所以我们不能使用 unique 索引
objectStore.createIndex("name", "name", { unique: false });
// 使用邮箱建立索引,我们向确保客户的邮箱不会重复,所以我们使用 unique 索引
objectStore.createIndex("email", "email", { unique: true });
// 使用事务的 oncomplete 事件确保在插入数据前对象仓库已经创建完毕
objectStore.transaction.oncomplete = function (event) {
// 将数据保存到新创建的对象仓库
var customerObjectStore = db.transaction("customers", "readwrite").objectStore("customers");
customerData.forEach(function (customer) {
// add 函数在 keyPath 冲突时会抛异常,如果期望直接覆盖,请使用 put 函数
customerObjectStore.add(customer);
});
};
};
关于索引: IndexedDB 的索引会自动按照由小到大的顺序自动进行排列(有点像 HBase 的 RowKey 设计),同时可以支持多个字段联合索引,如: objectStore.createIndex(“f1-f2”, [“f1”, “f2”]); 对于要想使用类似于 SQL 中的 where 条件查询,则必须预先在对应的字段创建索引才能实现。另外由于 IndexedDB API 并不提供类似于 Order By 的功能,因此只能使用 cursor 的遍历方向来替代。
// ========= 增加数据 ============
// 在所有数据添加完毕后的处理
transaction.oncomplete = function (event) {
alert("All done!");
};
transaction.onerror = function (event) {
// 不要忘记错误处理!
};
var objectStore = transaction.objectStore("customers");
customerData.forEach(function (customer) {
var request = objectStore.add(customer);
request.onsuccess = function (event) {
// event.target.result === customer.ssn;
};
});
// =========== 删除数据 ==================
var request = db
.transaction(["customers"], "readwrite")
.objectStore("customers")
.delete("444-44-4444");
request.onsuccess = function (event) {
// 删除成功!
};
// =========== 读取数据 ==================
var transaction = db.transaction(["customers"]);
var objectStore = transaction.objectStore("customers");
var request = objectStore.get("444-44-4444");
request.onerror = function (event) {
// 错误处理!
};
request.onsuccess = function (event) {
// 对 request.result 做些操作!
alert("Name for SSN 444-44-4444 is " + request.result.name);
};
// 简化示例,和上面等效
db.transaction("customers").objectStore("customers").get("444-44-4444").onsuccess = function (
event
) {
alert("Name for SSN 444-44-4444 is " + event.target.result.name);
};
// 使用游标
var objectStore = db.transaction("customers").objectStore("customers");
objectStore.openCursor().onsuccess = function (event) {
var cursor = event.target.result;
if (cursor) {
alert("Name for SSN " + cursor.key + " is " + cursor.value.name);
cursor.continue();
} else {
alert("No more entries!");
}
};
// 使用 index 索引数据
// 首先,确定你已经在 request.onupgradeneeded 中创建了索引:
// objectStore.createIndex("name", "name");
// 否则你将得到 DOMException。
var index = objectStore.index("name");
index.get("Donna").onsuccess = function (event) {
alert("Donna's SSN is " + event.target.result.ssn);
};
// =========== 更新数据 ==================
var objectStore = db.transaction(["customers"], "readwrite").objectStore("customers");
var request = objectStore.get("444-44-4444");
request.onerror = function (event) {
// 错误处理
};
request.onsuccess = function (event) {
// 获取我们想要更新的数据
var data = event.target.result;
// 更新你想修改的数据
data.age = 42;
// 把更新过的对象放回数据库
var requestUpdate = objectStore.put(data);
requestUpdate.onerror = function (event) {
// 错误处理
};
requestUpdate.onsuccess = function (event) {
// 完成,数据已更新!
};
};
通过上述的示例,可以看到其实 IndexedDB 存储其实是文档型数据库的套路,并且自身的 API 并不支持像 SQL 那样复杂功能多样的查询功能,因此在使用前必须好好设计存储结构,务必确保自己的查询条件不至于太复杂难以实现,这里列出一些常见的查询示例与 SQL 的对照(所有需要用到的 index 需要事先创建好,这里不再赘述):
// select * from table order by field desc
objectStore.index("field").openCursor(null, "prev");
// select * from table where field between 1 and 10
objectStore.index("field").openCursor(IDBKeyRange.bound(1, 10, false, true));
// select distinct on (f1, f2) * from table
// 需要预先创建f1, f2的联合索引: objectStore.createIndex("f1-f2", ["f1", "f2"], { unique: false });
objectStore.index("f1-f2").openCursor(null, "nextunique");
条件搜索主要使用openCursor(keyRange, direction)
函数,IDBKeyRange 参数表示查询的范围,类似于where
的功能,而direction 决定游标遍历的方向,因此可以实现简单的order by
的功能。但是特别注意对于联合索引,每个字段也都是按照asc
顺序排序的,因此没办法通过 direction 实现类似于order by f1 asc, f2 desc, f3 desc
这样的排序,使用前务必设计好存储结构。
还需要注意无论nextunique
还是prevunique
,由于它们内部对于相同 value 的排序都是从小到大的,因此它们返回的内容都是一样的,例如:
f1 | f2 | f3 |
---|---|---|
“a” | “b” | “c” |
“a” | “b” | “d” |
假设创建objectStore.createIndex("f1-f2", ["f1", "f2"], { unique: false })
索引,无论openCursor
的 direction 参数是nextunique
还是prevunique
,都只会筛选出{"f1":"a","f2":"b","f3":"c"}
这条结果,cursor 的遍历顺序只会影响这条记录出现的次序而已。
IndexedDB API 本身并不提供 Promise 接口,如果你的场景确实需要用到 Promise,可以考虑自己简单封装下:
new Promise(resolve, reject) {
const request = window.indexedDB.open("MyTestDatabase");
request.onerror = function (event) {
return reject(event);
}
request.onsuccess = function (event) {
const db = event.target.result;
const getReq = db
.transaction("customers")
.objectStore("customers")
.get("444-44-4444")
getReq.onsuccess = function (event) {
return resolve(event.target.result.name);
};
getReq.onerror = function (event) {
return reject(event)
}
}
}
如果需要大量使用 Promise,可以考虑使用第三方类库idb或更精简的版本idb-keyval,它们将 IndexedDB 的 API 进行了 Promise 的封装,其中idb-keyval对 KV 对这种存储结构进行了更简化的封装调用,对于自己的场景只有简单的 KV 存储这种场景可以考虑使用
觉得有帮助的话,不妨考虑购买付费文章来支持我们 🙂 :
付费文章