JavaScript IndexedDB
什么是 IndexedDB?
IndexedDB 是一个强大的客户端存储系统,允许你在用户浏览器中存储大量的结构化数据。与 localStorage 不同,IndexedDB 可以存储更大量的数据,并且支持索引、事务和键值对存储。它是一个面向对象的数据库,而非简单的键值存储系统。
IndexedDB 是一种低级 API,用于客户端存储大量结构化数据,包括文件和 blobs。它使用索引来实现高性能数据检索。
IndexedDB 的主要特点
- 存储容量大:理论上可以存储无限量的数据(取决于浏览器和设备存储限制)
- 支持事务:所有操作都在事务内执行,确保数据完整性
- 支持索引:可以根据任何属性创建索引,加速数据查询
- 异步 API:操作不会阻塞主线程,提高应用性能
- 支持多种数据类型:可以存储几乎任何类型的 JavaScript 对象
- 同源策略:遵循浏览器的同源策略,保证安全性
IndexedDB 基本概念
在深入了解如何使用 IndexedDB 之前,我们需要理解几个核心概念:
- 数据库(Database):最顶层的容器,一个域名可以创建多个数据库
- 对象仓库(Object Store):类似于传统数据库中的表
- 索引(Index):用于高效查询的数据结构
- 事务(Transaction):所有数据操作都在事务中进行
- 游标(Cursor):用于遍历对象仓库中的记录
使用 IndexedDB
打开数据库连接
首先,我们需要打开到数据库的连接:
// 打开名为 "myDatabase" 的数据库,版本为 1
let request = indexedDB.open("myDatabase", 1);
// 处理成功事件
request.onsuccess = function(event) {
let db = event.target.result;
console.log("数据库打开成功");
};
// 处理错误事件
request.onerror = function(event) {
console.error("打开数据库失败: " + event.target.errorCode);
};
// 处理升级事件(首次创建数据库或版本升级时触发)
request.onupgradeneeded = function(event) {
let db = event.target.result;
// 创建一个对象仓库
let objectStore = db.createObjectStore("customers", { keyPath: "id" });
// 创建索引
objectStore.createIndex("name", "name", { unique: false });
objectStore.createIndex("email", "email", { unique: true });
console.log("数据库设置完成");
};
添加数据
一旦数据库连接打开,我们可以添加数据:
function addData(db) {
// 创建一个新的事务
let transaction = db.transaction(["customers"], "readwrite");
// 获取对象仓库
let objectStore = transaction.objectStore("customers");
// 添加一条数据
let customer = { id: 1, name: "张三", email: "zhangsan@example.com", age: 35 };
let request = objectStore.add(customer);
request.onsuccess = function(event) {
console.log("数据添加成功");
};
request.onerror = function(event) {
console.error("添加数据失败");
};
}
获取数据
我们可以通过主键轻松获取数据:
function getData(db, id) {
let transaction = db.transaction(["customers"]);
let objectStore = transaction.objectStore("customers");
let request = objectStore.get(id);
request.onsuccess = function(event) {
if (request.result) {
console.log("获取到的数据:", request.result);
// 输出: {id: 1, name: "张三", email: "zhangsan@example.com", age: 35}
} else {
console.log("未找到对应数据");
}
};
request.onerror = function(event) {
console.error("获取数据失败");
};
}
使用索引查询数据
索引可以帮助我们根据非主键字段快速查找数据:
function getDataByIndex(db, email) {
let transaction = db.transaction(["customers"], "readonly");
let objectStore = transaction.objectStore("customers");
let index = objectStore.index("email");
let request = index.get(email);
request.onsuccess = function(event) {
if (request.result) {
console.log("通过邮箱找到的用户:", request.result);
} else {
console.log("未找到该邮箱对应的用户");
}
};
}
更新数据
更新已有的数据记录也是常见操作:
function updateData(db, customer) {
let transaction = db.transaction(["customers"], "readwrite");
let objectStore = transaction.objectStore("customers");
// customer 对象必须包含完整的数据和正确的主键
let request = objectStore.put(customer);
request.onsuccess = function(event) {
console.log("数据更新成功");
};
request.onerror = function(event) {
console.error("更新数据失败");
};
}
删除数据
我们可以根据主键删除数据:
function deleteData(db, id) {
let transaction = db.transaction(["customers"], "readwrite");
let objectStore = transaction.objectStore("customers");
let request = objectStore.delete(id);
request.onsuccess = function(event) {
console.log("数据删除成功");
};
}
使用游标遍历数据
当我们需要遍历所有数据时,可以使用游标:
function getAllData(db) {
let transaction = db.transaction(["customers"], "readonly");
let objectStore = transaction.objectStore("customers");
let request = objectStore.openCursor();
let customers = [];
request.onsuccess = function(event) {
let cursor = event.target.result;
if (cursor) {
customers.push(cursor.value);
cursor.continue(); // 移动到下一条记录
} else {
console.log("所有客户数据:", customers);
}
};
}
实际应用案例
案例一:离线待办事项应用
以下是一个简单的待办事项应用,它可以在没有网络连接的情况下工作:
// 初始化数据库
let db;
const dbRequest = indexedDB.open("todoApp", 1);
dbRequest.onupgradeneeded = function(event) {
db = event.target.result;
const todoStore = db.createObjectStore("todos", { keyPath: "id", autoIncrement: true });
todoStore.createIndex("status", "status", { unique: false });
};
dbRequest.onsuccess = function(event) {
db = event.target.result;
displayTodos();
};
// 添加待办事项
function addTodo(text) {
const transaction = db.transaction(["todos"], "readwrite");
const todoStore = transaction.objectStore("todos");
const todo = {
text: text,
status: "pending",
createdAt: new Date()
};
const request = todoStore.add(todo);
request.onsuccess = function() {
console.log("待办事项已添加");
displayTodos();
};
}
// 显示所有待办事项
function displayTodos() {
const todoList = document.getElementById("todo-list");
todoList.innerHTML = "";
const transaction = db.transaction(["todos"], "readonly");
const todoStore = transaction.objectStore("todos");
const request = todoStore.openCursor();
request.onsuccess = function(event) {
const cursor = event.target.result;
if (cursor) {
const todo = cursor.value;
const li = document.createElement("li");
li.textContent = todo.text;
li.dataset.id = todo.id;
if (todo.status === "completed") {
li.classList.add("completed");
}
todoList.appendChild(li);
cursor.continue();
}
};
}
// 更新待办事项状态
function toggleTodoStatus(id) {
const transaction = db.transaction(["todos"], "readwrite");
const todoStore = transaction.objectStore("todos");
const request = todoStore.get(Number(id));
request.onsuccess = function() {
const todo = request.result;
todo.status = todo.status === "pending" ? "completed" : "pending";
todoStore.put(todo);
displayTodos();
};
}
案例二:离线文章阅读器
这个案例展示如何存储文章内容以便离线阅读:
// 初始化数据库
let articlesDB;
const dbRequest = indexedDB.open("articlesReader", 1);
dbRequest.onupgradeneeded = function(event) {
articlesDB = event.target.result;
const articlesStore = articlesDB.createObjectStore("articles", { keyPath: "id" });
articlesStore.createIndex("title", "title", { unique: false });
articlesStore.createIndex("author", "author", { unique: false });
};
dbRequest.onsuccess = function(event) {
articlesDB = event.target.result;
loadArticlesList();
};
// 从服务器获取文章并存储
async function fetchAndStoreArticles() {
try {
const response = await fetch('https://api.example.com/articles');
const articles = await response.json();
const transaction = articlesDB.transaction(["articles"], "readwrite");
const articlesStore = transaction.objectStore("articles");
articles.forEach(article => {
articlesStore.put(article);
});
transaction.oncomplete = function() {
console.log("文章已保存到本地");
loadArticlesList();
};
} catch (error) {
console.error("获取文章失败:", error);
// 加载本地存储的文章
loadArticlesList();
}
}
// 加载文章列表
function loadArticlesList() {
const articlesList = document.getElementById("articles-list");
articlesList.innerHTML = "";
const transaction = articlesDB.transaction(["articles"], "readonly");
const articlesStore = transaction.objectStore("articles");
const request = articlesStore.openCursor();
request.onsuccess = function(event) {
const cursor = event.target.result;
if (cursor) {
const article = cursor.value;
const li = document.createElement("li");
const title = document.createElement("h3");
title.textContent = article.title;
const author = document.createElement("p");
author.textContent = `作者: ${article.author}`;
li.appendChild(title);
li.appendChild(author);
li.addEventListener("click", () => showArticle(article.id));
articlesList.appendChild(li);
cursor.continue();
}
};
}
// 显示文章内容
function showArticle(id) {
const transaction = articlesDB.transaction(["articles"], "readonly");
const articlesStore = transaction.objectStore("articles");
const request = articlesStore.get(id);
request.onsuccess = function() {
const article = request.result;
const contentDiv = document.getElementById("article-content");
contentDiv.innerHTML = `
<h2>${article.title}</h2>
<p class="author">作者: ${article.author}</p>
<div class="content">${article.content}</div>
`;
};
}
处理 IndexedDB 的版本升级
当你需要修改数据库结构(如添加新的对象仓库或索引)时,需要升级数据库版本:
let dbRequest = indexedDB.open("myDatabase", 2); // 注意版本号为 2
dbRequest.onupgradeneeded = function(event) {
let db = event.target.result;
// 检查是从哪个版本升级而来
let oldVersion = event.oldVersion;
if (oldVersion < 1) {
// 从无到版本 1 的升级代码
let customerStore = db.createObjectStore("customers", { keyPath: "id" });
customerStore.createIndex("name", "name", { unique: false });
customerStore.createIndex("email", "email", { unique: true });
}
if (oldVersion < 2) {
// 从版本 1 到版本 2 的升级代码
let orderStore = db.createObjectStore("orders", { keyPath: "orderId" });
orderStore.createIndex("customerId", "customerId", { unique: false });
orderStore.createIndex("orderDate", "orderDate", { unique: false });
}
};
IndexedDB 的浏览器兼容性
IndexedDB 在现代浏览器中得到了广泛支持,但在旧版浏览器中可能存在兼容性问题。考虑添加一个简单的检测:
if (!window.indexedDB) {
console.log("您的浏览器不支持 IndexedDB。");
} else {
console.log("您的浏览器支持 IndexedDB!");
}
最佳实践
使用 IndexedDB 时,以下是一些最佳实践:
- 使用 Promise 包装 IndexedDB:原生 API 基于事件,使用 Promise 可以简化代码
function openDB(name, version) {
return new Promise((resolve, reject) => {
const request = indexedDB.open(name, version);
request.onupgradeneeded = (event) => {
const db = event.target.result;
// 在这里进行数据库结构设置
};
request.onsuccess = (event) => {
resolve(event.target.result);
};
request.onerror = (event) => {
reject(event.target.error);
};
});
}
// 使用方式
openDB("myDatabase", 1)
.then(db => {
console.log("数据库打开成功");
})
.catch(error => {
console.error("打开数据库失败:", error);
});
- 合理设计数据结构:在开始前设计好数据模型和索引
- 错误处理:始终为数据库操作添加错误处理
- 性能考虑:对于大型操作,考虑使用游标分批处理数据
- 考虑存储限制:虽然存储量较大,但也不是无限的
IndexedDB 不应用于存储敏感数据,因为它可以被清除或被恶意脚本访问。
总结
IndexedDB 是一个功能强大的客户端数据库,适用于需要离线功能或大量数据存储的 Web 应用。它提供了事务、索引和游标等高级功能,使得复杂的数据操作成为可能。
掌握 IndexedDB:
- 学会创建和打开数据库连接
- 理解对象仓库和索引的概念
- 熟悉基本的 CRUD 操作(创建、读取、更新、删除)
- 掌握使用事务和游标
- 学会处理数据库版本升级
通过 IndexedDB,你可以创建真正离线优先的 Web 应用,提供更好的用户体验。
练习
- 创建一个简单的联系人管理应用,使用 IndexedDB 存储联系人信息(姓名、电话、电子邮件)
- 实现一个离线笔记应用,允许用户创建、编辑和删除笔记
- 构建一个简单的离线图书库应用,可以按作者、标题或类别搜索图书
- 使用 IndexedDB 创建一个离线购物车功能
- 实现一个本地缓存系统,使用 IndexedDB 存储 API 响应
进一步学习资源
通过本教程,你应该已经掌握了 IndexedDB 的基础知识和实际应用技巧。随着你的实践经验增加,你会发现 IndexedDB 是构建强大的离线 Web 应用的重要工具。