前言
已知咱们前端的存储有cookie、 localStorage、 sessionStorage,但是这几个存储都有各种各样的限制,并且有一个统一的限制就是存储不行最大的localStorage也就5M,如果大数据储存应该使用什么方案, 来请咱们重量级选手 浏览器indexedDb
来上概念,讲特点:
IndexedDB 是一个用于在浏览器中储存较大数据结构的 Web [API], 并提供索引功能以实现高性能查找。像其他基于 [SQL] 的 [关系型数据库管理系统 (RDBMS)] 一样,IndexedDB 是一个事务型的数据库系统。然而,它是使用 [JavaScript] 对象而非列数固定的表格来储存数据的,由于它是浏览器提供的本地 [数据库] ,可以被网页脚本创建和操作,允许存贮大量数据,提供查找接口,能建立索引,用于在客户端存储大量的 [结构化数据](也包括文件/二进制大型对象(blobs)。
- 数据库(IDBDatabase 对象)数据库是一系列相关数据的容器。每个域名(严格的说,是协议 + 域名 + 端口)都可以新建任意多个数据库。但是它版本的概念。同一个时刻,只能有一个版本的数据库存在。如果要修改数据库结构(新增或删除表、索引或者主键),只能通过升级数据库版本完成。
- 对象仓库(IDBObjectStore 对象)每个数据库包含若干个对象仓库(object store)。它类似于关系型数据库的表格。
- 索引(IDBIndex 对象)为了加速数据的检索,可以在对象仓库里面,为不同的属性建立索引。
- 事务(IDBTransaction 对象)数据记录的读写和删改,都要通过事务完成。事务对象提供error、abort和complete三个事件监听操作结果。
- 操作请求(IDBRequest 对象)。
- 指针 (IDBCursor 对象) 。
- 主键集合 (IDBKeyRange 对象)。
IndexedDB 的主要特点:
1、IndexedDB 遵守 [同源策略],每一个数据库对应创建它的域名,网页只能访问自身域名下的数据库,而不能访问跨域的数据库。
2、IndexedDB 执行的操作是异步执行的,不会影响用户进行其他操作。这样也可以防止进行大量数据的读写时,拖慢网页的现象。
3、IndexedDB是采用对象仓库(类似关系型数据库中的表)存储数据的,所有类型的数据都可以直接存入,比如js对象,二进制流等。对象仓库中的数据是以键值对的形式保存的,其中键必须是唯一的,不能重复,否则会抛出错误。
4、IndexedDB支持事务(transaction),即操作要么全部执行,要么全部不执行。因此,在执行操作的过程中,只要有一步失败,整个事务就会被取消,数据库会进行回滚,回到事务发生前的状态。
5、 一般来说不少于 250MB,甚至没有上限。储 存 在 电 脑 上 中 的 位 置 为 C:\Users\当 前 的 登 录 用 户\AppData\Local\Google\Chrome\User Data\Default\IndexedDB。
6、不支持DOM操作,不能跨域。
先了解Api
indexedDB.open()
- onsuccess: 数据库打开成功触发该事件
- onerror: 数据库打开失败触发该事件
- onupgradeneeded: 第一打开该数据库或者数据库版本发生变化时触发该事件
- onblocked: 上一次的数据连接还未关闭时触发该事件
let request = window.indexedDB.open('dbName');
let db ;
request.onupgradeneeded = function(e) {
console.log("onupgradeneeded");
}
request.onsuccess = function(e) {
console.log("onsuccess");
db = e.target.result;
}
request.onerror = function(e) {
console.log("onerror");
console.dir(e);
}
上面代码有两个地方需要注意。首先,open方法返回的是一个对象(IDBOpenDBRequest),回调函数定义在这个对象上面。其次,回调函数接受一个事件对象event作为参数,它的target.result属性就指向打开的IndexedDB数据库。
createObjectStore方法
createObjectStore方法用于创建存放数据的“对象仓库”(object store),类似于传统关系型数据库的表格
db.createObjectStore('dbObjectStore')
该代码创建了dbObjectStore的对象仓库, 如果该仓库已经存在就会抛出一个错误。
objectStoreNames
可以通过 objectStoreNames 来检测当前仓库
if(!db.objectStoreNames.contains("dbObjectStore")) {
db.createObjectStore("dbObjectStore");
}
下面代码中的keyPath属性表示,所存入对象的keys属性用作每条记录的键名(由于键名不能重复,所以存入之前必须保证数据的keys属性值都是不一样的),默认值为null。autoIncrement属性表示,是否使用自动递增的整数作为键名(第一个数据为1,第二个数据为2,以此类推),默认为false。一般来说,keyPath和autoIncrement属性只要使用一个就够了,如果两个同时使用,表示键名为递增的整数,且对象不得缺少指定属性。
db.createObjectStore("test", { keyPath: "keys" });
db.createObjectStore("test1", { autoIncrement: true });
transaction方法
transaction方法用于创建一个数据库事务。向数据库添加数据之前,必须先创建数据库事务。
let dbObj = db.transaction(["dbObjectStore"],"readwrite");
transaction方法接受两个参数:第一个参数是一个数组,里面是所涉及的对象仓库,通常是只有一个,第二个参数是一个表示操作类型的字符串。目前,操作类型只有两种:readonly(只读)和readwrite(读写)。添加数据使用readwrite,读取数据使用readonly。
let store = dbObj.objectStore('dbObjectStore')
transaction方法返回一个事务对象,该对象的objectStore方法用于获取指定的对象仓库。
transaction方法有三个事件,可以用来定义回调函数。
- abort:事务中断。
- complete:事务完成。
- error:事务出错。
add方法
add方法的第一个参数是所要添加的数据,第二个参数是这条数据对应的键名(key),上面代码将对象o的键名设为keys。如果在创建数据仓库时,对键名做了设置,这里也可以不指定键名。
注意 add方法是异步的,有自己的success和error事件
store.add({'name': '测试数据'}, keys)
put 方法 跟add方法接近
store.put({'name': '测试数据'}, keys)
get 方法
注意get 方法也是异步回调同add一样
store.get(x)
delete方法
注意 delete方法也是异步回调同add一样
store.delete(id)
openCursor 遍历数据
注意 openCursor方法也是异步回调同add一样
// 回调函数接受一个事件对象作为参数,该对象的target.result属性指向当前数据对象。
// 当前数据对象的key和value分别返回键名和键值(即实际存入的数据)。
// continue方法将光标移到下一个数据对象,如果当前数据对象已经是最后一个数据了,则光标指向null。
// openCursor方法还可以接受第二个参数,表示遍历方向,默认值为next,其他可能的值为prev、nextunique和prevunique。后两个值表示如果遇到重复值,会自动跳过。
const cursor = store.openCursor()
cursor.onsuccess = function(e) {
var res = e.target.result;
if(res) {
console.log("Key", res.key);
console.dir("Data", res.value);
res.continue();
}
}
有了以上的功能的Api 咱们来自己搞一下
封装indexedDb
支持功能 增删改查
class IndexedDB {
constructor(dbName, storeName, version = 1, key) {
this.dbName = dbName;
this.storeName = storeName;
this.version = version;
this.key = key
this.db = null;
}
// 初始化需要 调用openDB 创建仓库以及关联关系
openDB() {
return new Promise((resolve, reject) => {
const request = window.indexedDB.open(this.dbName, this.version);
// 数据仓库打开成功
request.onsuccess = (event) => {
this.db = event.target.result;
resolve(this.db);
};
// 数据仓库打开失败
request.onerror = (event) => {
reject(event);
};
// 数据仓库升级事件(第一次新建库是也会触发)
request.onupgradeneeded = (event) => {
// 生成db实例
this.db = event.target.result;
if (!this.db.objectStoreNames.contains(this.storeName)) {
this.db.createObjectStore(this.storeName, { keyPath: this.key });
}
};
});
}
executeRequest(request) {
return new Promise((resolve, reject) => {
request.onsuccess = (event) => {
resolve(event.target.result);
};
request.onerror = (event) => {
reject(event);
throw new Error(event.target.error);
};
});
}
addData(data) {
const transaction = this.db.transaction([this.storeName], 'readwrite');
const objectStore = transaction.objectStore(this.storeName);
const request = objectStore.add(data);
return this.executeRequest(request);
}
getDataByKey(key) {
const transaction = this.db.transaction([this.storeName]);
const objectStore = transaction.objectStore(this.storeName);
const request = objectStore.get(key);
return this.executeRequest(request);
}
cursorGetData() {
const list = [];
const store = this.db.transaction(this.storeName, 'readwrite').objectStore(this.storeName);
const request = store.openCursor();
return new Promise((resolve, reject) => {
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
list.push(cursor.value);
cursor.continue();
} else {
resolve(list);
}
};
request.onerror = (event) => {
reject(event);
};
});
}
getDataByIndex(indexName, indexValue) {
const store = this.db.transaction(this.storeName, 'readwrite').objectStore(this.storeName);
const request = store.index(indexName).get(indexValue);
return this.executeRequest(request);
}
cursorGetDataByIndex(indexName, indexValue) {
const list = [];
const store = this.db.transaction(this.storeName, 'readwrite').objectStore(this.storeName);
const request = store.index(indexName).openCursor(IDBKeyRange.only(indexValue));
return new Promise((resolve, reject) => {
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
list.push(cursor.value);
cursor.continue();
} else {
resolve(list);
}
};
request.onerror = (event) => {
reject(event);
};
});
}
updateDB(data) {
const request = this.db.transaction([this.storeName], 'readwrite').objectStore(this.storeName).put(data);
return this.executeRequest(request);
}
deleteDB(id) {
const request = this.db.transaction([this.storeName], 'readwrite').objectStore(this.storeName).delete(id);
return this.executeRequest(request);
}
static deleteDBAll(dbName) {
const deleteRequest = window.indexedDB.deleteDatabase(dbName);
return new Promise((resolve, reject) => {
deleteRequest.onerror = (event) => {
console.log('删除失败');
};
deleteRequest.onsuccess = (event) => {
console.log('删除成功');
};
});
}
static closeDB(db) {
db.close();
console.log('数据库已关闭');
}
}
为什么要用 indexedDB
脱开需求讲技术 那不是纸上谈兵么, 有这样一个场景, 渲染某一个页面, 当前页面接口请求服务端,服务端还会调用自己的服务,或者多个服务,服务之间的连接也是需要消耗时间,同时返回的数据也超过5m,为了相对好的体验,咱们就可以用到indexedDb的存储了 ,这个时候要考虑一个小细节,如果数据储存本地了,服务端的数据发生了新的变化,如何通知前端呢,毕竟数据存储在浏览器,前端不知道的话,那么每次都是取indexedDb的数据?
- 首先跟服务端约定一个查询版本的接口,以版本号作为key
- 初始化请求来数据之后储存在indexedDb中 以版本号为key 数据 为value 当版本号发生变化时替换仓库中的数据。
- 根据当前的返回的key来判定是否取当前indexedDb中的数据
模拟下业务场景
const response = await fetch('/listTag', {
method: 'get',
})
// 生成基本信息
const indexDb = new IndexedDB('DetailDb', 'detailStore', '1.0', 'tag')
// 连接仓库
const openDB = await indexDb.openDB()
if (openDB) {
// 查询具体的key
const info = await indexDb.getDataByKey(response.tag)
if (info) {
this.data = info
}else {
const res = await fetch('/list', {
method: 'get',
})
// 仓库添加数据
indexDb.addData(res)
}
}
// 当然了增删改查 根据业务场景调用对应的方法即好。