今天做项目,遇到了 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 允许您存储和检索用键索引的对象;可以存储结构化克隆算法支持的任何对象。您只需要指定数据库模式,打开与数据库的连接,然后检索和更新一系列事务。
Web 开发技术 IndexedDB
如何使用 IndexedDB
第一步:准备环境和基本的html页面
这里需要一个服务器容器打开页面,我这里使用vscode live-server,路径地址要是有ip地址或者localhost加端口号的格式,如:http://127.0.0.1:5500/document/ IndexedDB.html.
新建html文件
Document
第二步:打开数据库(如果没有自动创建)
let dbName = 'hello IndexedDB', version = 1, storeName = 'helloStore' let indexedDB = window.indexedDBlet dbconst 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 objectStoreif (!db.objectStoreNames.contains(storeName)) {objectStore = db.createObjectStore(storeName, { keyPath: 'id' }) // 创建表// objectStore.createIndex('name', 'name', { unique: true }) // 创建索引 可以让你搜索任意字段}}
完整代码
Title let dbName = 'hello IndexedDB',version = 1,storeName = 'helloStore'let indexedDB = window.indexedDBlet dbconst 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 objectStoreif (!db.objectStoreNames.contains(storeName)) {objectStore = db.createObjectStore(storeName, {keyPath: 'id'}) // 创建表// objectStore.createIndex('name', 'name', { unique: true }) // 创建索引 可以让你搜索任意字段}}
运行如上面的代码后打开控制台可以看到如下效果,数据库已经创建完成了,此时什么数据都没有
第三步:存入一个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)}
完整代码
Title let dbName = 'hello IndexedDB',version = 1,storeName = 'helloStore'let indexedDB = window.indexedDBlet dbconst 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 objectStoreif (!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)}
刷新页面后可以看到如下结果,此时我这里已经存进去了,(我点击了两次所以有两条数据)
第四步:封装删除,查询,修改方法并分别执行查看结果
// 根据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)}
完整代码
Title let dbName = ' IndexedDBDemo',version = 1,storeName = 'helloStore'let indexedDB = window.indexedDBlet dbconst 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 objectStoreif (!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)}
基于 IndexedDB的Dexie数据库
什么是Dexie?
A Minimalistic Wrapper for IndexedDB (IndexedDB 的一个最小化包装)
Dexie使用本机IndexedDB API解决了三个主要问题:
- 模棱两可的错误处理
- 不好查询
- 代码复杂性
Dexie的官网
性能
Dexie表现出色。 它的批量方法利用了IndexedDB中一个鲜为人知的特性,可以在不收听每个onsuccess事件的情况下存储东西。 这样可以最大限度地提高性能。
安装及使用
1.安装
可以使用npm/cnpm/yarn安装
npm install dexie
而想直接引入使用的也可以使用以下方法
使用
//模块化开发下需要引入该组件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 methodsave() {return db.friends.put(this); // 只保存自己的 props.}// Prototype propertyget age() {return moment(Date.now()).diff(this.birthDate, 'years');}}db.friends.mapToClass(Friend);
Table.mapToClass()
新增数据
add()
await db.friends.add({name: "Josephine", age: 21});//orawait db.friends.bulkAdd([{name: "Foo", age: 31},{name: "Bar", age: 32}]);
注意
将给定对象添加到存储:
- 如果已经存在具有相同主键的对象,则操作将失败,并将使用错误对象调用返回的promise catch()回调。
- 如果操作成功,则返回的promise then()回调将接收对象存储区上的add请求的结果,即插入对象的id。
只有当表使用非入站键时,才必须使用可选的第二个键参数。
如果在具有入站密钥的表上提供密钥参数,则操作将失败,返回的承诺将被拒绝。
table.bulkAdd(items, keys” />
如果有大量对象要添加到对象存储中,那么bulkAdd()比在循环中执行add()要快一点。
Reference: Table.add() Table.bulkAdd()
更新数据
await db.friends.put({id: 4, name: "Foo", age: 33});//orawait db.friends.bulkPut([{id: 4, name: "Foo2", age: 34},{id: 5, name: "Bar2", age: 44}]);
await db.friends.update(4, {name: "Bar"});//orawait 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);});
参考文档
- https://blog.csdn.net/qq_41579192/article/details/121605983
- https://blog.csdn.net/qq_36587420/article/details/105058830