各位观众老爷,晚上好!今天咱们聊聊 IndexedDB,这玩意儿听起来高大上,其实就是浏览器里的一个“小仓库”,专门用来存放那些不想丢掉的数据。 就像你从网上下载了个小说,总不能每次看都重新下吧?IndexedDB 就是干这个用的,让你的浏览器也能像个小电脑一样,把数据存起来,下次直接用,离线也能用!
今天咱们的讲座主要包括以下几个方面:
- IndexedDB 事务 (Transactions) 模型: 保证数据操作的“原子性”,要么全成功,要么全失败。
- IndexedDB 版本管理: 升级数据库的正确姿势,教你如何优雅地更新你的“小仓库”。
- IndexedDB 异步操作: 为什么 IndexedDB 是异步的?异步操作的优势和注意事项。
- IndexedDB 在离线数据存储中的高级应用: 实际项目中的应用场景,让你知道这玩意儿到底能干啥。
一、IndexedDB 事务 (Transactions) 模型:保证数据操作的“原子性”
什么是事务? 你可以把它想象成你去银行办业务,比如你要转账给你的女神,这个操作包括:
- 从你的账户扣钱。
- 往女神的账户加钱。
如果第一个步骤成功了,但是第二个步骤失败了(比如女神的账户被冻结了),那可就惨了!你的钱没了,女神也没收到钱,银行肯定要被你投诉到爆。
所以,银行会把这两个步骤放在一个事务里。要么两个步骤都成功,要么两个步骤都失败,保证你的钱不会凭空消失。
IndexedDB 的事务也是一样的道理。它保证一系列数据库操作的“原子性”,要么全部成功,要么全部失败。 这样可以防止数据损坏,保持数据的完整性。
1. 事务的种类
IndexedDB 有三种事务模式:
- readonly: 只读事务,只能读取数据,不能修改数据。就像你只能参观银行,不能动银行里的钱一样。
- readwrite: 读写事务,可以读取和修改数据。就像你可以在银行里存钱、取钱、转账一样。
- versionchange: 版本变更事务,用于创建、删除或更新数据库的结构(比如创建新的表、删除旧的表)。这个事务只能在
onupgradeneeded
事件处理函数中使用,后面我们细说。
2. 如何创建事务
const request = indexedDB.open("myDatabase", 1);
request.onsuccess = (event) => {
const db = event.target.result;
// 创建一个读写事务
const transaction = db.transaction(["myObjectStore"], "readwrite");
// 获取 object store
const objectStore = transaction.objectStore("myObjectStore");
// 添加数据
const addRequest = objectStore.add({ id: 1, name: "张三", age: 20 });
addRequest.onsuccess = () => {
console.log("数据添加成功!");
};
addRequest.onerror = () => {
console.error("数据添加失败!");
};
// 事务完成事件
transaction.oncomplete = () => {
console.log("事务完成!");
};
// 事务错误事件
transaction.onerror = () => {
console.error("事务出错!");
};
};
这段代码做了什么?
indexedDB.open("myDatabase", 1)
: 打开一个名为 "myDatabase" 的数据库,版本号为 1。db.transaction(["myObjectStore"], "readwrite")
: 创建一个读写事务,作用于名为 "myObjectStore" 的 object store (相当于数据库中的表)。objectStore.add({ id: 1, name: "张三", age: 20 })
: 向 object store 中添加一条数据。transaction.oncomplete
: 事务成功完成时触发的事件。transaction.onerror
: 事务出错时触发的事件。
3. 事务的“原子性”
如果在一个事务中,多个数据库操作有一个失败了,那么整个事务都会回滚,就像什么都没发生一样。
比如,我们修改上面的代码,让其中一个操作失败:
const request = indexedDB.open("myDatabase", 1);
request.onsuccess = (event) => {
const db = event.target.result;
// 创建一个读写事务
const transaction = db.transaction(["myObjectStore"], "readwrite");
// 获取 object store
const objectStore = transaction.objectStore("myObjectStore");
// 添加数据
const addRequest1 = objectStore.add({ id: 1, name: "张三", age: 20 });
const addRequest2 = objectStore.add({ id: 1, name: "李四", age: 25 }); // id 重复,会报错
addRequest1.onsuccess = () => {
console.log("数据1添加成功!");
};
addRequest1.onerror = () => {
console.error("数据1添加失败!");
};
addRequest2.onsuccess = () => {
console.log("数据2添加成功!");
};
addRequest2.onerror = () => {
console.error("数据2添加失败!");
};
// 事务完成事件
transaction.oncomplete = () => {
console.log("事务完成!");
};
// 事务错误事件
transaction.onerror = () => {
console.error("事务出错!");
};
};
由于 id
是 object store 的 keyPath,不能重复,所以 addRequest2
会报错。 这会导致整个事务回滚,addRequest1
即使成功了,也会被撤销。
总结: 事务保证了数据的一致性,是 IndexedDB 非常重要的一个特性。 就像你玩游戏,存档失败了,就得重新来过,虽然痛苦,但是保证了你的游戏进度不会出错。
特性 | 描述 |
---|---|
原子性 | 事务中的所有操作要么全部成功,要么全部失败。 |
一致性 | 事务执行前后,数据库的状态必须保持一致。 |
隔离性 | 多个事务并发执行时,它们之间应该互相隔离,互不影响。 |
持久性 | 事务一旦提交,其结果应该永久保存,即使系统崩溃也不会丢失。 |
事务类型 | readonly (只读), readwrite (读写), versionchange (版本变更) |
应用场景 | 批量数据操作,需要保证数据完整性的场景 (例如:用户注册,订单创建) |
二、IndexedDB 版本管理:升级数据库的正确姿势
你的“小仓库”用久了,肯定会想升级一下,比如增加新的货架(增加 object store),或者调整货架的摆放位置(修改 object store 的结构)。 IndexedDB 的版本管理就是用来做这个的。
1. onupgradeneeded
事件
当你的代码尝试打开一个数据库,并且指定的版本号比数据库当前的版本号高时,onupgradeneeded
事件就会被触发。 这个事件处理函数是升级数据库的唯一入口。
2. 如何升级数据库
const request = indexedDB.open("myDatabase", 2); // 版本号从 1 升级到 2
request.onupgradeneeded = (event) => {
const db = event.target.result;
const oldVersion = event.oldVersion;
const newVersion = event.newVersion || db.version;
console.log(`数据库版本从 ${oldVersion} 升级到 ${newVersion}`);
// 如果数据库不存在,则创建 object store
if (!db.objectStoreNames.contains("myObjectStore")) {
const objectStore = db.createObjectStore("myObjectStore", {
keyPath: "id",
autoIncrement: true,
});
// 创建索引
objectStore.createIndex("name", "name", { unique: false });
objectStore.createIndex("age", "age", { unique: false });
console.log("创建 object store 成功!");
}
// 升级数据库结构的代码
if (oldVersion < 2) {
// 添加新的 object store
if (!db.objectStoreNames.contains("myNewObjectStore")) {
const newObjectStore = db.createObjectStore("myNewObjectStore", {
keyPath: "id",
autoIncrement: true,
});
console.log("创建 newObjectStore 成功!");
}
}
};
request.onsuccess = (event) => {
const db = event.target.result;
console.log("数据库打开成功!");
};
request.onerror = (event) => {
console.error("数据库打开失败!");
};
这段代码做了什么?
indexedDB.open("myDatabase", 2)
: 尝试打开 "myDatabase" 数据库,并将版本号设置为 2。 如果数据库当前的版本号是 1,那么onupgradeneeded
事件就会被触发。event.oldVersion
: 数据库的旧版本号。event.newVersion
: 数据库的新版本号。db.createObjectStore("myObjectStore", { keyPath: "id", autoIncrement: true })
: 创建一个名为 "myObjectStore" 的 object store,并指定id
为 keyPath (主键),并开启自增。objectStore.createIndex("name", "name", { unique: false })
: 为 "name" 字段创建一个索引,unique: false
表示允许重复的 "name"。if (oldVersion < 2)
: 判断数据库是否需要从版本 1 升级到版本 2。
3. 版本管理的注意事项
onupgradeneeded
事件处理函数必须是同步的。 也就是说,你不能在onupgradeneeded
事件处理函数中使用await
或者Promise
。 如果你需要执行异步操作,可以将操作放在onsuccess
或onerror
事件处理函数中。- 版本号必须是整数,并且必须递增。 你不能把版本号设置为 0 或者负数,也不能把版本号从 2 降到 1。
- 在
onupgradeneeded
事件处理函数中,必须使用versionchange
事务。versionchange
事务是专门用来修改数据库结构的事务,只有在这个事务中才能创建、删除或更新 object store。
4. 删除数据库
如果你想彻底删除一个数据库,可以使用 indexedDB.deleteDatabase()
方法:
const request = indexedDB.deleteDatabase("myDatabase");
request.onsuccess = () => {
console.log("数据库删除成功!");
};
request.onerror = () => {
console.error("数据库删除失败!");
};
总结: 版本管理是 IndexedDB 非常重要的一个特性,它可以让你在不丢失数据的情况下,升级你的“小仓库”。 就像你升级你的手机系统,虽然可能会花一些时间,但是可以让你体验到更好的功能。
特性 | 描述 |
---|---|
onupgradeneeded |
当数据库版本需要升级时触发,用于创建、删除或修改 object store 和索引。 |
版本号 | 必须是整数,并且必须递增。 |
事务类型 | 必须使用 versionchange 事务。 |
indexedDB.deleteDatabase() |
用于删除数据库。 |
应用场景 | 数据库结构变更,例如:增加新的 object store,修改 object store 的 keyPath,增加或删除索引。 |
三、IndexedDB 异步操作:为什么 IndexedDB 是异步的?
IndexedDB 所有的操作都是异步的。 为什么呢? 因为数据库操作可能会很耗时,如果同步执行,会导致浏览器卡死,用户体验极差。 就像你在网上购物,如果每次点击都要等半天才能响应,你肯定会直接关掉网页,换一家店买。
1. 异步操作的优势
- 避免阻塞主线程: 异步操作不会阻塞浏览器的主线程,保证用户界面的流畅性。
- 提高响应速度: 异步操作可以并发执行,提高整体的响应速度。
- 更好的用户体验: 用户可以在数据库操作的同时,继续浏览网页,不会感到卡顿。
2. IndexedDB 的异步 API
IndexedDB 的 API 都是基于事件的异步 API。 也就是说,你发起一个数据库操作后,需要监听相应的事件 (例如 onsuccess
, onerror
) 来获取操作的结果。
const request = indexedDB.open("myDatabase", 1);
request.onsuccess = (event) => {
const db = event.target.result;
const transaction = db.transaction(["myObjectStore"], "readonly");
const objectStore = transaction.objectStore("myObjectStore");
// 获取 id 为 1 的数据
const getRequest = objectStore.get(1);
getRequest.onsuccess = () => {
const data = getRequest.result;
console.log("获取到的数据:", data);
};
getRequest.onerror = () => {
console.error("获取数据失败!");
};
};
这段代码做了什么?
objectStore.get(1)
: 发起一个获取 id 为 1 的数据的异步请求。getRequest.onsuccess
: 当请求成功时触发的事件,getRequest.result
包含了获取到的数据。getRequest.onerror
: 当请求失败时触发的事件。
3. 使用 Promise 封装 IndexedDB 操作
虽然 IndexedDB 的 API 是基于事件的,但是我们可以使用 Promise 来封装这些 API,让代码更简洁,更易于维护。
function getFromIndexedDB(dbName, storeName, key) {
return new Promise((resolve, reject) => {
const request = indexedDB.open(dbName);
request.onsuccess = (event) => {
const db = event.target.result;
const transaction = db.transaction([storeName], "readonly");
const objectStore = transaction.objectStore(storeName);
const getRequest = objectStore.get(key);
getRequest.onsuccess = () => {
resolve(getRequest.result);
};
getRequest.onerror = () => {
reject(getRequest.error);
};
};
request.onerror = () => {
reject(request.error);
};
});
}
// 使用 Promise 封装的函数
getFromIndexedDB("myDatabase", "myObjectStore", 1)
.then((data) => {
console.log("获取到的数据:", data);
})
.catch((error) => {
console.error("获取数据失败!", error);
});
这段代码使用 Promise 封装了 getFromIndexedDB
函数,可以更方便地获取 IndexedDB 中的数据。
4. async/await 语法
有了 Promise,我们就可以使用 async/await
语法来更优雅地处理异步操作:
async function getDataFromIndexedDB() {
try {
const data = await getFromIndexedDB("myDatabase", "myObjectStore", 1);
console.log("获取到的数据:", data);
return data; // 返回数据
} catch (error) {
console.error("获取数据失败!", error);
throw error; // 抛出错误,方便上层处理
}
}
// 调用 async 函数
getDataFromIndexedDB()
.then(result => {
console.log("getDataFromIndexedDB 返回的结果", result);
})
.catch(error => {
console.error("getDataFromIndexedDB 出错", error);
});
这段代码使用 async/await
语法,让异步代码看起来更像同步代码,更容易理解和维护。
总结: IndexedDB 的异步操作是其核心特性之一,它可以保证浏览器的流畅性,提高用户体验。 使用 Promise 和 async/await
语法可以更方便地处理异步操作。 就像你泡茶,烧水的过程是异步的,你不用一直等着水开,可以先准备茶叶,水开了就可以直接泡茶了。
特性 | 描述 |
---|---|
异步操作 | IndexedDB 的所有操作都是异步的,避免阻塞主线程。 |
基于事件的 API | IndexedDB 的 API 都是基于事件的,需要监听相应的事件来获取操作的结果。 |
Promise 封装 | 可以使用 Promise 封装 IndexedDB 的 API,让代码更简洁,更易于维护。 |
async/await | 可以使用 async/await 语法来更优雅地处理异步操作。 |
应用场景 | 所有需要访问 IndexedDB 的场景,例如:读取数据,写入数据,更新数据,删除数据。 |
四、IndexedDB 在离线数据存储中的高级应用:实际项目中的应用场景
IndexedDB 的最大优势就是可以在浏览器中存储大量数据,并且支持离线访问。 这让它在离线应用、PWA (Progressive Web App) 等场景中大放异彩。
1. 离线应用
离线应用是指可以在没有网络连接的情况下使用的应用。 IndexedDB 可以用来存储应用的数据,让用户在离线状态下也能访问这些数据。
比如,一个新闻阅读应用可以使用 IndexedDB 存储新闻内容,用户在有网络连接的时候下载新闻,然后在没有网络连接的时候也可以阅读这些新闻。
代码示例 (简化版):
async function fetchNewsAndStore(url) {
try {
const response = await fetch(url);
const newsData = await response.json();
// 将数据存储到 IndexedDB
await storeNewsInIndexedDB(newsData);
console.log("新闻数据存储成功!");
} catch (error) {
console.error("获取新闻数据失败!", error);
}
}
async function storeNewsInIndexedDB(newsData) {
return new Promise((resolve, reject) => {
const request = indexedDB.open("newsDatabase", 1);
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains("news")) {
db.createObjectStore("news", { keyPath: "id" });
}
};
request.onsuccess = (event) => {
const db = event.target.result;
const transaction = db.transaction(["news"], "readwrite");
const objectStore = transaction.objectStore("news");
newsData.forEach(newsItem => {
objectStore.put(newsItem); // 使用 put 方法,如果 id 存在则更新,不存在则添加
});
transaction.oncomplete = () => {
console.log("所有新闻项存储完成");
resolve();
};
transaction.onerror = () => {
console.error("存储新闻项失败");
reject(transaction.error);
};
};
request.onerror = () => {
console.error("打开数据库失败");
reject(request.error);
};
});
}
async function getNewsFromIndexedDB() {
return new Promise((resolve, reject) => {
const request = indexedDB.open("newsDatabase", 1);
request.onsuccess = (event) => {
const db = event.target.result;
const transaction = db.transaction(["news"], "readonly");
const objectStore = transaction.objectStore("news");
const getAllRequest = objectStore.getAll(); // 获取所有数据
getAllRequest.onsuccess = () => {
resolve(getAllRequest.result); // 返回所有新闻数据
};
getAllRequest.onerror = () => {
reject(getAllRequest.error);
};
};
request.onerror = () => {
reject(request.error);
};
});
}
// 使用示例
// 在有网络连接时,获取新闻并存储
fetchNewsAndStore("https://example.com/api/news");
// 在没有网络连接时,从 IndexedDB 获取新闻
getNewsFromIndexedDB()
.then(news => {
console.log("从 IndexedDB 获取的新闻数据:", news);
// 显示新闻数据
})
.catch(error => {
console.error("从 IndexedDB 获取新闻数据失败!", error);
});
这段代码演示了如何从服务器获取新闻数据,并将数据存储到 IndexedDB 中。 然后,即使在没有网络连接的情况下,也可以从 IndexedDB 中获取新闻数据并显示出来。
2. PWA (Progressive Web App)
PWA 是一种可以像原生应用一样安装在设备上的 Web 应用。 IndexedDB 是 PWA 实现离线功能的重要组成部分。
PWA 可以使用 Service Worker 来拦截网络请求,如果网络不可用,则从 IndexedDB 中获取数据,从而实现离线访问。
3. 大型数据集的存储
IndexedDB 可以存储大量的数据,远超过 Cookie 和 Local Storage 的限制。 这让它非常适合存储大型数据集,比如图片、视频、音频等。
4. 数据缓存
IndexedDB 可以用来缓存 API 请求的结果,减少网络请求的次数,提高应用的性能。
5. 用户数据存储
IndexedDB 可以用来存储用户的个人数据,比如用户的设置、偏好等。 这样即使在用户清除浏览器缓存后,这些数据也不会丢失。
总结: IndexedDB 在离线数据存储中有着广泛的应用,可以用来实现离线应用、PWA、大型数据集的存储、数据缓存、用户数据存储等功能。 就像你的 U 盘,可以用来存储各种各样的文件,随时随地都可以使用。
应用场景 | 描述 |
---|---|
离线应用 | 允许应用在没有网络连接的情况下使用,用户可以访问之前下载的数据。 |
PWA (Progressive Web App) | 通过 Service Worker 和 IndexedDB 实现离线功能,提供更接近原生应用的体验。 |
大型数据集的存储 | 存储图片、视频、音频等大型数据集,突破 Cookie 和 Local Storage 的限制。 |
数据缓存 | 缓存 API 请求的结果,减少网络请求次数,提高应用性能。 |
用户数据存储 | 存储用户的设置、偏好等数据,即使在用户清除浏览器缓存后,这些数据也不会丢失。 |
实际案例 | 离线地图应用、离线笔记应用、离线音乐播放器、离线游戏。 |
好了,今天的讲座就到这里了。 希望大家对 IndexedDB 有了更深入的了解。 记住,IndexedDB 就像你的“小仓库”,好好利用它,可以让你的 Web 应用更加强大! 下次有机会再和大家分享更多 Web 开发的知识! 拜拜!