今天做项目,遇到了 IndexedDB问题,搞了好久,终于有点头绪。
对于 IndexedDB,之前只是了解,但不是很深入。
不得已我查找了MDN的官网,又从网上找了几篇文章,分享如下。 
相信通过此文,你对于也会有比较深入的认识IndexedDB。

什么是 IndexedDB

IndexedDB 是一种底层 API,用于在客户端存储大量的结构化数据(也包括文件/二进制大型对象(blobs))

该 API 使用索引实现对数据的高性能搜索。

虽然 Web Storage 在存储较少量的数据(最多能存5m,不是一个key的大小,是所有key的总大小最多加起来最多5m)很有用,但对于存储更大量的结构化数据来说力不从心。而 IndexedDB 提供了这种场景的解决方案。

备注:

IndexedDB API 是强大的,但对于简单的情况可能看起来太复杂。如果你更喜欢一个简单的 API,请尝试 localForagedexie.jsPouchDBidbidb-keyvalJsStore 或者 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文件

<!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的官网

性能

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);

Table.mapToClass()

新增数据

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);
});

在这里插入图片描述

参考文档

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐