跳到主要内容

JavaScript IndexedDB

什么是 IndexedDB?

IndexedDB 是一个强大的客户端存储系统,允许你在用户浏览器中存储大量的结构化数据。与 localStorage 不同,IndexedDB 可以存储更大量的数据,并且支持索引、事务和键值对存储。它是一个面向对象的数据库,而非简单的键值存储系统。

备注

IndexedDB 是一种低级 API,用于客户端存储大量结构化数据,包括文件和 blobs。它使用索引来实现高性能数据检索。

IndexedDB 的主要特点

  • 存储容量大:理论上可以存储无限量的数据(取决于浏览器和设备存储限制)
  • 支持事务:所有操作都在事务内执行,确保数据完整性
  • 支持索引:可以根据任何属性创建索引,加速数据查询
  • 异步 API:操作不会阻塞主线程,提高应用性能
  • 支持多种数据类型:可以存储几乎任何类型的 JavaScript 对象
  • 同源策略:遵循浏览器的同源策略,保证安全性

IndexedDB 基本概念

在深入了解如何使用 IndexedDB 之前,我们需要理解几个核心概念:

  1. 数据库(Database):最顶层的容器,一个域名可以创建多个数据库
  2. 对象仓库(Object Store):类似于传统数据库中的表
  3. 索引(Index):用于高效查询的数据结构
  4. 事务(Transaction):所有数据操作都在事务中进行
  5. 游标(Cursor):用于遍历对象仓库中的记录

使用 IndexedDB

打开数据库连接

首先,我们需要打开到数据库的连接:

javascript
// 打开名为 "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("数据库设置完成");
};

添加数据

一旦数据库连接打开,我们可以添加数据:

javascript
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("添加数据失败");
};
}

获取数据

我们可以通过主键轻松获取数据:

javascript
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("获取数据失败");
};
}

使用索引查询数据

索引可以帮助我们根据非主键字段快速查找数据:

javascript
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("未找到该邮箱对应的用户");
}
};
}

更新数据

更新已有的数据记录也是常见操作:

javascript
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("更新数据失败");
};
}

删除数据

我们可以根据主键删除数据:

javascript
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("数据删除成功");
};
}

使用游标遍历数据

当我们需要遍历所有数据时,可以使用游标:

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

实际应用案例

案例一:离线待办事项应用

以下是一个简单的待办事项应用,它可以在没有网络连接的情况下工作:

javascript
// 初始化数据库
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();
};
}

案例二:离线文章阅读器

这个案例展示如何存储文章内容以便离线阅读:

javascript
// 初始化数据库
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 的版本升级

当你需要修改数据库结构(如添加新的对象仓库或索引)时,需要升级数据库版本:

javascript
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 在现代浏览器中得到了广泛支持,但在旧版浏览器中可能存在兼容性问题。考虑添加一个简单的检测:

javascript
if (!window.indexedDB) {
console.log("您的浏览器不支持 IndexedDB。");
} else {
console.log("您的浏览器支持 IndexedDB!");
}

最佳实践

使用 IndexedDB 时,以下是一些最佳实践:

  1. 使用 Promise 包装 IndexedDB:原生 API 基于事件,使用 Promise 可以简化代码
javascript
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);
});
  1. 合理设计数据结构:在开始前设计好数据模型和索引
  2. 错误处理:始终为数据库操作添加错误处理
  3. 性能考虑:对于大型操作,考虑使用游标分批处理数据
  4. 考虑存储限制:虽然存储量较大,但也不是无限的
警告

IndexedDB 不应用于存储敏感数据,因为它可以被清除或被恶意脚本访问。

总结

IndexedDB 是一个功能强大的客户端数据库,适用于需要离线功能或大量数据存储的 Web 应用。它提供了事务、索引和游标等高级功能,使得复杂的数据操作成为可能。

掌握 IndexedDB:

  1. 学会创建和打开数据库连接
  2. 理解对象仓库和索引的概念
  3. 熟悉基本的 CRUD 操作(创建、读取、更新、删除)
  4. 掌握使用事务和游标
  5. 学会处理数据库版本升级

通过 IndexedDB,你可以创建真正离线优先的 Web 应用,提供更好的用户体验。

练习

  1. 创建一个简单的联系人管理应用,使用 IndexedDB 存储联系人信息(姓名、电话、电子邮件)
  2. 实现一个离线笔记应用,允许用户创建、编辑和删除笔记
  3. 构建一个简单的离线图书库应用,可以按作者、标题或类别搜索图书
  4. 使用 IndexedDB 创建一个离线购物车功能
  5. 实现一个本地缓存系统,使用 IndexedDB 存储 API 响应

进一步学习资源

通过本教程,你应该已经掌握了 IndexedDB 的基础知识和实际应用技巧。随着你的实践经验增加,你会发现 IndexedDB 是构建强大的离线 Web 应用的重要工具。