HarmonyOS 调用系统文件保存器导出 JSON 与 CSV

为什么"能导出"很重要

本地应用经常把"不上传云端"作为卖点,但如果用户只能在应用里查看、无法导出,那么数据依然被锁在产品内部。

在"心晴手记"中,我提供了两种导出:

  • 完整 JSON:包含心情、习惯和打卡,适合备份;
  • 心情 CSV:适合用表格软件查看或做个人分析。

保存位置不由应用擅自决定,而是打开系统文件保存器,让用户选择目标目录和文件名。

一、把系统保存流程封装成通用函数

先引入 AbilityKit 和 CoreFileKit:

typescript 复制代码
import { common } from '@kit.AbilityKit';
import { fileIo, picker } from '@kit.CoreFileKit';

通用函数接收文件名和文本内容:

typescript 复制代码
export async function exportTextFile(
  context: common.UIAbilityContext,
  fileName: string,
  content: string
): Promise<boolean> {
  try {
    const options = new picker.DocumentSaveOptions();
    options.newFileNames = [fileName];

    const documentPicker =
      new picker.DocumentViewPicker(context);

    const uris: string[] = await documentPicker.save(options);
    if (uris.length === 0) {
      return false;
    }

    const file = fileIo.openSync(
      uris[0],
      fileIo.OpenMode.READ_WRITE |
        fileIo.OpenMode.TRUNC
    );

    fileIo.writeSync(file.fd, content);
    fileIo.closeSync(file);
    return true;
  } catch (_) {
    return false;
  }
}

系统 Picker 返回 URI 后,应用只对用户选择的目标写入,不需要自己扫描整个文件系统。

TRUNC 表示如果目标已有内容则截断,避免旧文件尾部残留。写入完成后必须关闭文件句柄。

二、用户取消保存不是程序异常

用户进入文件保存器后可能直接返回。业务层应把它当作正常分支,而不是弹出严重错误。

当前封装把"没有 URI""抛出异常"和"写入失败"统一返回 false,页面显示简短状态:

typescript 复制代码
const ok: boolean = await exportTextFile(
  this.context,
  `moodmemoir-${Date.now()}.json`,
  payload
);

this.statusMessage = this.named(
  ok ? 'export_ready' : 'export_failed'
);

如果产品需要更精细的体验,可以把结果改成枚举:

typescript 复制代码
enum ExportResult {
  SUCCESS,
  CANCELLED,
  FAILED
}

这样取消时可以不提示,真正写入失败时再给出重试建议。

三、完整 JSON 要包含格式版本

备份文件不是简单地把页面上看到的文字拼起来,而是一个未来可能需要恢复的数据协议。

typescript 复制代码
const payload: string = JSON.stringify({
  exportVersion: 1,
  exportedAt: new Date().toISOString(),
  moodEntries: this.moodEntries,
  habits: this.habits,
  habitCompletions: this.completions
}, null, 2);

其中两个字段很重要:

  • exportVersion:未来导入或迁移时判断结构;
  • exportedAt:告诉用户备份生成时间。

JSON.stringify(..., null, 2) 使用缩进输出,文件略大一点,但便于用户检查,也方便问题排查。

设置项是否导出需要根据隐私和恢复需求决定。当前项目导出用户记录,不包含认证凭据,也不会导出任何系统生物特征信息。

四、CSV 的难点不是 join,而是转义

最简单的 CSV 代码经常这样写:

typescript 复制代码
rows.push([date, mood, note, tags].join(','));

一旦日记中出现逗号、换行或双引号,列就会错位。

项目统一把字段放进双引号,并把内部双引号替换成两个双引号:

typescript 复制代码
export function escapeCsv(value: string): string {
  return `"${value.replace(/"/g, '""')}"`;
}

生成内容:

typescript 复制代码
const rows: string[] = ['date,mood,note,tags'];

this.moodEntries
  .slice()
  .reverse()
  .forEach((entry: MoodEntry) => {
    rows.push([
      escapeCsv(entry.dayIdentifier),
      escapeCsv(entry.mood),
      escapeCsv(entry.note),
      escapeCsv(entry.tags.join(','))
    ].join(','));
  });

例如原文:

text 复制代码
今天说了"你好",心情不错

会变成:

text 复制代码
"今天说了""你好"",心情不错"

表格软件才能正确识别为同一个字段。

五、为什么在 CSV 前增加 UTF-8 BOM

一些桌面表格软件打开没有 BOM 的 UTF-8 CSV 时,可能错误猜测编码,导致中文乱码。

项目在内容前增加:

typescript 复制代码
`\uFEFF${rows.join('\n')}`

完整调用:

typescript 复制代码
await exportTextFile(
  this.context,
  `moodmemoir-moods-${Date.now()}.csv`,
  `\uFEFF${rows.join('\n')}`
);

BOM 并不是所有 CSV 消费方都必需,但如果目标用户会直接用常见表格软件打开,它通常能减少中文编码问题。

六、文件名要可识别,也要避免冲突

当前使用时间戳:

typescript 复制代码
`moodmemoir-${Date.now()}.json`

优点是简单且不容易重名。若更重视可读性,可以使用本地日期:

text 复制代码
moodmemoir-backup-2026-08-14.json

如果一天可能导出多次,再附加时分秒。文件名不要包含不同文件系统不支持的字符。

七、导出前后要验证什么

建议至少覆盖下面这些测试:

  1. 用户正常选择目录并保存;
  2. 在 Picker 中取消;
  3. 目标文件已存在;
  4. 日记包含英文逗号;
  5. 日记包含双引号;
  6. 日记包含换行;
  7. 日记包含中文、英文、日文和 Emoji;
  8. 空数据导出;
  9. JSON 可以重新解析;
  10. CSV 在不同表格软件中列数正确;
  11. 写入失败时没有残留未关闭句柄;
  12. 真机上的 URI 写入行为与模拟器一致。

还可以在自动测试中构造特殊文本,验证 escapeCsv()

typescript 复制代码
escapeCsv('a,"b"')
// 期望:"a,""b"""

八、导出不等于备份恢复

提供 JSON 导出后,用户自然会期待未来可以导入。因此产品文案需要准确:

  • 如果暂时只有导出,称为"导出完整 JSON"更合适;
  • 如果称为"备份",最好同时具备经过验证的恢复能力;
  • 导入前必须校验版本、字段和引用关系;
  • 不能导入失败后覆盖当前数据。

清晰的命名能避免用户把"可查看的数据文件"误解成"保证可恢复的完整备份"。

总结

HarmonyOS 文本文件导出的完整链路包括:

  1. 业务层构造结构化内容;
  2. 系统 DocumentViewPicker 让用户选择位置;
  3. 使用返回 URI 打开并写入文件;
  4. 正确关闭句柄;
  5. 区分成功、取消和失败;
  6. 对 JSON 做版本化,对 CSV 做转义和编码兼容。

真正体现"数据属于用户"的,不只是不上传,也包括用户随时能把自己的记录带走。

本文案例来自"心晴手记"HarmonyOS 版的数据导出功能。

相关推荐
HarmonyOS_SDK2 小时前
基于增强QUIC协议优化弱网下的直播观看体验
harmonyos
安好说AI2 小时前
Flutter 三方库 adaptive_image_picker 的鸿蒙化适配指南:零权限选图、纯 Dart 裁剪与目标大小压缩
flutter·harmonyos
昇腾知识体系3 小时前
msprobe/msdebug 全家桶:昇腾精度比对、溢出检测、msSanitizer 内存检测与 msOpProf 算子调优实战
人工智能·华为·知识图谱
2501_919749034 小时前
华为鸿蒙免费PDF工具—小羊免费PDF
华为·pdf·harmonyos·鸿蒙
贾伟康6 小时前
【口算王|10】HarmonyOS ArkTS 语音报题实战:协调朗读、作答和页面生命周期
生命周期·harmonyos·arkts·语音交互·texttospeech
她说..6 小时前
MySQL 与 Java 的 JSON 数据处理
java·mysql·json·springboot
贾伟康7 小时前
【口算王|08】HarmonyOS ArkTS 分类训练实战:让年级与运算分类参数保持一致
harmonyos·arkts·数据建模·路由参数·分类训练
小飞象—木兮7 小时前
经营复盘指南:核心概念、7 个核心指标、华为复盘管理标杆实践、案例·
华为
光锥智能7 小时前
华为新麒麟芯片、鸿蒙7问世,手机底层竞赛升级
华为·智能手机·harmonyos
贾伟康8 小时前
【口算王|09】HarmonyOS ArkTS 学习统计实战:计算连续训练和正确率趋势
harmonyos·arkts·数据可视化·preferences·学习统计