HarmonyOS 调用系统文件保存器导出 JSON 与 CSV
为什么“能导出”很重要
本地应用经常把“不上传云端”作为卖点,但如果用户只能在应用里查看、无法导出,那么数据依然被锁在产品内部。
在“心晴手记”中,我提供了两种导出:
- 完整 JSON:包含心情、习惯和打卡,适合备份;
- 心情 CSV:适合用表格软件查看或做个人分析。
保存位置不由应用擅自决定,而是打开系统文件保存器,让用户选择目标目录和文件名。
一、把系统保存流程封装成通用函数
先引入 AbilityKit 和 CoreFileKit:
import { common } from '@kit.AbilityKit';
import { fileIo, picker } from '@kit.CoreFileKit';
通用函数接收文件名和文本内容:
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,页面显示简短状态:
const ok: boolean = await exportTextFile(
this.context,
`moodmemoir-${Date.now()}.json`,
payload
);
this.statusMessage = this.named(
ok ? 'export_ready' : 'export_failed'
);
如果产品需要更精细的体验,可以把结果改成枚举:
enum ExportResult {
SUCCESS,
CANCELLED,
FAILED
}
这样取消时可以不提示,真正写入失败时再给出重试建议。
三、完整 JSON 要包含格式版本
备份文件不是简单地把页面上看到的文字拼起来,而是一个未来可能需要恢复的数据协议。
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 代码经常这样写:
rows.push([date, mood, note, tags].join(','));
一旦日记中出现逗号、换行或双引号,列就会错位。
项目统一把字段放进双引号,并把内部双引号替换成两个双引号:
export function escapeCsv(value: string): string {
return `"${value.replace(/"/g, '""')}"`;
}
生成内容:
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(','));
});
例如原文:
今天说了“你好”,心情不错
会变成:
"今天说了""你好"",心情不错"
表格软件才能正确识别为同一个字段。
五、为什么在 CSV 前增加 UTF-8 BOM
一些桌面表格软件打开没有 BOM 的 UTF-8 CSV 时,可能错误猜测编码,导致中文乱码。
项目在内容前增加:
`\uFEFF${rows.join('\n')}`
完整调用:
await exportTextFile(
this.context,
`moodmemoir-moods-${Date.now()}.csv`,
`\uFEFF${rows.join('\n')}`
);
BOM 并不是所有 CSV 消费方都必需,但如果目标用户会直接用常见表格软件打开,它通常能减少中文编码问题。
六、文件名要可识别,也要避免冲突
当前使用时间戳:
`moodmemoir-${Date.now()}.json`
优点是简单且不容易重名。若更重视可读性,可以使用本地日期:
moodmemoir-backup-2026-08-14.json
如果一天可能导出多次,再附加时分秒。文件名不要包含不同文件系统不支持的字符。
七、导出前后要验证什么
建议至少覆盖下面这些测试:
- 用户正常选择目录并保存;
- 在 Picker 中取消;
- 目标文件已存在;
- 日记包含英文逗号;
- 日记包含双引号;
- 日记包含换行;
- 日记包含中文、英文、日文和 Emoji;
- 空数据导出;
- JSON 可以重新解析;
- CSV 在不同表格软件中列数正确;
- 写入失败时没有残留未关闭句柄;
- 真机上的 URI 写入行为与模拟器一致。
还可以在自动测试中构造特殊文本,验证 escapeCsv():
escapeCsv('a,"b"')
// 期望:"a,""b"""
八、导出不等于备份恢复
提供 JSON 导出后,用户自然会期待未来可以导入。因此产品文案需要准确:
- 如果暂时只有导出,称为“导出完整 JSON”更合适;
- 如果称为“备份”,最好同时具备经过验证的恢复能力;
- 导入前必须校验版本、字段和引用关系;
- 不能导入失败后覆盖当前数据。
清晰的命名能避免用户把“可查看的数据文件”误解成“保证可恢复的完整备份”。
总结
HarmonyOS 文本文件导出的完整链路包括:
- 业务层构造结构化内容;
- 系统
DocumentViewPicker让用户选择位置; - 使用返回 URI 打开并写入文件;
- 正确关闭句柄;
- 区分成功、取消和失败;
- 对 JSON 做版本化,对 CSV 做转义和编码兼容。
真正体现“数据属于用户”的,不只是不上传,也包括用户随时能把自己的记录带走。
本文案例来自“心晴手记”HarmonyOS 版的数据导出功能。
更多推荐



所有评论(0)