HarmonyOS NEXT 企业级记账APP:数据导出、备份与恢复

📅 2026/8/2 4:03:33
HarmonyOS NEXT 企业级记账APP:数据导出、备份与恢复
数据导出、备份与恢复本文是《HarmonyOS NEXT 企业级开发实战30篇打造智能记账APP》系列的第25篇对应 Git Tagv0.2.5。承接上一篇设置中心开发本篇深入讲解SettingView中的exportData数据导出方法与doClear数据清空实现并扩展探讨本地备份与数据恢复的完整设计方案。前言数据是记账应用的核心资产。用户产生的每一笔账单、每一个分类都应该可以被导出、备份和恢复。企业级应用必须让用户掌控自己的数据而不是将数据锁死在应用内部。本文将带你深入解析SettingView中exportData方法的实现细节设计ExportData接口约束导出数据结构掌握PreferenceUtil偏好存储的读写机制实现doClear数据清空与二次确认防护扩展设计本地备份与恢复的完整方案企业级核心原则数据必须可导出、可备份、可恢复同时操作必须类型安全。ArkTS 严格禁止使用any类型所有数据结构通过interface明确定义。参考 HarmonyOS NEXT 开发者文档 了解官方约定。一、需求分析1.1 功能介绍数据导出与清空功能集成在SettingView设置页面中无需独立页面。导出功能将账单与分类数据序列化为 JSON 格式清空功能通过ConfirmDialog二次确认后清除所有本地数据。需求项说明实现位置数据导出读取bills与categories偏好数据组装为 JSON 格式SettingView.exportData数据清空清除所有偏好数据二次确认防护SettingView.doClear数据备份将全量数据序列化存储为备份文件扩展设计PreferenceUtil.setObject数据恢复从备份 JSON 解析并还原数据扩展设计PreferenceUtil.getObject1.2 导出格式对比记账应用的数据导出通常支持多种格式不同格式各有优劣。格式优点缺点适用场景JSON结构完整、易于解析、保留类型体积略大完整备份与恢复CSVExcel 可直接打开、体积小转义复杂、丢失类型财务报表导出XLSX支持公式与样式依赖第三方库高级报表导出本项目当前采用JSON 格式导出因为 JSON 能完整保留数据结构便于后续恢复。1.3 业务流程用户进入 SettingView ↓ 点击导出数据列表项 ↓ exportData 方法执行 ↓ PreferenceUtil.getString 读取 bills 和 categories ↓ 组装 ExportData 接口对象 ↓ JSON.stringify 序列化为 JSON 字符串 ↓ ToastUtil.show 提示数据已准备JSON格式 —— 清空流程 —— 用户点击清空所有数据 ↓ showClearConfirm true → 弹出 ConfirmDialog ↓ 用户点击清空确认 ↓ doClear 方法执行 ↓ PreferenceUtil.clear 清空所有偏好 ↓ ToastUtil.show 提示数据已清空二、数据导出核心实现2.1 exportData 方法详解exportData是SettingView中的私有异步方法负责从PreferenceUtil读取账单与分类数据组装为结构化 JSON 后输出。以下是实际源码中的完整实现。// pages/SettingView.ets - exportData 方法 private async exportData(): Promisevoid { const billsJson await PreferenceUtil.getInstance().getString(bills, []); const categoriesJson await PreferenceUtil.getInstance().getString(categories, []); interface ExportData { bills: string; categories: string; exportTime: string; } const data: ExportData { bills: billsJson, categories: categoriesJson, exportTime: new Date().toISOString() }; const json JSON.stringify(data, null, 2); ToastUtil.show(数据已准备JSON格式); }2.2 ExportData 接口设计ExportData接口定义了导出数据的结构约束包含三个字段。这是 ArkTS 类型安全的核心实践用interface代替any类型确保编译期类型检查。interface ExportData { bills: string; // 账单数据的 JSON 字符串 categories: string; // 分类数据的 JSON 字符串 exportTime: string; // 导出时间ISO 8601 格式 }字段类型说明默认值billsstring账单数据 JSON 字符串[]categoriesstring分类数据 JSON 字符串[]exportTimestring导出时间戳new Date().toISOString()2.3 PreferenceUtil 数据读取导出数据的核心是调用PreferenceUtil.getInstance().getString()方法读取已持久化的账单与分类数据。getString方法接收key和defaultValue两个参数。const billsJson await PreferenceUtil.getInstance().getString(bills, []); const categoriesJson await PreferenceUtil.getInstance().getString(categories, []);关键点getString的第二个参数是默认值当偏好中不存在对应key时返回该默认值。这里使用[]空数组 JSON确保即使没有数据也能正常导出空结构。2.4 JSON 序列化输出组装好ExportData对象后通过JSON.stringify序列化为格式化的 JSON 字符串。第三个参数2表示使用 2 个空格缩进便于人类阅读。const json JSON.stringify(data, null, 2); ToastUtil.show(数据已准备JSON格式);序列化后的 JSON 输出示例如下{bills:[{\id\:\1\,\money\:100,\type\:\expense\,\remark\:\午餐\,\date\:\2026-07-28\}],categories:[{\id\:\1\,\name\:\餐饮\,\type\:\expense\}],exportTime:2026-07-28T10:30:00.000Z}三、SettingView 中的导出入口3.1 导出数据列表项数据导出功能作为SettingView设置列表的一个ListItem点击后触发exportData方法。列表项采用标准箭头布局左侧标题、右侧箭头图标。// 数据导出 ListItem() { Row() { Text(导出数据).fontSize(AppFontSize.MD).fontColor(AppColors.PrimaryText).layoutWeight(1) Image($r(app.media.icon_arrow_right)).width(20).height(20).fillColor(AppColors.SecondaryText) } .width(100%).height(56) .padding({ left: AppSpace.MD, right: AppSpace.MD }) .backgroundColor(AppColors.CardBackground).borderRadius(AppSpace.CardRadius) .onClick(() { this.exportData(); }) }3.2 完整 SettingView 代码以下是SettingView的完整实现包含导出数据与清空数据两个核心功能。所有代码与实际源码完全一致。// pages/SettingView.ets import { AppColors } from ../theme/Colors; import { AppFontSize } from ../theme/Typography; import { AppSpace } from ../theme/Spacing; import { RouterUtil } from ../utils/RouterUtil; import { SettingRepository } from ../repository/SettingRepository; import { PreferenceUtil } from ../utils/PreferenceUtil; import { ToastUtil } from ../utils/ToastUtil; import { ConfirmDialog } from ../components/dialog/ConfirmDialog; Entry Component struct SettingView { State darkMode: boolean false; State showClearConfirm: boolean false; aboutToAppear(): void { this.loadSettings(); } private async loadSettings(): Promisevoid { this.darkMode await SettingRepository.getInstance().loadDarkMode(); } build() { Column() { Row() { Image($r(app.media.icon_back)).width(24).height(24).fillColor(AppColors.PrimaryText) .onClick(() { RouterUtil.back(); }) Text(设置).fontSize(AppFontSize.XL).fontWeight(FontWeight.Bold).layoutWeight(1).textAlign(TextAlign.Center) }.width(100%).height(56).alignItems(VerticalAlign.Center) List({ space: AppSpace.SM }) { // 深色模式 ListItem() { Row() { Text(深色模式).fontSize(AppFontSize.MD).fontColor(AppColors.PrimaryText).layoutWeight(1) Toggle({ type: ToggleType.Switch, isOn: this.darkMode }) .onChange((isOn: boolean) { this.darkMode isOn; SettingRepository.getInstance().saveDarkMode(isOn); ToastUtil.show(重启应用后生效); }) } .width(100%).height(56) .padding({ left: AppSpace.MD, right: AppSpace.MD }) .backgroundColor(AppColors.CardBackground).borderRadius(AppSpace.CardRadius) } // 数据导出 ListItem() { Row() { Text(导出数据).fontSize(AppFontSize.MD).fontColor(AppColors.PrimaryText).layoutWeight(1) Image($r(app.media.icon_arrow_right)).width(20).height(20).fillColor(AppColors.SecondaryText) } .width(100%).height(56) .padding({ left: AppSpace.MD, right: AppSpace.MD }) .backgroundColor(AppColors.CardBackground).borderRadius(AppSpace.CardRadius) .onClick(() { this.exportData(); }) } // 清空数据 ListItem() { Row() { Text(清空所有数据).fontSize(AppFontSize.MD).fontColor(AppColors.Expense).layoutWeight(1) } .width(100%).height(56) .padding({ left: AppSpace.MD, right: AppSpace.MD }) .backgroundColor(AppColors.CardBackground).borderRadius(AppSpace.CardRadius) .onClick(() { this.showClearConfirm true; }) } // 关于 ListItem() { Row() { Text(关于).fontSize(AppFontSize.MD).fontColor(AppColors.PrimaryText).layoutWeight(1) Image($r(app.media.icon_arrow_right)).width(20).height(20).fillColor(AppColors.SecondaryText) } .width(100%).height(56) .padding({ left: AppSpace.MD, right: AppSpace.MD }) .backgroundColor(AppColors.CardBackground).borderRadius(AppSpace.CardRadius) .onClick(() { RouterUtil.push(pages/AboutView); }) } } .layoutWeight(1).margin({ top: AppSpace.MD }) if (this.showClearConfirm) { ConfirmDialog({ title: 确认清空, message: 清空后将删除所有账单、分类和预算数据此操作不可恢复, confirmText: 清空, confirmColor: AppColors.Expense, onConfirm: () { this.doClear(); }, onCancel: () { this.showClearConfirm false; } }) } } .height(100%).padding({ left: AppSpace.XL, right: AppSpace.XL, top: AppSpace.MD }) .backgroundColor(AppColors.Background) } private async exportData(): Promisevoid { const billsJson await PreferenceUtil.getInstance().getString(bills, []); const categoriesJson await PreferenceUtil.getInstance().getString(categories, []); interface ExportData { bills: string; categories: string; exportTime: string; } const data: ExportData { bills: billsJson, categories: categoriesJson, exportTime: new Date().toISOString() }; const json JSON.stringify(data, null, 2); ToastUtil.show(数据已准备JSON格式); } private async doClear(): Promisevoid { await PreferenceUtil.getInstance().clear(); this.showClearConfirm false; ToastUtil.show(数据已清空); } }3.3 导出触发流程导出流程从用户点击导出数据列表项开始经过数据读取、接口组装、JSON 序列化三个步骤最终通过 Toast 反馈结果用户点击导出列表项触发onClick回调调用this.exportData()私有方法PreferenceUtil.getString异步读取bills和categories组装为ExportData接口对象JSON.stringify序列化为格式化 JSONToastUtil.show提示导出完成四、数据清空与恢复4.1 doClear 清空实现doClear方法是数据清空的核心实现调用PreferenceUtil.getInstance().clear()清空所有偏好数据。清空完成后关闭确认对话框并提示用户。// pages/SettingView.ets - doClear 方法 private async doClear(): Promisevoid { await PreferenceUtil.getInstance().clear(); this.showClearConfirm false; ToastUtil.show(数据已清空); }4.2 ConfirmDialog 二次确认数据清空是不可恢复的高危操作必须通过ConfirmDialog进行二次确认。在SettingView中通过State showClearConfirm控制对话框的显示与隐藏。if (this.showClearConfirm) { ConfirmDialog({ title: 确认清空, message: 清空后将删除所有账单、分类和预算数据此操作不可恢复, confirmText: 清空, confirmColor: AppColors.Expense, onConfirm: () { this.doClear(); }, onCancel: () { this.showClearConfirm false; } }) }安全设计确认按钮使用AppColors.Expense红色背景突出危险操作文案明确告知此操作不可恢复最大程度避免误操作。4.3 清空操作防护策略防护层级实现方式说明视觉警示AppColors.Expense红色文字列表项标题使用红色二次确认ConfirmDialog弹窗必须点击清空按钮确认文案明确“此操作不可恢复”明确告知后果取消机制onCancel回调点击取消或遮罩关闭五、PreferenceUtil 存储机制5.1 偏好存储初始化PreferenceUtil是数据导出与清空的底层支撑。它基于kit.ArkData的preferences模块需要在应用启动时初始化。// utils/PreferenceUtil.ets import { preferences } from kit.ArkData; import { common } from kit.AbilityKit; const PREF_NAME harmonyledger; export class PreferenceUtil { private pref: preferences.Preferences | null null; private static instance: PreferenceUtil | null null; static getInstance(): PreferenceUtil { if (PreferenceUtil.instance null) { PreferenceUtil.instance new PreferenceUtil(); } return PreferenceUtil.instance; } async init(context: common.Context): Promisevoid { this.pref await preferences.getPreferences(context, PREF_NAME); } }5.2 字符串读写方法数据导出依赖getString方法读取已持久化的数据数据清空依赖clear方法清除所有数据。async setString(key: string, value: string): Promisevoid { if (this.pref null) return; await this.pref.put(key, value); await this.pref.flush(); } async getString(key: string, defaultValue: string ): Promisestring { if (this.pref null) return defaultValue; let result: ESObject await this.pref.get(key, defaultValue); return result; }5.3 对象序列化存储PreferenceUtil还提供了泛型对象存储方法setObjectT用于将复杂对象序列化为 JSON 后存储。这在备份恢复场景中非常实用。async setObjectT(key: string, value: T): Promisevoid { if (this.pref null) return; const json JSON.stringify(value); await this.pref.put(key, json); await this.pref.flush(); } async getObject(key: string, defaultValue: ESObject | null null): PromiseESObject | null { if (this.pref null) return defaultValue; let json: ESObject await this.pref.get(key, ); let jsonStr: string json; if (jsonStr.length 0) { return defaultValue; } try { return JSON.parse(jsonStr); } catch (e) { return defaultValue; } }5.4 clear 全量清空clear方法清除偏好存储中的所有数据SettingView.doClear正是调用此方法实现数据清空。async clear(): Promisevoid { if (this.pref null) return; await this.pref.clear(); await this.pref.flush(); }5.5 API 速查表方法参数返回值导出场景用途getStringkey, defaultValuePromisestring读取 bills/categories 数据setStringkey, valuePromisevoid写入导出数据setObjectTkey, value: TPromisevoid存储备份对象getObjectkey, defaultValuePromiseESObject | null读取备份对象clear无Promisevoid清空所有数据六、备份恢复扩展设计6.1 备份数据结构当前exportData方法导出的数据包含账单与分类。完整的备份还应包含预算与设置数据。以下是推荐的备份接口定义。// 扩展设计完整备份接口 interface BackupData { bills: string; categories: string; budgets: string; settings: string; version: string; backupTime: string; }字段类型说明billsstring账单数据 JSONcategoriesstring分类数据 JSONbudgetsstring预算数据 JSONsettingsstring设置项 JSONversionstring应用版本号用于兼容性校验backupTimestring备份时间戳6.2 本地备份方案利用PreferenceUtil.setObject方法可以将完整的备份数据序列化后存储到本地偏好中。// 扩展设计本地备份方法 private async createBackup(): Promisevoid { const billsJson await PreferenceUtil.getInstance().getString(bills, []); const categoriesJson await PreferenceUtil.getInstance().getString(categories, []); const budgetsJson await PreferenceUtil.getInstance().getString(budgets, []); interface BackupData { bills: string; categories: string; budgets: string; version: string; backupTime: string; } const backup: BackupData { bills: billsJson, categories: categoriesJson, budgets: budgetsJson, version: 1.0.0, backupTime: new Date().toISOString() }; await PreferenceUtil.getInstance().setObjectBackupData(backup_latest, backup); ToastUtil.show(备份成功); }6.3 恢复流程设计恢复数据时通过PreferenceUtil.getObject读取备份对象解析后逐项写回偏好存储。恢复前应进行版本校验。// 扩展设计数据恢复方法 private async restoreBackup(): Promisevoid { const backup await PreferenceUtil.getInstance().getObject(backup_latest, null); if (backup null) { ToastUtil.show(暂无备份); return; } try { const bills: string backup.bills; const categories: string backup.categories; await PreferenceUtil.getInstance().setString(bills, bills); await PreferenceUtil.getInstance().setString(categories, categories); ToastUtil.show(恢复成功); } catch (e) { ToastUtil.show(恢复失败数据格式错误); } }6.4 版本兼容处理备份与恢复的核心挑战是版本兼容。当应用升级导致数据结构变更时旧备份可能无法直接恢复。// 扩展设计版本校验 interface BackupData { bills: string; categories: string; budgets: string; version: string; backupTime: string; } private checkVersion(backup: BackupData, currentVersion: string): boolean { if (backup.version ! currentVersion) { console.warn(Backup version mismatch: backup.version vs currentVersion); // 可在此处添加数据迁移逻辑 return false; } return true; }七、导出格式详解7.1 JSON 格式结构当前项目采用 JSON 格式导出JSON.stringify(data, null, 2)的第三个参数2表示使用 2 个空格缩进。导出的 JSON 结构清晰、易于解析。const json JSON.stringify(data, null, 2);{bills:[{\id\:\1\,\money\:100,\type\:\expense\}],categories:[{\id\:\1\,\name\:\餐饮\}],exportTime:2026-07-28T10:30:00.000Z}注意bills和categories字段的值是 JSON 字符串字符串内嵌套 JSON而非直接的数组。这是因为PreferenceUtil存储时已将数组序列化为字符串。7.2 CSV 格式对比若需支持 CSV 导出可将账单数据转换为逗号分隔的表格格式。但 CSV 无法表达嵌套结构适合简单报表场景。// 扩展设计CSV 导出仅账单 private exportBillsToCSV(billsJson: string): string { const bills: ArrayESObject JSON.parse(billsJson); const header: string id,money,type,remark,date; const rows: string bills.map((b: ESObject) { return b.id , b.money , b.type , b.remark , b.date; }).join(\n); return header \n rows; }7.3 格式选择建议维度JSONCSV结构完整性保留嵌套结构扁平化类型保留保留原始类型全部转字符串可读性需格式化工具Excel 直接打开恢复能力可完整恢复仅可恢复账单体积较大较小建议日常备份使用 JSON 格式财务报表导出可扩展 CSV 格式。八、ArkTS 类型安全规范8.1 禁用 any 类型ArkTS 严格禁止使用any类型。在数据导出场景中所有变量都有明确的类型标注包括string、boolean、Promisevoid等。违反此规范会导致编译错误。8.2 interface 定义规范数据导出的核心是ExportData接口。通过interface定义数据结构可以确保编译期类型检查避免运行时错误。错误写法禁止// 违反 ArkTS 类型安全禁止使用 any private async exportData(): Promisevoid { const data: any { bills: , categories: , exportTime: }; const json JSON.stringify(data); }正确写法推荐// 使用 interface 定义明确的类型约束 interface ExportData { bills: string; categories: string; exportTime: string; } const data: ExportData { bills: billsJson, categories: categoriesJson, exportTime: new Date().toISOString() };8.3 类型规范对比场景错误写法正确写法导出数据类型data: anydata: ExportData仓库类名中文类名编译报错SettingRepository页面结构体名中文结构体名编译报错SettingView列表数据类型data: Arrayanydata: ArrayExportDataForEach 键值函数(item: any) item.id(item: ExportData) item.exportTime方法参数类型save(item: any)setObjectT(key: string, value: T)8.4 异步方法返回类型exportData和doClear都是异步方法返回类型必须显式标注为Promisevoidprivate async exportData(): Promisevoid { ... } private async doClear(): Promisevoid { ... }九、运行验证9.1 构建命令hvigorw assembleHap--modemodule-pproductdefault9.2 验证清单验证项操作步骤预期结果导出空数据无账单时点击导出数据Toast 提示数据已准备JSON格式导出有数据有账单时点击导出数据Toast 提示数据已准备JSON格式JSON 格式正确检查json变量内容包含 bills、categories、exportTime 三字段清空确认弹窗点击清空所有数据弹出 ConfirmDialog 确认框取消清空在确认框点击取消对话框关闭数据不变确认清空在确认框点击清空数据清空Toast 提示数据已清空清空后验证清空后查看账单页账单列表为空默认值生效无 bills 数据时导出billsJson为[]验证要点导出功能需检查 JSON 内容是否正确清空功能需确认数据确实被清除。十、常见问题10.1 导出数据为空现象导出后 JSON 中bills字段为[]但页面上有账单数据。原因PreferenceUtil未初始化getString始终返回默认值[]。解决在EntryAbility.onCreate中调用PreferenceUtil.getInstance().init(this.context)完成初始化。10.2 清空后数据仍在现象点击清空并确认后重新进入账单页数据仍然存在。原因PreferenceUtil.pref为nullclear方法直接return未执行。解决确保PreferenceUtil.init已在应用启动时调用。10.3 JSON 序列化失败现象JSON.stringify抛出异常。原因ExportData对象中包含循环引用或undefined值。解决确保所有字段值都是基本类型string、number、boolean不包含函数或循环引用对象。10.4 恢复数据格式错误现象恢复备份时提示数据格式错误。原因备份 JSON 结构与当前版本不匹配或备份文件被篡改。解决在恢复前进行版本校验与数据结构校验参考第六章的版本兼容处理方案。十一、Git 提交11.1 提交命令gitadd.gitcommit-mfeat(data): 数据导出与清空 - 实现 SettingView.exportData 数据导出JSON格式 - 实现 SettingView.doClear 数据清空二次确认 - 定义 ExportData 接口约束导出结构 - 基于 PreferenceUtil 完成数据读写 - 扩展设计备份恢复方案11.2 变更记录## [v0.2.5] - 2026-07-28 ### Added - SettingView.exportData 数据导出方法 - SettingView.doClear 数据清空方法 - ExportData 接口定义 ### Changed - 完善设置中心数据管理能力附录运行效果截图总结本文完整介绍了数据导出、备份与恢复的实现方案基于SettingView中的exportData与doClear方法展开深入解析了PreferenceUtil存储机制并扩展设计了本地备份与恢复的完整方案。通过本篇你可以实现 JSON 格式的数据导出使用ExportData接口约束结构掌握PreferenceUtil偏好存储的读写与清空机制封装ConfirmDialog二次确认保护高危清空操作扩展设计本地备份与数据恢复的完整链路遵循 ArkTS 类型安全规范使用interface代替any类型下一篇预告第 26 篇将进入应用性能优化与体验打磨阶段涵盖列表懒加载、防抖节流、骨架屏等企业级优化实践。如果这篇文章对你有帮助欢迎点赞、收藏、关注你的支持是我持续创作的动力欢迎在评论区留言讨论或参与下方投票告诉我你最关注记账应用的哪个数据管理能力相关资源本篇源码GitHub Tag v0.2.5HarmonyOS NEXT 开发者文档developer.harmonyos.com鸿蒙数据存储 preferencesdata-preferences 指南ArkUI List 组件list 组件参考ArkUI State 状态管理state 装饰器ArkUI 条件渲染if/else 渲染控制JSON API 参考JSON 处理方法ArkTS 类型安全规范arkts 类型约束CSV 格式规范RFC 4180