IndexedDB
- Indexed Database API 简称IndexedDB,是浏览器存储结构化数据的方案,替代了已经废弃的Web SQL Database API,IndexDB的思想是创建一套API用于js对象的存储和获取,同时支持查询和搜索。
- 它的设计完全是异步的,大部分操作都是用请求的方法获取执行,这些请求都是会异步执行,产生的结果是错误或者正确的,绝大多数indexDB操作要添加onerror和onsucces事件处理方式来处理程序来确定输出。
- 主流的浏览器都支持indedb,比如:Chrome,Firefox,Opera,Safari完全支持IndexDB,IE10/IE11和Edge部分支持
数据库
- indexdb的设计不是像mysql一样,像表格一样存储,而是像NoSQL的设计一样,在公共命名空间先开一个对象存储
- 使用Indexdb
//调用indexeddb.open并传入版本号 注意这里的版本号会被转化成 unsigned long long 类型参数 所以不要写成小数
//数据库版本决定数据的模式
let db,request ,version = 1;
request = indexedDB.open("admin",version);
request.onerror = (event) =>{
alert("出现错误 ${event.target.errorCode}")
}
request.onsuccess = (event) =>{
db = event.target.result;
}
//注意这里event.target指向的都是request
indexedDB是模式版本化数据库,数据结构上很灵活但数据库结构是强制管理的,必须通过版本升级显示修改
对象存储
创建一个对象
let user = {
username: "007",
firstname: "Jamas",
lastname: "Boon",
password: "123456"
}
存储是必须指定一个键作为keyPath属性
下面写一下存储对象的过程
//open操作创建了一个数据库 理解出发事件upgradeneeded
request.onupgradeneeded = (event) => {
const db = event.target.result;
//如果存在就删除这个objectStore 测试的时候可以这么做
//但是这么做会删除这个数据
if(db.objectStoreName.contains("user") {
db.deleteObjectStore("users");
}
db.createObjectStore(“user”,{keyPath: "username"})
}
db.objectStoreNames 是一个类似数组的DOMStringList,他列出当前数据库所有对象存储名称,不是数组但是又cantain方法,只能在onupgradeneeded回调中使用,这是indexedDB内部维护的一个只读列表,表示当前版本下的数据库有哪些对象存储
事务
- 提到数据库就不得不提事务,事务是数据库数据一致性,安全性,可靠性的核心机制
- IndexedDB中的事务和MySQL,NoSQL一样,都是为了确保要么一系列操作全部完成,要么一个不做
- 创建了对象存储之后一切的操作都要事务完成,通过调用对象的
transaction()方法创建,想要修改删除数据都要靠事务组织起来
let transaction = db.transation("users")
- 这样就能实现在操作事务期间只加载该对象存储信息(不传参就是加载所有当前版本的对象,想加载多个对象就传入字符串数组)
- 每个事件都是以只读的访问方式,想修改模式必须传入第二个参数
readonly readwrite versionchange
- 有了事件的引用 就能通过objectStore传入存储对象名称,访问特定的对象存储
const transation = db.transaction("users)
const store = transaction.objectStore("users")
request = store.get("007")
request.onerror = (event) =>{
alert("没找到")
}
request.onsuccess = (event)=>{
alert(event.target.result.firstname)
}
- 因为事务可以完成任意多个请求 =,所以事务也有自己的事件处理程序
transaction.onerror = (event)=>{
}
transaction.oncomplete = (event)=>{
}
注意不能通过oncomplete事件处理event对象访问get()请求返回的任何数据,因此仍然需要通过这些请求的onsuccess处理数据返回结结果 因为他只是告诉你事务完成了,但是不能拿到get()的结构
const tx = db.transaction("user", "readonly");
const store = tx.objectStore("user");
const request = store.get("alice");
tx.oncomplete = function(event) {
//错误 这里拿不到 request.result
console.log("result:", request.result); // 可能为 undefined!
};
const tx = db.transaction("user", "readonly");
const store = tx.objectStore("user");
const request = store.get("alice");
request.onsuccess = function(event) {
console.log("查到的数据是:", request.result);
};
事务在indexedDB是怎么实现的
indexedDB 实现事务(transaction)是通过浏览器内部机制控制的一种轻量级、本地原子操作系统,底层采用了写前日志(Write-Ahead Logging, WAL)和版本快照机制,实现了事务的核心特性(原子性、一致性、隔离性、持久性),即我们常说的 ACID。
虽然它不像关系型数据库那样复杂,但它的事务机制在本地数据库中是非常严格的。下面我们从机制、原理和实际表现来说明它是如何工作的。
- 写前日志
写前日志,所有的数据都不会立即写入磁盘而是创建一个临时区域(写前日志)
- 先写一块临时区域
- 所有操作完成之后才会提交到主数据存储
- 如果失败或者取消,事务就会回滚,主数据不会改变
- 快照隔离
只读事务中
IndexedDB创建数据快照
- 事务期间读到的数据是那一刻的数据
- 即使其他事务修改了当时对象存储的内容,读到的内容依然不会发生改变,体现了隔离性,类似于数据库的多版本并发控制
- 自动提交或者回滚
- 所有操作都是自动提交的,如果返回的请求是onsucess,事务就会主动提交
- 如果任何操作都会触发onerror或抛出异常,事务就会回滚
下面一个表格总结一下
| 特性 | IndexedDB 是怎么实现的? |
|---|---|
| 原子性(Atomicity) | 所有操作要么都成功,要么都不生效(通过 WAL 和回滚机制) |
| 一致性(Consistency) | 结构操作必须在 onupgradeneeded 中做,确保 schema 一致 |
| 隔离性(Isolation) | 每个事务有自己的视图,不受其他事务干扰(快照隔离) |
| 持久性(Durability) | 提交后的操作会被持久写入本地(IndexedDB 是持久存储) |
浏览器内部是怎么构建IndexedDB事务的
- 其本身是一个Web API,实际存储室友浏览器底层数据库实现的 | 浏览器内核 | IndexedDB 实现依赖 | | ---------------------------------- | --------------------------------- | | Chromium (Chrome, Edge, Opera) | 使用 LevelDB(Google 自家高性能键值数据库) | | Firefox | 使用 SQLite | | Safari | 使用 自研 SQLite 封装 |
创建一个事务
const tx = db.transaction("users", "readwrite");
-
Blink(浏览器渲染引擎) 拦截这个 API 调用; -
把事务和对象存储的请求封装成消息发往
Browser Process;
3.Browser Process中的 IndexedDB Dispatcher 将请求翻译为 LevelDB 操作;
-
所有操作会被暂时写入 内存事务日志(
WriteBatch); -
只有事务成功时才
flush到磁盘(WAL→SSTable); -
如果出错或
abort,内存中的batch被丢弃,LevelDB不变。
LevelDB是IndexedDB在Chromium中的真正存储引擎。
| 层级 | 事务实现方式 |
|---|---|
| JS 层 | 你显式声明 transaction(),浏览器维护操作列表 |
| Blink 层 | 用 IPC 消息将请求发到 Browser Process |
| IndexedDB Dispatcher | 将事务缓存成 WriteBatch |
| LevelDB 层 | 用 WAL + memtable 实现事务提交与回滚 |
| 存储层 | 最终数据写入磁盘上的 .ldb 或 .sqlite 文件 |
插入对象
- 拿到对象存储之后就能通过
add()或者put()方法写入数据,这俩方法都接受一个参数,并把,简单的说前者是添加新的值,后者是更新
for (let user of users){
store.add(user)
//store.put(user)
}
- 每次调用该api都会创建对象存储的新跟新请求,像验证请求成功与否,可以把请求保存到一个变量中为它添加
onsucess和onerror方法实现
let requests = []
let request
for (let user of users){
request = store.add(user)
request.onerror = (event)=>{
alert("错了")
//处理错误的逻辑
}
request.onsucess = (event)=>{
}
requests.push(request)
}
通过游标查询
- 填充数据之后就可以通过游标查询了
- 游标是一个指向结果集的指针,与传统数据库不同,游标不会直接拿到所有结果集数据,游标指向第一个结果,在接收到指令之前不会加载下一条数据
- 需要在对象存储上调用
openCursor创建一个游标,和其他indexedDB操纵一样,openCursor返回一个请求,必须为他添加onerror和onsuccess事件处理程序
const transaction = db.transaction("users") //创建事务
const store = transaction.objectStore("users") //通过该api访问到其存储对象
const request = store.openCursor()
request.onsuccess = (event)=>{
//处理成功逻辑
}
request.onerror = (event)=>{
//处理错误逻辑
}
- 通过
event.target.result访问对象存储 - 该对象保存着
IDBCursor的实例或者null(没有记录) - 该实例有以下属性
direction key value primaryKey
//得到结果
request.onsuccess = (event)=>{
const cursor = event.target.result;
if(cursor) {
console.log(`key:${cursor.key},Value:${JSON.Stringfy(cursor.value)}`}
}
}
//cursor.value保存着实际的对象 因此需要JSON来编码
- 常用
apiupdate:更新数值delete:删除游标当前位置的记录
更新示例
request.onsucess = (event)=>{
const cursor = event.target.result;
let value,updateRequest;
if(cursor){ //必须检查!!!
if(cursor.value == "foo"){
value = cursor.value;
value.password = "ncoancoa";
updateRequest = cursor.update(value);
updateRequest.onsucess = (event)=>{
}
updateRequest.onerror = (event)=>{
}
}
}
}
如果事务没有修改的权限 update() delete()就会抛出错误 默认情况,一个游标指挥创建一个请求,要创建另一个请求就必须调用以下的方法 continue:参数是key,可选的,不指定就会直接到下一条 advance:count,向前移动指定的count条记录
cursor.continue创建的另一个请求在没有数据记录的时候onsuccess最后一次被调用,测试event.target.result为空
键范围
IDBKeyRange定义查询的范围
IDBKeyRange.lowerBound(10) // key ≥ 10
IDBKeyRange.upperBound(20) // key ≤ 20
IDBKeyRange.bound(10, 20) // 10 ≤ key ≤ 20
IDBKeyRange.only(42) // key === 42
用法:
const tx = db.transaction("users");
const store = tx.objectStore("users");
const range = IDBKeyRange.bound(18, 30); // 查年龄在 18~30 之间的
const request = store.openCursor(range);
游标方向
- 遍历的时候可以控制方向
当你用
openCursor()或openKeyCursor()遍历数据时,可以控制遍历方向。 | 游标方向字符串 | 含义 | | -------------- | ------------ | |"next"| 按主键升序(默认) | |"prev"| 按主键降序 | |"nextunique"| 跳过重复 key(升序) | |"prevunique"| 跳过重复 key(降序) |
设置游标方向
索引
IndexedDB作为标准API层不会规定底层存储用什么结构,但是大部分底层存储都是B+树结构 索引是在非主键建立的二级查询入口 步骤
- 创建索引
const store = db.createObjectStore("users", { keyPath: "id" });
store.createIndex("by_name", "name", { unique: false });
- 使用索引查询:(没有索引就只能通过id快速查询,其余字段只能全表遍历,所以高频字段要挂上索引)
const index = db.transaction("users").objectStore("users").index("by_name");
const request = index.get("Alice");
request.onsuccess = () => {
console.log("查到的用户是:", request.result);
};
注意删除索引不会影响到存储的数据结构,所以这个操作没有回调
-
它只能在
onupgradeneeded中使用(数据库版本升级时); -
不涉及 I/O 操作(浏览器会内部缓存在 upgrade 完成时一次性提交);
-
所以不需要异步监听,也不会触发
.onsuccess、.onerror回调。
并发问题
IndexedDB虽然是异步API,但是仍然存在并发问题,不同浏览器标签页同时打开同一个网页,出现一个网页尝试升级数据库而另一个尚未就绪怎么办? 数据库什么时候升级?
-
当你调用 indexedDB.open(name, version) 且 version > 当前版本 时触发;
-
如果不传版本号(或传已有版本),不会触发升级;
-
升级只能在 onupgradeneeded 中进行,包括建表、建索引、迁移数据等操作。
多标签升级冲突会发生什么?
当标签页 A 正在升级: - 标签页 B 调用 indexedDB.open("myDB", 2):
- 如果 A 的升级还没完成,B 会触发 blocked 事件;
- B 不能访问数据库,直到 A 完成升级或被关闭;
- 你必须监听 request.onblocked 来处理这类场景。
如何处理?
- 监听 blocked 事件
const request = indexedDB.open("myDB", 2);
request.onblocked = () => {
alert("数据库正在被其他标签页升级,请关闭其他页面重试。");
};
- 监听
versionchange事件(通知旧页面释放数据库) 如果你打开的是旧版本页面,新的标签页尝试升级时会通知旧的页面:
- 比较推荐这个方法:同源标签页将数据库打开到新版本的时候触发这个回调,最好的处理就是关掉数据库以便完成数据库升级
db.onversionchange = () => {
db.close();
alert("数据库即将升级,当前页面会自动刷新");
window.location.reload();
};
- 只在一个地方做升级(常规建议)
-
在你的代码中,只让首个打开的页面进行数据库结构升级;
-
其他页面不强求升级,可以做降级处理或只读模式;
-
可以借助
BroadcastChannel API或localStorage传递“谁在升级”的信号。
-
既然是异步API为什么出现这种问题
异步API设计是为了不阻塞主线程,不阻塞UI,不会卡住页面,属于事件驱动模型
但是数据库不是普通操作,尤其是版本变更操作需要具备
- 排他性
- 一致性
- 原子性
- 持久性
限制
- indexedDB是和页面源(协议,域名,端口)绑定的,因此很多数据不能实现跨域共享
- 每个源都有存储空间限制,当前firefox限制每个源的空间50mb,Chrome是5mb,移动端firefox也是5mb,超出配额会申请用户许可,在Firefox中本地文本不能访问IndexedDB,Chrome没有这个限制。这个兼容性比较差也是他没有流行起来的原因,另外诸如存储敏感数据缺乏原生的加密支持等都有一定的影响。