IndexedDB ?你知道吗?
今天做项目,遇到了 IndexedDB问题。之前对于 IndexedDB只限于了解,但不是很深入。所以我整理一下 IndexedDB问题。通过查找MDN的官网,又从网上找了几篇文章,终于有些许收获。分享如下: 相信通过此文,您对于IndexedDB也会有比较深入的认识。
今天做项目,遇到了 IndexedDB问题,搞了好久,终于有点头绪。
对于 IndexedDB,之前只是了解,但不是很深入。
不得已我查找了MDN的官网,又从网上找了几篇文章,分享如下。
相信通过此文,你对于也会有比较深入的认识IndexedDB。
什么是 IndexedDB
IndexedDB 是一种底层 API,用于在客户端存储大量的结构化数据(也包括文件/二进制大型对象(blobs))。
该 API 使用索引实现对数据的高性能搜索。
虽然 Web Storage 在存储较少量的数据(最多能存5m,不是一个key的大小,是所有key的总大小最多加起来最多5m)很有用,但对于存储更大量的结构化数据来说力不从心
。而 IndexedDB 提供了这种场景的解决方案。
备注:
IndexedDB API 是强大的,但对于简单的情况可能看起来太复杂。如果你更喜欢一个简单的 API,请尝试 localForage、dexie.js、PouchDB、idb、idb-keyval、JsStore 或者 lovefield 之类的库,这些库使 IndexedDB 对开发者来说更加友好。
IndexedDB 是一个事务型数据库系统,类似于基于 SQL 的 RDBMS。然而,不像 RDBMS 使用固定列表,IndexedDB 是一个基于 JavaScript 的面向对象数据库。
IndexedDB 允许您存储和检索用键索引的对象;可以存储结构化克隆算法支持的任何对象。您只需要指定数据库模式,打开与数据库的连接,然后检索和更新一系列事务。
如何使用 IndexedDB
第一步:准备环境和基本的html页面
这里需要一个服务器容器打开页面,我这里使用vscode live-server,路径地址要是有ip地址或者localhost加端口号的格式,如:http://127.0.0.1:5500/document/ IndexedDB.html.
新建html文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
</body>
</html>
第二步:打开数据库(如果没有自动创建)
let dbName = 'hello IndexedDB', version = 1, storeName = 'helloStore'
let indexedDB = window.indexedDB
let db
const request = indexedDB.open(dbName, version)
request.onsuccess = function(event) {
db = event.target.result // 数据库对象
console.log('数据库打开成功')
}
request.onerror = function(event) {
console.log('数据库打开报错')
}
request.onupgradeneeded = function(event) {
// 数据库创建或升级的时候会触发
console.log('onupgradeneeded')
db = event.target.result // 数据库对象
let objectStore
if (!db.objectStoreNames.contains(storeName)) {
objectStore = db.createObjectStore(storeName, { keyPath: 'id' }) // 创建表
// objectStore.createIndex('name', 'name', { unique: true }) // 创建索引 可以让你搜索任意字段
}
}
完整代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0">
<title>Title</title>
<link href="./favicon.png">
</head>
<body>
</body>
<script>
let dbName = 'hello IndexedDB',
version = 1,
storeName = 'helloStore'
let indexedDB = window.indexedDB
let db
const request = indexedDB.open(dbName, version)
request.onsuccess = function (event) {
db = event.target.result // 数据库对象
console.log('数据库打开成功')
}
request.onerror = function (event) {
console.log('数据库打开报错')
}
request.onupgradeneeded = function (event) {
// 数据库创建或升级的时候会触发
console.log('onupgradeneeded')
db = event.target.result // 数据库对象
let objectStore
if (!db.objectStoreNames.contains(storeName)) {
objectStore = db.createObjectStore(storeName, {
keyPath: 'id'
}) // 创建表
// objectStore.createIndex('name', 'name', { unique: true }) // 创建索引 可以让你搜索任意字段
}
}
</script>
</html>
运行如上面的代码后打开控制台可以看到如下效果,数据库已经创建完成了,此时什么数据都没有
第三步:存入一个helloWorld
// 添加数据
function addData(db, storeName, data) {
let request = db.transaction([storeName], 'readwrite') // 事务对象 指定表格名称和操作模式("只读"或"读写")
.objectStore(storeName) // 仓库对象
.add(data)
request.onsuccess = function (event) {
console.log('数据写入成功')
}
request.onerror = function (event) {
console.log('数据写入失败')
throw new Error(event.target.error)
}
}
function addDataHandel() {
// 由于打开 IndexedDB是异步的加个定时器避免 db对象还没获取到值导致 报错
setTimeout(() => {
addData(db, storeName, {
id: new Date().getTime(), // 必须且值唯一
name: '张三',
age: 18,
desc: 'helloWord'
})
}, 200)
}
完整代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0">
<title>Title</title>
<link href="./favicon.png">
</head>
<body>
<button onclick="addDataHandel()">添加</button>
</body>
<script>
let dbName = 'hello IndexedDB',
version = 1,
storeName = 'helloStore'
let indexedDB = window.indexedDB
let db
const request = indexedDB.open(dbName, version)
request.onsuccess = function (event) {
db = event.target.result // 数据库对象
console.log('数据库打开成功')
}
request.onerror = function (event) {
console.log('数据库打开报错')
}
request.onupgradeneeded = function (event) {
// 数据库创建或升级的时候会触发
console.log('onupgradeneeded')
db = event.target.result // 数据库对象
let objectStore
if (!db.objectStoreNames.contains(storeName)) {
objectStore = db.createObjectStore(storeName, {
keyPath: 'id'
}) // 创建表
// objectStore.createIndex('name', 'name', { unique: true }) // 创建索引 可以让你搜索任意字段
}
}
// 添加数据
function addData(db, storeName, data) {
let request = db.transaction([storeName], 'readwrite') // 事务对象 指定表格名称和操作模式("只读"或"读写")
.objectStore(storeName) // 仓库对象
.add(data)
request.onsuccess = function (event) {
console.log('数据写入成功')
}
request.onerror = function (event) {
console.log('数据写入失败')
throw new Error(event.target.error)
}
}
function addDataHandel() {
// 由于打开 IndexedDB是异步的加个定时器避免 db对象还没获取到值导致 报错
setTimeout(() => {
addData(db, storeName, {
id: new Date().getTime(), // 必须且值唯一
name: '张三',
age: 18,
desc: 'helloWord'
})
}, 200)
}
</script>
</html>
刷新页面后可以看到如下结果,此时我这里已经存进去了,(我点击了两次所以有两条数据)
第四步:封装删除,查询,修改方法并分别执行查看结果
// 根据id获取数据
function getDataByKey(db, storeName, key) {
let transaction = db.transaction([storeName]) // 事务
let objectStore = transaction.objectStore(storeName) // 仓库对象
let request = objectStore.get(key)
request.onerror = function (event) {
console.log('事务失败')
}
request.onsuccess = function (event) {
console.log('主键查询结果: ', request.result)
}
}
// 根据id修改数
function updateDB(db, storeName, data) {
let request = db.transaction([storeName], 'readwrite') // 事务对象
.objectStore(storeName) // 仓库对象
.put(data)
request.onsuccess = function () {
console.log('数据更新成功')
}
request.onerror = function () {
console.log('数据更新失败')
}
}
// 根据id删除数据
function deleteDB(db, storeName, id) {
let request = db.transaction([storeName], 'readwrite').objectStore(storeName).delete(id)
request.onsuccess = function () {
console.log('数据删除成功')
}
request.onerror = function () {
console.log('数据删除失败')
}
}
// 根据id获取数据
function getDataByKeyHandel() {
getDataByKey(db, storeName, 1678199973342)
}
// 根据id修改数
function updateDBHandel() {
updateDB(db, storeName, {
id: 1678199997952,
desc: '修改的内容'
})
}
// 根据id删除数据
function deleteDBHandel() {
deleteDB(db, storeName, 1678200006653)
}
完整代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0">
<title>Title</title>
<link href="./favicon.png">
</head>
<body>
<button onclick="addDataHandel()">添加</button>
<button onclick="getDataByKeyHandel()"> 查询</button>
<button onclick="updateDBHandel()">修改</button>
<button onclick="deleteDBHandel()">删除</button>
</body>
<script>
let dbName = ' IndexedDBDemo',
version = 1,
storeName = 'helloStore'
let indexedDB = window.indexedDB
let db
const request = indexedDB.open(dbName, version)
request.onsuccess = function (event) {
db = event.target.result // 数据库对象
console.log('数据库打开成功')
}
request.onerror = function (event) {
console.log('数据库打开报错')
}
request.onupgradeneeded = function (event) {
// 数据库创建或升级的时候会触发
console.log('onupgradeneeded')
db = event.target.result // 数据库对象
let objectStore
if (!db.objectStoreNames.contains(storeName)) {
objectStore = db.createObjectStore(storeName, {
keyPath: 'id'
}) // 创建表
// objectStore.createIndex('name', 'name', { unique: true }) // 创建索引 可以让你搜索任意字段
}
}
// 添加数据
function addData(db, storeName, data) {
let request = db.transaction([storeName], 'readwrite') // 事务对象 指定表格名称和操作模式("只读"或"读写")
.objectStore(storeName) // 仓库对象
.add(data)
request.onsuccess = function (event) {
console.log('数据写入成功')
}
request.onerror = function (event) {
console.log('数据写入失败')
throw new Error(event.target.error)
}
}
function addDataHandel() {
// 由于打开 IndexedDB是异步的加个定时器避免 db对象还没获取到值导致 报错
setTimeout(() => {
addData(db, storeName, {
id: new Date().getTime(), // 必须且值唯一
name: '张三',
age: 18,
desc: 'helloWord'
})
}, 1000)
}
// 根据id获取数据
function getDataByKey(db, storeName, key) {
let transaction = db.transaction([storeName]) // 事务
let objectStore = transaction.objectStore(storeName) // 仓库对象
let request = objectStore.get(key)
request.onerror = function (event) {
console.log('事务失败')
}
request.onsuccess = function (event) {
console.log('主键查询结果: ', request.result)
}
}
// 根据id修改数
function updateDB(db, storeName, data) {
let request = db.transaction([storeName], 'readwrite') // 事务对象
.objectStore(storeName) // 仓库对象
.put(data)
request.onsuccess = function () {
console.log('数据更新成功')
}
request.onerror = function () {
console.log('数据更新失败')
}
}
// 根据id删除数据
function deleteDB(db, storeName, id) {
let request = db.transaction([storeName], 'readwrite').objectStore(storeName).delete(id)
request.onsuccess = function () {
console.log('数据删除成功')
}
request.onerror = function () {
console.log('数据删除失败')
}
}
// 根据id获取数据
function getDataByKeyHandel() {
getDataByKey(db, storeName, 1678199973342)
}
// 根据id修改数
function updateDBHandel() {
updateDB(db, storeName, {
id: 1678199997952,
desc: '修改的内容'
})
}
// 根据id删除数据
function deleteDBHandel() {
deleteDB(db, storeName, 1678200006653)
}
</script>
</html>
基于 IndexedDB的Dexie数据库
什么是Dexie?
A Minimalistic Wrapper for IndexedDB (IndexedDB 的一个最小化包装)
Dexie使用本机IndexedDB API解决了三个主要问题:
- 模棱两可的错误处理
- 不好查询
- 代码复杂性
性能
Dexie表现出色。 它的批量方法利用了IndexedDB中一个鲜为人知的特性,可以在不收听每个onsuccess事件的情况下存储东西。 这样可以最大限度地提高性能。
安装及使用
1.安装
可以使用npm/cnpm/yarn安装
npm install dexie
而想直接引入使用的也可以使用以下方法
<script src="https://unpkg.com/dexie@latest/dist/dexie.js"></script>
使用
//模块化开发下需要引入该组件
import Dexie from 'dexie'
//创建一个数据库 若数据库已存在则为打开
//打开数据库时,会判断当前version值是否大于已经存在的version值,若大于则会upgrade即升到最高版本
var db = new Dexie("test_db");
db.version(1).stores({
student: 'name,age'
});
db.open()
//写入一些数据
db.student.put({name: "小明", age: 18}).then (function(){
//当数据存储完成后 我们可以读取它
return db.student.get('小明');
}).then(function (data) {
console.log("我是小明,今年 " + data.age);
}).catch(function(error) {
//最后别忘了抓住任何可能发生在上面的代码块。
console.log("error: " + error);
db.close()
});
db.close()
API
创建数据库
//注意:不要像在SQL中那样声明所有列。只声明要索引的属性,即要在where(…)查询中使用的属性。
var db = new Dexie("MyDatabase");
db.version(1).stores({
friends: "++id, name, age, *tags",
gameSessions: "id, score"
});
语法
|- |- |
| ++|自动递增主键|
|& | 唯一主键|
| * |多条目索引
|+ |复合索引
第一第二栏目就不说了很好理解,这里主要说明一下第三第四栏
多条目索引
var db = new Dexie('dbname');
db.version(1).stores ({
books: 'id, author, name, *categories'
});
在本示例中,书籍可以按多个类别进行分类。
这是通过让book对象具有一个名为 “categories” 的数组属性来实现的,该数组属性包含类别字符串。
见以下示例:
db.books.put({
id: 1,
name: 'Under the Dome',
author: 'Stephen King',
categories: ['sci-fi', 'thriller']
});
在示例中,我们添加了一本包含多个类别“科幻”和“惊悚”的书。
注意,不仅字符串可以放入数组,而且任何可索引类型都是有效的。
如何查询多条目索引?
所有where子句运算符都可用于查询多条目索引对象。
但是,运算符的行为不像普通索引那样直观。
例如,应该使用WhereClause.equals()运算符查询属于特定类别的书籍,而更具语义的名称可能是contains()。
这样做的原因是要映射indexedDB在本机上的工作方式,还允许使用任何运算符,而不将多条目索引绑定到某些运算符。
// 查询所有科幻书籍:
function getSciFiBooks() {
return db.books
.where('categories').equals('sci-fi')
.toArray ();
}
distinct()运算符
查询多条目索引时,如果同一项有多个索引匹配,则可能会得到同一对象的多个结果。
因此,在对多条目索引的查询中始终使用**Collection.distinct()**是一个很好的做法。
// 定义数据库
var db = new Dexie('dbname');
db.version(1).stores ({
books: 'id, author, name, *categories'
});
// 插入一本多类别的书
db.books.put({
id: 1,
name: 'Under the Dome',
author: 'Stephen King',
categories: ['sci-fi', 'thriller']
});
// 查询所有科幻书籍:
function getSciFiBooks() {
return db.books
.where('categories').equals('sci-fi')
.toArray ();
}
// 查询所有科幻或浪漫书籍:
function getSciFiOrRomanceBooks() {
return db.books
.where('categories').anyOf('sci-fi', 'romance')
.distinct() // 筛选掉重复的数据
.toArray()
}
// 复杂查询
function complexQuery() {
return db.books
.where('categories').startsWithAnyOfIgnoreCase('sci', 'ro')
.or('author').equalsIgnoreCase('stephen king')
.distinct()
.toArray();
}
局限性
- 复合索引不能标记为多条目。其局限性在于indexedDB本身。
- 不能将主键标记为MultiEntry。
以下浏览器不支持多条目索引:
- ie10 、11
- 基于非chromium的Microsoft Edge浏览器
- Safari 8, 9.
复合索引
复合索引是基于多个键路径的索引。
它可以有效地为一个索引中的多个属性建立索引,以方便地找到两个键的组合及其值的存在性。
定义架构时必须指定复合索引:
var db = new Dexie('dbname');
db.version(1).stores({
people: 'id, [firstName+lastName]'
});
在上面的示例中,firstName和lastName属性中包含有效键的记录将被索引。
如果存储了具有属性{firstName:‘foo’,lastName:‘bar’}的对象,则可以使用以下方法有效地查找该对象:
db.people.where('[firstName+lastName]').equals(['foo', 'bar'])
//或者下面这样
db.people.where({firstName: 'foo', lastName: 'bar'})`
第二种写法是一个特例,它只在Dexie>=2.0中工作,并且可以通过多个属性进行匹配,无论您的浏览器是否支持复合查询。
升级(upgrade)
db.version(1).stores({
friends: "++id,name,age,*tags",
gameSessions: "id,score"
});
db.version(2).stores({
friends: "++id, [firstName+lastName], yearOfBirth, *tags", // 更改索引
gameSessions: null // Delete 对象仓库
}).upgrade(tx => {
// 仅当安装了低于2的version时才会执行
return tx.table("friends").modify(friend => {
friend.firstName = friend.name.split(' ')[0];
friend.lastName = friend.name.split(' ')[1];
friend.birthDate = new Date(new Date().getFullYear() - friend.age, 0);
delete friend.name;
delete friend.age;
});
});
有关数据库版本控制的详细信息
类绑定
class Friend {
// Prototype method
save() {
return db.friends.put(this); // 只保存自己的 props.
}
// Prototype property
get age() {
return moment(Date.now()).diff(this.birthDate, 'years');
}
}
db.friends.mapToClass(Friend);
新增数据
add()
await db.friends.add({name: "Josephine", age: 21});
//or
await db.friends.bulkAdd([
{name: "Foo", age: 31},
{name: "Bar", age: 32}
]);
注意
将给定对象添加到存储:
- 如果已经存在具有相同主键的对象,则操作将失败,并将使用错误对象调用返回的promise catch()回调。
- 如果操作成功,则返回的promise then()回调将接收对象存储区上的add请求的结果,即插入对象的id。
只有当表使用非入站键时,才必须使用可选的第二个键参数。
如果在具有入站密钥的表上提供密钥参数,则操作将失败,返回的承诺将被拒绝。
table.bulkAdd(items, keys?, options?);
item | 要添加的对象数组 |
---|---|
keys (非必填) | 对应于给定项数组的主键数组 |
option(非必填) | {allKeys?: boolean} 如果指定{allKeys:true},则返回值将是结果主键的数组,而不是上一次添加的主键。如果表使用入站键,则可以将选项作为第二个参数提供。API将知道第二个参数是通过类型检查表示选项还是键数组。 |
何时使用keys参数
- 如果主键是入站的,则不能提供keys参数。
- 如果主键是非入站但自动递增的,则keys参数是可选的。
- 如果主键是非入站和非自动递增的,那么keys参数是必需的。
var db = new Dexie("test");
db.version(1).stores({
tableWithInboundKeys: "id,x,y,z", // 不能提供 "keys"
tableWithAutoIncNonInbound: "++,x,y,x", // 可选 "keys"
tableWithoutInboundKeys: ",x,y,z" // 必需提供 "keys"
});
如果有大量对象要添加到对象存储中,那么bulkAdd()比在循环中执行add()要快一点。
Reference: Table.add() Table.bulkAdd()
更新数据
await db.friends.put({id: 4, name: "Foo", age: 33});
//or
await db.friends.bulkPut([
{id: 4, name: "Foo2", age: 34},
{id: 5, name: "Bar2", age: 44}
]);
await db.friends.update(4, {name: "Bar"});
//or
await db.customers
.where("age")
.inAnyRange([ [0, 18], [65, Infinity] ])
.modify({discount: 0.5});
删除数据
await db.friends.delete(4);
or
await db.friends.bulkDelete([1,2,4]);
Table.delete()
参数名 | 参数值 |
---|---|
主键 | 要删除的对象的主键 |
Table.bulkDelete()
参数名 | 参数值 |
---|---|
keys | 要删除的对象的主键数组 |
const oneWeekAgo = new Date(Date.now() - 60*60*1000*24*7);
await db.logEntries
.where('timestamp').below(oneWeekAgo)
.delete();
查询数据
const someFriends = await db.friends
.where("age").between(20, 25)
.offset(150).limit(25)
.toArray();
await db.friends
.where("name").equalsIgnoreCase("josephine")
.each(friend => {
console.log("Found Josephine", friend);
});
const abcFriends = await db.friends
.where("name")
.startsWithAnyOfIgnoreCase(["a", "b", "c"])
.toArray();
await db.friends
.where('age')
.inAnyRange([[0,18], [65, Infinity]])
.modify({discount: 0.5});
const forbundsKansler = await db.friends
.where('[firstName+lastName]')
.equals(["Angela", "Merkel"])
.first();
在Dexie2.0中,以简单地执行上面的查询:
const angelasSortedByLastName = await db.friends
.where('[firstName+lastName]')
.between([["Angela", ""], ["Angela", "\uffff"])
.toArray()
选择成绩前五的数据
const best5GameSession = await db.gameSessions
.orderBy("score").reverse()
.limit(5)
.toArray();
Table.where()
通过创建WhereClause实例开始筛选对象存储。
// Dexie 1.x and 2.x:
table.where(indexOrPrimaryKey)
// Dexie 2.x only:
table.where(keyPathArray);
table.where({keyPath1: value1, keyPath2: value2, ...});
存储二进制数据
var db = new Dexie("MyImgDb");
db.version(1).stores({
friends: "name"
});
// 下载并存储图片
async function downloadAndStoreImage() {
const res = await fetch("some-url-to-an-image.png");
const blob = await res.blob();
await db.friends.put({
name: "David",
image: blob
});
}
索引二进制数据( IndexedDB 2.0)
IndexedDB 2.0包含对索引二进制数据的支持。
Chrome和Safari以及部分Firefox支持这个规范(Firefox在使用二进制主键时有一个bug,但是在使用二进制索引时效果很好)。
var db = new Dexie("MyImgDb");
db.version(1).stores({
friends: "id, name" // 使用二进制UUID作为id
});
// IndexedDB 2.0 允许索引ArrayBuffer和XXXArray
// (类型化数组,但不是blob)
async function playWithBinaryPrimKey() {
// 存储二进制数据:
await db.friends.put({
id: new Uint8Array([1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16]),
name: "David"
});
// 通过二进制搜索检索
const friend = await db.friends.get(
new Uint8Array([1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16]));
if (friend) {
console.log(`Found friend: ${friend.name}`);
} else {
console.log(`Friend not found`);
}
}
事务(Transaction)
一系列操作步骤之中,只要有一步失败,整个事务就都取消,数据库回滚到事务发生之前的状态,不存在只改写一部分数据的情况。
await db.transaction('rw', [db.friends], async () => {
const friend = await db.friends.get(1);
++friend.age;
await db.friends.put(friend);
});
参考文档
更多推荐
所有评论(0)