IndexedDB简介
MDN官网是这样解释Indexed DB的:
IndexedDB 是一种底层 API,用于在客户端存储大量的结构化数据(也包括文件/二进制大型对象(blobs))。该 API 使用索引实现对数据的高性能搜索。虽然 Web Storage 在存储较少量的数据很有用,但对于存储更大量的结构化数据来说力不从心。而 IndexedDB 提供了这种场景的解决方案。
通俗地说,IndexedDB 就是浏览器提供的本地数据库,它可以被网页脚本创建和操作。IndexedDB 允许储存大量数据,提供查找接口,还能建立索引。这些都是 LocalStorage 所不具备的。就数据库类型而言,IndexedDB 不属于关系型数据库(不支持 SQL 查询语句),更接近 NoSQL 数据库。
客户端各存储方式对比:
IndexedDB使用场景
所有的场景都基于客户端需要存储大量数据的前提下:
-
数据可视化等界面,大量数据,每次请求会消耗很大性能。
-
即时聊天工具,大量消息需要存在本地。
-
其它存储方式容量不满足时,不得已使用IndexedDB。
IndexedDB特点
(1) 非关系型数据库(NoSql)
我们都知道MySQL等数据库都是关系型数据库,它们的主要特点就是数据都以一张二维表的形式存储,而Indexed DB是非关系型数据库,主要以键值对的形式存储数据。
(2)持久化存储
cookie、localStorage、sessionStorage等方式存储的数据当我们清楚浏览器缓存后,这些数据都会被清除掉的,而使用IndexedDB存储的数据则不会,除非手动删除该数据库。
(3)异步操作
IndexedDB操作时不会锁死浏览器,用户依然可以进行其他的操作,这与localstorage形成鲜明的对比,后者是同步的。
(4)支持事务
IndexedDB支持事务(transaction),这意味着一系列的操作步骤之中,只要有一步失败了,整个事务都会取消,数据库回滚的事务发生之前的状态,这和MySQL等数据库的事务类似。
(6)同源策略
IndexedDB同样存在同源限制,每个数据库对应创建它的域名。网页只能访问自身域名下的数据库,而不能访问跨域的数据库。
(7)存储容量大
这也是IndexedDB最显著的特点之一了,这也是不用localStorage等存储方式的最好理由。
IndexedDB重要概念讲解
仓库objectStore
IndexedDB没有表的概念,它只有仓库store的概念,大家可以把仓库理解为表即可,即一个store是一张表。
索引index
在关系型数据库当中也有索引的概念,我们可以给对应的表字段添加索引,以便加快查找速率。
游标cursor
游标是IndexedDB数据库新的概念,大家可以把游标想象为一个指针,比如我们要查询满足某一条件的所有数据时,就需要用到游标,我们让游标一行一行的往下走,游标走到的地方便会返回这一行数据,此时我们便可对此行数据进行判断,是否满足条件。
【注意】:IndexedDB查询不像MySQL等数据库方便,它只能通过主键、索引、游标方式查询数据。
事务
数据记录的读写和删改,都要通过事务完成。即对数据库进行操作时,只要失败了,都会回滚到最初始的状态,确保数据的一致性。
IndexedDB实操
IndexedDB所有针对仓库的操作都是基于事务的。
新建/打开数据库
使用 IndexedDB 的第一步是打开数据库,使用indexedDB.open()
方法。
var request = window.indexedDB.open(databaseName, version);
这个方法接受两个参数,第一个参数是字符串,表示数据库的名字。如果指定的数据库不存在,就会新建数据库。第二个参数是整数,表示数据库的版本。如果省略,打开已有数据库时,默认为当前版本;新建数据库时,默认为1。
indexedDB.open()
方法返回一个 IDBRequest 对象。这个对象通过三种事件error
、success
、upgradeneeded
,处理打开数据库的操作结果。
(1)error 事件
error
事件表示打开数据库失败。
request.onerror = (event) => {
console.log('数据库打开报错');
};
(2)success 事件
success
事件表示成功打开数据库。
let db;
request.onsuccess = (event) => {
db = request.result;
console.log('数据库打开成功');
};
这时,通过request
对象的result
属性拿到数据库对象。
(3)upgradeneeded 事件
当数据库版本有变化的时候会执行该函数,比如我们想创建新的存储库(表),就可以在该函数里面操作,更新数据库版本即可。
let db;
request.onupgradeneeded = (event) => {
db = event.target.result;
}
这时通过事件对象的target.result
属性,拿到数据库实例。
新建数据库与打开数据库是同一个操作。如果指定的数据库不存在,就会新建。不同之处在于,后续的操作主要在upgradeneeded
事件的监听函数里面完成,因为这时版本从无到有,所以会触发这个事件。
通常,新建数据库以后,第一件事是新建对象仓库(即新建表)。
request.onupgradeneeded = function (event: any) {
// 数据库创建或升级的时候会触发
console.log("onupgradeneeded");
db = event.target.result; // 数据库对象
var objectStore;
// 新建对象仓库(即新建表)
objectStore = db.createObjectStore("users", {
keyPath: "uid", // 主键
// autoIncrement: true // 如果数据记录里面没有合适作为主键的属性,那么可以让 IndexedDB 自动生成主键
});
};
主键(key)是默认建立索引的属性。
通过IDBObject.createIndex()
新建索引。
IDBObject.createIndex()
的三个参数分别为索引名称、索引所在的属性、配置对象(说明该属性是否包含重复的值)。
request.onupgradeneeded = function (event: any) {
// 数据库创建或升级的时候会触发
console.log("onupgradeneeded");
db = event.target.result; // 数据库对象
var objectStore;
// 新建对象仓库(即新建表)
objectStore = db.createObjectStore("users", {
keyPath: "uid", // 主键
// autoIncrement: true // 如果数据记录里面没有合适作为主键的属性,那么可以让 IndexedDB 自动生成主键
});
// 创建索引,在后面查询数据的时候可以根据索引查
objectStore.createIndex("name", "name", { unique: true });
objectStore.createIndex("name", "name", { unique: false });
objectStore.createIndex("age", "age", { unique: false });
};
新增数据
新增数据指的是向对象仓库写入数据记录。这需要通过事务完成。
/**
* 新增数据
* @param {object} db 数据库实例
* @param {string} storeName 仓库名称
* @param {string} data 数据
*/
addData(db: any, storeName: string, data: any) {
return new Promise<void>((resolve, reject) => {
const request = db
.transaction([storeName], "readwrite") // 事务对象 指定表格名称和操作模式("只读"或"读写")
.objectStore(storeName) // 仓库对象
.add(data);
request.onsuccess = function () {
console.log("数据写入成功");
resolve()
};
request.onerror = function () {
console.log("数据写入失败");
reject()
};
})
}
上面代码中,写入数据需要新建一个事务。新建时必须指定表格名称和操作模式("只读"或"读写")。新建事务以后,通过IDBTransaction.objectStore(name)
方法,拿到 IDBObjectStore 对象,再通过表格对象的add()
方法,向表格写入一条记录。
add()
接收三个参数,分别如下:
-
db:在创建或连接数据库时,返回的db实例,需要那个时候保存下来。
-
storeName:仓库名称(或者表名),在创建或连接数据库时我们就已经创建好了仓库。
-
data:需要插入的数据,通常是一个对象。
通过主键读取数据
读取数据也是通过事务完成。
/**
* 通过主键读取数据
* @param {object} db 数据库实例
* @param {string} storeName 仓库名称
* @param {string} key 主键值
*/
getDataByKey(db: any, storeName: string, key: number) {
return new Promise((resolve, reject) => {
const transaction = db.transaction([storeName]); // 事务
const objectStore = transaction.objectStore(storeName); // 仓库对象
const request = objectStore.get(key); // 通过主键获取数据
request.onerror = () => {
console.log("事务失败");
reject();
};
request.onsuccess = () => {
console.log("主键查询结果: ", request.result);
resolve(request.result);
};
});
}
上面代码中,objectStore.get()
方法用于读取数据,参数是主键的值。
通过游标查询数据
/**
* 通过游标读取数据
* @param {object} db 数据库实例
* @param {string} storeName 仓库名称
*/
cursorGetData(db: any, storeName: string) {
return new Promise((resolve, reject) => {
const list:any[] = [];
const store = db
.transaction(storeName, "readwrite") // 事务
.objectStore(storeName); // 仓库对象
const request = store.openCursor(); // 指针对象
// 游标开启成功,逐行读数据
request.onsuccess = (e: any) => {
const cursor = e.target.result;
if (cursor) {
// 必须要检查
list.push(cursor.value);
cursor.continue(); // 遍历了存储对象中的所有内容
} else {
console.log("游标读取的数据:", list);
resolve(list);
}
};
request.onerror = () => {
console.log("游标读取失败");
reject();
}
})
}
上面函数开启了一个游标,然后逐行读取数据,存入数组,最终得到整个仓库的所有数据。
通过索引查询数据
索引的意义在于,可以让你搜索任意字段,也就是说从任意字段拿到数据记录。如果不建立索引,默认只能搜索主键(即从主键取值)。
/**
* 通过索引读取数据
* @param {object} db 数据库实例
* @param {string} storeName 仓库名称
* @param {string} indexName 索引名称
* @param {string} indexValue 索引值
*/
getDataByIndex(db: IDBDatabase, storeName: string, indexName: string, indexValue: number) {
return new Promise((resolve, reject) => {
const store = db.transaction(storeName, "readonly").objectStore(storeName);
const request = store.index(indexName).get(indexValue);
request.onerror = function () {
console.log("事务失败");
reject();
};
request.onsuccess = function (e) {
const result = (e.target as IDBOpenDBRequest).result;
console.log("索引查询结果:", result);
resolve(result);
};
})
}
索引名称即我们创建仓库的时候创建的索引名称,也就是键值对中的键,最终会查询出所有满足我们传入函数索引值的数据。
单独通过索引或者游标查询出的数据都是部分或者所有数据,如果我们想要查询出索引中满足某些条件的所有数据,那么单独使用索引或游标是无法实现的。当然,你也可以查询出所有数据之后在循环数组筛选出合适的数据,但是这不是最好的实现方式,最好的方式当然是将索引和游标结合起来。
值得注意的是使用了IDBKeyRange.only()API,该API代表只能当两个值相等时,具体API解释可参考MDN官网。
/**
* 通过索引和游标查询记录
* @param {object} db 数据库实例
* @param {string} storeName 仓库名称
* @param {string} indexName 索引名称
* @param {string} indexValue 索引值
*/
cursorGetDataByIndex(db: IDBDatabase, storeName: string, indexName: string, indexValue: number) {
return new Promise((resolve, reject) => {
const list: any = [];
const store = db.transaction(storeName, "readwrite").objectStore(storeName); // 仓库对象
const request = store
.index(indexName) // 索引对象
.openCursor(IDBKeyRange.only(indexValue)); // 指针对象
request.onsuccess = function (e) {
const cursor: any = (e.target as IDBOpenDBRequest).result;
if (cursor) {
list.push(cursor.value);
cursor.continue();
} else {
console.log("游标索引查询结果:", list);
resolve(list);
}
};
request.onerror = function (e) {
reject();
};
})
}
更新数据
IndexedDB更新数据较为简单,直接使用put
方法,值得注意的是如果数据库中没有该条数据,则会默认增加该条数据,否则更新。
/**
* 更新数据
* @param {object} db 数据库实例
* @param {string} storeName 仓库名称
* @param {object} data 数据
*/
updateDB(db: IDBDatabase, storeName: string, data: any): Promise<void> {
return new Promise((resolve, reject) => {
const request = db
.transaction([storeName], "readwrite") // 事务对象
.objectStore(storeName) // 仓库对象
.put(data);
request.onsuccess = function () {
console.log("数据更新成功");
resolve();
};
request.onerror = function () {
console.log("数据更新失败");
reject();
};
})
}
删除数据
IDBObjectStore.delete()
方法用于删除记录。
/**
* 通过主键删除数据
* @param {object} db 数据库实例
* @param {string} storeName 仓库名称
* @param {object} id 主键值
*/
deleteDB(db: IDBDatabase, storeName: string, id: number) {
return new Promise(() => {
const request = db
.transaction([storeName], "readwrite")
.objectStore(storeName)
.delete(id);
request.onsuccess = function () {
console.log("数据删除成功");
};
request.onerror = function () {
console.log("数据删除失败");
};
})
}
该种删除只能删除一条数据,必须传入主键。
有时候我们拿不到主键值,只能只能通过索引值来删除,通过这种方式,我们可以删除一条数据(索引值唯一)或者所有满足条件的数据(索引值不唯一)。
/**
* 通过索引和游标删除指定的数据
* @param {object} db 数据库实例
* @param {string} storeName 仓库名称
* @param {string} indexName 索引名
* @param {object} indexValue 索引值
*/
cursorDelete(db: IDBDatabase, storeName: string, indexName: string, indexValue: number | string): Promise<void> {
return new Promise((resolve, reject) => {
const store = db.transaction(storeName, "readwrite").objectStore(storeName);
const request = store
.index(indexName) // 索引对象
.openCursor(IDBKeyRange.only(indexValue)); // 指针对象
request.onsuccess = function (e) {
const cursor: any = (e.target as IDBOpenDBRequest).result;
let deleteRequest;
if (cursor) {
deleteRequest = cursor.delete(); // 请求删除当前项
deleteRequest.onerror = function () {
console.log("游标删除该记录失败");
reject();
};
deleteRequest.onsuccess = function () {
console.log("游标删除该记录成功");
resolve();
};
cursor.continue();
}
};
request.onerror = function (e) {
console.log(e);
reject();
};
})
}
关闭数据库
当我们数据库操作完毕后,建议关闭它,节约资源。
closeDB(db: IDBDatabase) {
db.close();
console.log("数据库已关闭");
}
删除数据库
最后我们需要删库跑路,删除操作也很简单。
deleteDBAll(dbName: string): Promise<void> {
return new Promise((resolve, reject) => {
const deleteRequest = window.indexedDB.deleteDatabase(dbName);
deleteRequest.onerror = function (event) {
console.log("删除失败");
reject();
};
deleteRequest.onsuccess = function (event) {
console.log("删除成功");
resolve();
};
})
}
参考文档
https://developer.mozilla.org/zh-CN/docs/Web/API/IndexedDB_API
标签:IndexedDB,string,storeName,db,数据库,request,索引 From: https://www.cnblogs.com/yangser/p/18573953