IndexedDB 数据库设计与实现-Cordova 与 OpenHarmony 混合开发实战
本文介绍了在HarmonyOS Cordova应用中使用IndexedDB实现数据持久化的方案。IndexedDB作为浏览器提供的本地数据库API,适用于存储待办事项应用中的多种数据类型,包括任务、分类、标签、提醒等8个对象存储表。文章详细阐述了数据库初始化流程,包括版本控制、表结构创建和索引建立,并提供了完整的初始化代码示例。通过Promise处理异步操作,确保数据库连接稳定可靠。该方案为Web
欢迎大家加入开源鸿蒙跨平台开发者社区,一起共建开源鸿蒙跨平台生态。
本文对应模块:Web 层的数据持久化实现,包括 IndexedDB 数据库的初始化、CRUD 操作、高级查询、事务处理,以及与原生层数据存储的同步机制。
📌 概述
IndexedDB 是浏览器提供的一个本地数据库 API,用于在客户端存储大量数据。在 HarmonyOS Cordova 应用中,IndexedDB 是实现数据持久化的最佳选择。待办事项应用需要存储任务、分类、标签、提醒、笔记、习惯、习惯记录和目标等多种数据。数据库采用 IndexedDB 实现,包含 8 个对象存储(表)。tasks 表存储任务信息,包括标题、描述、状态、优先级、分类、截止日期等字段。categories 表存储分类信息,包括名称、图标和颜色。tags 表存储标签,reminders 表存储提醒时间,notes 表存储笔记内容,habits 表存储习惯信息,habitRecords 表存储习惯打卡记录,goals 表存储目标信息。
🔗 数据库初始化流程
当应用启动时,Web 层会首先初始化 IndexedDB 数据库。初始化过程包括打开数据库连接、创建必要的对象存储(表)和索引。如果数据库版本号增加,会触发 onupgradeneeded 事件,在这个事件中创建新的表和索引。初始化完成后,应用就可以进行数据的增删改查操作。整个初始化过程是异步的,使用 Promise 来处理成功和失败的情况。
💾 数据库初始化
完整的数据库初始化代码
/**
* 数据库模块 - IndexedDB 实现
* 负责所有数据的持久化存储和查询
*/
class DatabaseModule {
constructor() {
this.dbName = 'TodoAppDB';
this.version = 2;
this.db = null;
this.isInitialized = false;
}
/**
* 初始化数据库
* 创建所有必要的表和索引
*/
async init() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.version);
request.onerror = () => {
console.error('[DB] 数据库打开失败:', request.error);
reject(request.error);
};
request.onsuccess = () => {
this.db = request.result;
this.isInitialized = true;
console.log('[DB] 数据库初始化成功');
resolve(this.db);
};
request.onupgradeneeded = (event) => {
const db = event.target.result;
console.log('[DB] 创建数据库表...');
// 创建任务表
if (!db.objectStoreNames.contains('tasks')) {
const taskStore = db.createObjectStore('tasks', {
keyPath: 'id',
autoIncrement: true
});
// 创建索引以加快查询速度
taskStore.createIndex('status', 'status', { unique: false });
taskStore.createIndex('category', 'category', { unique: false });
taskStore.createIndex('priority', 'priority', { unique: false });
taskStore.createIndex('dueDate', 'dueDate', { unique: false });
taskStore.createIndex('createdAt', 'createdAt', { unique: false });
taskStore.createIndex('icon', 'icon', { unique: false });
taskStore.createIndex('parentId', 'parentId', { unique: false });
console.log('[DB] 任务表创建成功');
}
// 创建分类表
if (!db.objectStoreNames.contains('categories')) {
const categoryStore = db.createObjectStore('categories', {
keyPath: 'id',
autoIncrement: true
});
categoryStore.createIndex('name', 'name', { unique: true });
console.log('[DB] 分类表创建成功');
}
// 创建标签表
if (!db.objectStoreNames.contains('tags')) {
const tagStore = db.createObjectStore('tags', {
keyPath: 'id',
autoIncrement: true
});
tagStore.createIndex('name', 'name', { unique: true });
console.log('[DB] 标签表创建成功');
}
// 创建提醒表
if (!db.objectStoreNames.contains('reminders')) {
const reminderStore = db.createObjectStore('reminders', {
keyPath: 'id',
autoIncrement: true
});
reminderStore.createIndex('taskId', 'taskId', { unique: false });
reminderStore.createIndex('reminderTime', 'reminderTime', { unique: false });
console.log('[DB] 提醒表创建成功');
}
// 创建笔记表
if (!db.objectStoreNames.contains('notes')) {
const noteStore = db.createObjectStore('notes', {
keyPath: 'id',
autoIncrement: true
});
noteStore.createIndex('taskId', 'taskId', { unique: false });
noteStore.createIndex('createdAt', 'createdAt', { unique: false });
console.log('[DB] 笔记表创建成功');
}
// 创建习惯表
if (!db.objectStoreNames.contains('habits')) {
const habitStore = db.createObjectStore('habits', {
keyPath: 'id',
autoIncrement: true
});
habitStore.createIndex('createdAt', 'createdAt', { unique: false });
console.log('[DB] 习惯表创建成功');
}
// 创建习惯记录表
if (!db.objectStoreNames.contains('habitRecords')) {
const recordStore = db.createObjectStore('habitRecords', {
keyPath: 'id',
autoIncrement: true
});
recordStore.createIndex('habitId', 'habitId', { unique: false });
recordStore.createIndex('date', 'date', { unique: false });
console.log('[DB] 习惯记录表创建成功');
}
// 创建目标表
if (!db.objectStoreNames.contains('goals')) {
const goalStore = db.createObjectStore('goals', {
keyPath: 'id',
autoIncrement: true
});
goalStore.createIndex('status', 'status', { unique: false });
goalStore.createIndex('deadline', 'deadline', { unique: false });
console.log('[DB] 目标表创建成功');
}
};
});
}
/**
* 获取对象存储
* @param {string} storeName - 表名
* @param {string} mode - 访问模式 ('readonly' 或 'readwrite')
*/
getObjectStore(storeName, mode = 'readonly') {
const transaction = this.db.transaction(storeName, mode);
return transaction.objectStore(storeName);
}
}
// 创建全局数据库实例
const db = new DatabaseModule();
代码解释:
DatabaseModule 类的 init() 方法使用 indexedDB.open() 打开或创建数据库,第二个参数是版本号。当数据库版本升级时,会触发 onupgradeneeded 事件,在这个事件中创建所有必要的表和索引。createObjectStore() 方法创建表,keyPath 指定主键字段,autoIncrement 表示主键自动递增。createIndex() 方法为表创建索引,加快查询速度,unique 参数表示索引值是否唯一。getObjectStore() 方法获取表的引用,用于执行 CRUD 操作,mode 参数指定访问模式(readonly 或 readwrite)。整个初始化过程使用 Promise 来处理异步操作,成功时返回数据库实例,失败时返回错误信息。
🔍 CRUD 操作实现
添加数据
/**
* 添加任务
* @param {Object} taskData - 任务数据
*/
async addTask(taskData) {
return new Promise((resolve, reject) => {
const store = this.getObjectStore('tasks', 'readwrite');
// 准备任务数据
const task = {
title: taskData.title,
description: taskData.description || '',
status: 'pending',
priority: taskData.priority || 'medium',
category: taskData.category || 'default',
dueDate: taskData.dueDate || null,
createdAt: new Date().toISOString(),
completedAt: null,
icon: taskData.icon || '📋',
parentId: taskData.parentId || null,
tags: taskData.tags || [],
notes: taskData.notes || []
};
const request = store.add(task);
request.onsuccess = () => {
console.log('[DB] 任务添加成功, ID:', request.result);
resolve(request.result);
};
request.onerror = () => {
console.error('[DB] 任务添加失败:', request.error);
reject(request.error);
};
});
}
代码解释:
addTask() 方法首先调用 getObjectStore(‘tasks’, ‘readwrite’) 获取任务表的写入权限。然后准备任务数据,包括标题、描述、状态、优先级等字段,并为缺失的字段设置默认值。createdAt 字段自动设置为当前时间,completedAt 初始为 null。然后调用 store.add() 方法添加新记录到数据库。如果主键已存在,add() 会报错,此时应该使用 put() 方法进行更新。request.onsuccess 回调在添加成功时触发,返回新记录的自动生成 ID。request.onerror 回调在添加失败时触发,返回错误信息。整个操作被包装在 Promise 中,便于使用 async/await 语法。
查询数据
/**
* 获取所有任务
*/
async getAllTasks() {
return new Promise((resolve, reject) => {
const store = this.getObjectStore('tasks', 'readonly');
const request = store.getAll();
request.onsuccess = () => {
const tasks = request.result;
console.log('[DB] 获取所有任务:', tasks.length, '条');
resolve(tasks);
};
request.onerror = () => {
console.error('[DB] 获取任务失败:', request.error);
reject(request.error);
};
});
}
/**
* 按状态查询任务
* @param {string} status - 任务状态 ('pending' 或 'completed')
*/
async getTasksByStatus(status) {
return new Promise((resolve, reject) => {
const store = this.getObjectStore('tasks', 'readonly');
const index = store.index('status');
const request = index.getAll(status);
request.onsuccess = () => {
const tasks = request.result;
console.log('[DB] 按状态查询任务:', status, tasks.length, '条');
resolve(tasks);
};
request.onerror = () => {
console.error('[DB] 查询失败:', request.error);
reject(request.error);
};
});
}
/**
* 按分类查询任务
* @param {string} category - 分类名称
*/
async getTasksByCategory(category) {
return new Promise((resolve, reject) => {
const store = this.getObjectStore('tasks', 'readonly');
const index = store.index('category');
const request = index.getAll(category);
request.onsuccess = () => {
resolve(request.result);
};
request.onerror = () => {
reject(request.error);
};
});
}
/**
* 按 ID 获取单个任务
* @param {number} id - 任务 ID
*/
async getTaskById(id) {
return new Promise((resolve, reject) => {
const store = this.getObjectStore('tasks', 'readonly');
const request = store.get(id);
request.onsuccess = () => {
resolve(request.result);
};
request.onerror = () => {
reject(request.error);
};
});
}
代码解释:
getAllTasks() 方法使用 store.getAll() 获取任务表中的所有记录,返回一个数组。getTasksByStatus() 方法首先获取 status 索引的引用,然后使用 index.getAll(status) 查询所有状态匹配的任务。这种方法比全表扫描更高效,因为索引已经按状态排序。getTasksByCategory() 方法类似,使用 category 索引查询特定分类的任务。getTaskById() 方法使用 store.get(id) 按主键获取单个任务,这是最快的查询方式,因为主键是唯一的。所有查询方法都返回 Promise,便于使用 async/await 语法。
更新数据
/**
* 更新任务
* @param {number} id - 任务 ID
* @param {Object} updates - 要更新的字段
*/
async updateTask(id, updates) {
return new Promise((resolve, reject) => {
const store = this.getObjectStore('tasks', 'readwrite');
const getRequest = store.get(id);
getRequest.onsuccess = () => {
const task = getRequest.result;
if (!task) {
reject(new Error('任务不存在'));
return;
}
// 合并更新数据
const updatedTask = { ...task, ...updates };
const updateRequest = store.put(updatedTask);
updateRequest.onsuccess = () => {
console.log('[DB] 任务更新成功, ID:', id);
resolve(updatedTask);
};
updateRequest.onerror = () => {
console.error('[DB] 任务更新失败:', updateRequest.error);
reject(updateRequest.error);
};
};
getRequest.onerror = () => {
reject(getRequest.error);
};
});
}
/**
* 完成任务
* @param {number} id - 任务 ID
*/
async completeTask(id) {
return this.updateTask(id, {
status: 'completed',
completedAt: new Date().toISOString()
});
}
代码解释:
updateTask() 方法首先调用 store.get(id) 获取现有的任务记录。如果任务不存在,会抛出错误。如果任务存在,使用对象展开运算符(…)将更新的字段合并到现有任务对象中。然后调用 store.put() 方法更新记录。put() 方法与 add() 不同,如果主键已存在会更新,如果不存在会插入新记录。这种先查询再更新的模式确保了数据的完整性,避免了覆盖其他字段。completeTask() 方法是 updateTask() 的便利方法,用于标记任务为已完成,自动设置 status 为 ‘completed’ 和 completedAt 为当前时间。
删除数据
/**
* 删除任务
* @param {number} id - 任务 ID
*/
async deleteTask(id) {
return new Promise((resolve, reject) => {
const store = this.getObjectStore('tasks', 'readwrite');
const request = store.delete(id);
request.onsuccess = () => {
console.log('[DB] 任务删除成功, ID:', id);
resolve();
};
request.onerror = () => {
console.error('[DB] 任务删除失败:', request.error);
reject(request.error);
};
});
}
/**
* 清空所有任务
*/
async clearAllTasks() {
return new Promise((resolve, reject) => {
const store = this.getObjectStore('tasks', 'readwrite');
const request = store.clear();
request.onsuccess = () => {
console.log('[DB] 所有任务已清空');
resolve();
};
request.onerror = () => {
reject(request.error);
};
});
}
代码解释:
deleteTask() 方法使用 store.delete(id) 删除指定 ID 的任务记录。delete() 方法是异步的,成功时触发 onsuccess 回调。clearAllTasks() 方法使用 store.clear() 清空任务表中的所有记录,这是一个危险操作,应该谨慎使用。clear() 方法会删除表中的所有数据,但不会删除表本身或其索引。这两个方法都返回 Promise,便于在异步流程中使用。
📊 高级查询操作
范围查询
/**
* 查询指定日期范围内的任务
* @param {string} startDate - 开始日期 (ISO 格式)
* @param {string} endDate - 结束日期 (ISO 格式)
*/
async getTasksByDateRange(startDate, endDate) {
return new Promise((resolve, reject) => {
const store = this.getObjectStore('tasks', 'readonly');
const index = store.index('dueDate');
// 创建范围查询
const range = IDBKeyRange.bound(startDate, endDate);
const request = index.getAll(range);
request.onsuccess = () => {
resolve(request.result);
};
request.onerror = () => {
reject(request.error);
};
});
}
排序和分页
/**
* 获取分页任务列表
* @param {number} pageSize - 每页数量
* @param {number} pageNumber - 页码 (从 1 开始)
* @param {string} sortBy - 排序字段
* @param {string} order - 排序顺序 ('asc' 或 'desc')
*/
async getTasksPaginated(pageSize = 10, pageNumber = 1, sortBy = 'createdAt', order = 'desc') {
return new Promise((resolve, reject) => {
const store = this.getObjectStore('tasks', 'readonly');
const index = store.index(sortBy);
// 创建游标请求
const direction = order === 'desc' ? 'prev' : 'next';
const request = index.openCursor(null, direction);
const tasks = [];
let count = 0;
const skip = (pageNumber - 1) * pageSize;
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
if (count >= skip && count < skip + pageSize) {
tasks.push(cursor.value);
}
count++;
if (count < skip + pageSize) {
cursor.continue();
}
} else {
resolve({
data: tasks,
pageNumber: pageNumber,
pageSize: pageSize,
total: count
});
}
};
request.onerror = () => {
reject(request.error);
};
});
}
代码解释:
getTasksByDateRange() 方法使用 IDBKeyRange.bound() 创建范围查询条件,指定开始日期和结束日期。然后使用 index.getAll(range) 获取日期范围内的所有任务。getTasksPaginated() 方法实现了分页功能,首先获取指定排序字段的索引,然后使用 index.openCursor() 打开游标。游标允许逐条遍历记录,direction 参数控制遍历顺序(‘next’ 表示升序,‘prev’ 表示降序)。在遍历过程中,使用 skip 和 pageSize 计算要跳过的记录数和要取的记录数。当遍历到指定范围内的记录时,将其添加到 tasks 数组中。cursor.continue() 方法移动到下一条记录。当游标遍历完所有记录时,返回分页结果,包括数据、页码、每页数量和总记录数。
🔄 事务处理
/**
* 批量添加任务(事务处理)
* @param {Array} tasks - 任务数组
*/
async addTasksBatch(tasks) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction('tasks', 'readwrite');
const store = transaction.objectStore('tasks');
const results = [];
tasks.forEach((task) => {
const request = store.add(task);
request.onsuccess = () => {
results.push(request.result);
};
});
transaction.oncomplete = () => {
console.log('[DB] 批量添加任务成功:', results.length, '条');
resolve(results);
};
transaction.onerror = () => {
console.error('[DB] 批量添加失败:', transaction.error);
reject(transaction.error);
};
});
}
代码解释:
db.transaction() 方法创建一个事务,指定要操作的对象存储和访问模式(readwrite)。事务确保了多个操作的原子性,即事务中的所有操作要么全部成功,要么全部失败。在事务中,遍历任务数组,对每个任务调用 store.add() 方法添加到数据库。每个操作的成功结果会被收集到 results 数组中。transaction.oncomplete 事件在所有操作完成后触发,此时可以返回成功结果。transaction.onerror 事件在任何操作失败时触发,此时会拒绝 Promise 并返回错误信息。
🔌 原生层的数据存储与同步
HarmonyOS 原生层也需要与 Web 层的 IndexedDB 进行数据同步。原生层可以使用 HarmonyOS 的关系型数据库(RDB)来存储数据,并通过 Cordova 插件与 Web 层进行通信。
// ArkTS 代码示例 - 原生数据库初始化
import { relationalStore } from '@kit.ArkData';
import { common } from '@kit.AbilityKit';
import { CordovaPlugin, CallbackContext } from '@magongshou/harmony-cordova/Index';
import { PluginResult, MessageStatus } from '@magongshou/harmony-cordova/Index';
export class DatabaseSyncPlugin extends CordovaPlugin {
private rdbStore: relationalStore.RdbStore | null = null;
// 初始化原生数据库
async initNativeDatabase(callbackContext: CallbackContext, args: string[]): Promise<void> {
try {
const context = getContext() as common.UIAbilityContext;
const storeConfig: relationalStore.StoreConfig = {
name: 'TodoAppDB.db',
securityLevel: relationalStore.SecurityLevel.S1
};
this.rdbStore = await relationalStore.getRdbStore(context, storeConfig);
// 创建任务表
const createTaskTableSQL = `
CREATE TABLE IF NOT EXISTS tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
description TEXT,
status TEXT,
priority TEXT,
category TEXT,
dueDate TEXT,
createdAt TEXT,
completedAt TEXT,
icon TEXT
)
`;
await this.rdbStore.executeSql(createTaskTableSQL);
const result = PluginResult.createByString(MessageStatus.OK, '原生数据库初始化成功');
callbackContext.sendPluginResult(result);
} catch (error) {
const result = PluginResult.createByString(
MessageStatus.ERROR,
(error as Error).message
);
callbackContext.sendPluginResult(result);
}
}
// 从 Web 层同步数据到原生层
async syncTaskToNative(callbackContext: CallbackContext, args: string[]): Promise<void> {
try {
const taskData = JSON.parse(args[0]);
if (!this.rdbStore) {
throw new Error('原生数据库未初始化');
}
const valueBucket: relationalStore.ValuesBucket = {
title: taskData.title,
description: taskData.description,
status: taskData.status,
priority: taskData.priority,
category: taskData.category,
dueDate: taskData.dueDate,
createdAt: taskData.createdAt,
icon: taskData.icon
};
const rowId = await this.rdbStore.insert('tasks', valueBucket);
const result = PluginResult.createByString(
MessageStatus.OK,
JSON.stringify({ id: rowId })
);
callbackContext.sendPluginResult(result);
} catch (error) {
const result = PluginResult.createByString(
MessageStatus.ERROR,
(error as Error).message
);
callbackContext.sendPluginResult(result);
}
}
}
原生代码解释:
DatabaseSyncPlugin 是一个 Cordova 插件,负责原生层的数据库管理和与 Web 层的数据同步。initNativeDatabase 方法初始化原生数据库,使用 HarmonyOS 的 relationalStore 模块创建关系型数据库。StoreConfig 配置了数据库的名称和安全级别。然后通过 executeSql 方法执行 SQL 语句创建 tasks 表,表结构与 IndexedDB 中的任务表相同。syncTaskToNative 方法接收来自 Web 层的任务数据,将其插入到原生数据库中。首先将 JSON 字符串解析为对象,然后创建一个 ValuesBucket 对象,包含所有任务字段。最后调用 rdbStore.insert() 方法将数据插入到数据库,返回新插入记录的行 ID。
Web 层调用原生数据同步
Web 层在保存数据到 IndexedDB 后,可以调用原生插件将数据同步到原生数据库:
// JavaScript 代码 - 同步数据到原生层
async function syncTaskToNative(taskData) {
return new Promise((resolve, reject) => {
cordova.exec(
function(result) {
console.log('任务已同步到原生层:', result);
resolve(result);
},
function(error) {
console.error('同步失败:', error);
reject(error);
},
'DatabaseSyncPlugin',
'syncTaskToNative',
[JSON.stringify(taskData)]
);
});
}
// 在添加任务时调用
async function addTask(taskData) {
try {
// 先保存到 IndexedDB
const id = await db.addTask(taskData);
// 再同步到原生层
await syncTaskToNative({ ...taskData, id });
console.log('任务已保存并同步');
} catch (error) {
console.error('保存任务失败:', error);
}
}
Web 层代码解释:
syncTaskToNative 函数使用 cordova.exec() 调用原生插件的 syncTaskToNative 方法。任务数据被序列化为 JSON 字符串作为参数传递给原生层。成功回调接收原生层返回的结果,包括新插入记录的行 ID。错误回调处理同步失败的情况。addTask 函数展示了完整的流程:首先调用 db.addTask() 将任务保存到 IndexedDB,获得任务 ID。然后调用 syncTaskToNative() 将任务同步到原生数据库。这样可以确保 Web 层和原生层的数据保持同步。
📝 总结
IndexedDB 是 HarmonyOS Cordova 应用中实现 Web 层数据持久化的最佳选择。通过合理的数据库设计、高效的查询操作和事务处理,可以构建高效、可靠的数据存储系统。同时,通过 Cordova 插件与原生层的数据库进行同步,可以实现完整的跨层数据管理。在后续的文章中,我们将基于这个数据库系统实现具体的业务功能模块。
更多推荐
所有评论(0)