IndexedDB详解

1,625 阅读5分钟

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。

虽然它不像关系型数据库那样复杂,但它的事务机制在本地数据库中是非常严格的。下面我们从机制、原理和实际表现来说明它是如何工作的。

  1. 写前日志 写前日志,所有的数据都不会立即写入磁盘而是创建一个临时区域(写前日志)
    1. 先写一块临时区域
    2. 所有操作完成之后才会提交到主数据存储
    3. 如果失败或者取消,事务就会回滚,主数据不会改变
  2. 快照隔离 只读事务中IndexedDB创建数据快照
  • 事务期间读到的数据是那一刻的数据
  • 即使其他事务修改了当时对象存储的内容,读到的内容依然不会发生改变,体现了隔离性,类似于数据库的多版本并发控制
  1. 自动提交或者回滚
  • 所有操作都是自动提交的,如果返回的请求是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");
  1. Blink(浏览器渲染引擎) 拦截这个 API 调用;

  2. 把事务和对象存储的请求封装成消息发往 Browser Process

3.Browser Process中的 IndexedDB Dispatcher 将请求翻译为 LevelDB 操作;

  1. 所有操作会被暂时写入 内存事务日志(WriteBatch);

  2. 只有事务成功时才flush到磁盘(WALSSTable);

  3. 如果出错或 abort,内存中的 batch 被丢弃,LevelDB 不变。

LevelDB Google 开发的高性能、持久化的键值数据库,它是 IndexedDBChromium 中的真正存储引擎。

层级事务实现方式
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都会创建对象存储的新跟新请求,像验证请求成功与否,可以把请求保存到一个变量中为它添加onsucessonerror方法实现
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返回一个请求,必须为他添加onerroronsuccess事件处理程序
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来编码


  • 常用api
    • update:更新数值
    • 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 来处理这类场景。

如何处理?

  1. 监听 blocked 事件
const request = indexedDB.open("myDB", 2);

request.onblocked = () => {
  alert("数据库正在被其他标签页升级,请关闭其他页面重试。");
};
  1. 监听 versionchange 事件(通知旧页面释放数据库) 如果你打开的是旧版本页面,新的标签页尝试升级时会通知旧的页面:
  • 比较推荐这个方法:同源标签页将数据库打开到新版本的时候触发这个回调,最好的处理就是关掉数据库以便完成数据库升级
db.onversionchange = () => {
  db.close();
  alert("数据库即将升级,当前页面会自动刷新");
  window.location.reload();
};
  1. 只在一个地方做升级(常规建议)
    • 在你的代码中,只让首个打开的页面进行数据库结构升级;

    • 其他页面不强求升级,可以做降级处理或只读模式;

    • 可以借助 BroadcastChannel APIlocalStorage 传递“谁在升级”的信号。

既然是异步API为什么出现这种问题

异步API设计是为了不阻塞主线程,不阻塞UI,不会卡住页面,属于事件驱动模型

但是数据库不是普通操作,尤其是版本变更操作需要具备

  • 排他性
  • 一致性
  • 原子性
  • 持久性

限制

  • indexedDB是和页面源(协议,域名,端口)绑定的,因此很多数据不能实现跨域共享
  • 每个源都有存储空间限制,当前firefox限制每个源的空间50mb,Chrome是5mb,移动端firefox也是5mb,超出配额会申请用户许可,在Firefox中本地文本不能访问IndexedDB,Chrome没有这个限制。这个兼容性比较差也是他没有流行起来的原因,另外诸如存储敏感数据缺乏原生的加密支持等都有一定的影响。