React Native for OpenHarmony 实战:react-native-file-logger 日志库鸿蒙化适配
React Native for OpenHarmony 实战:react-native-file-logger 日志库鸿蒙化适配
这篇记录把 react-native-file-logger 适配到开源鸿蒙的过程,代码在 HarmonyOS 7.0.0(26) 模拟器上跑过,日志文件、hilog、截图都是实测的
适配结论速览
- 适配方法:5 个 TurboModule 方法,4 个用 fileIo 实现,1 个按平台能力边界处理
- 落盘结构:
<filesDir>/logs下 active 文件加时间戳归档,按大小和跨天轮转 - 依赖处理:上游只依赖 util 这个 Node polyfill,没有额外原生依赖
- 验证方式:模拟器跑通写入、轮转、列举、删除,归档与 active 文件内容首尾相接
- 产出物:example HAP、独立适配库 HAR、中英双版使用文档
一、为什么第二个库选它
上一篇写了 pulsar 触觉反馈,这篇换一个方向,写设备侧日志
RoboDeck 是机器人调试台,遥操的时候操作员手上只有一个平板,出了问题不可能连 USB 翻 hilog,得有一个应用自己能读的日志文件,然后从应用里导出来
react-native-file-logger 正好是干这个的,它在待适配清单里排第 80 位,155 个 star,月下载 4.2 万,热度 6.82,社区还没有鸿蒙化版本
选它还有个很实际的原因,它只有 5 个 TurboModule 方法,比 pulsar 那 19 个轻得多,适合把适配流程再走一遍,验证这套方法能复用

二、先看上游怎么实现的
Android 侧是 logback,RollingFileAppender 加两种策略,dailyRolling 打开时用 SizeAndTimeBasedRollingPolicy,关掉时用 FixedWindowRollingPolicy 按大小切
文件名规则是 <prefix>-latest.log 作为当前写入文件,归档按日期或序号命名,maximumFileSize 控制单文件上限,maximumNumberOfFiles 控制总数
鸿蒙这边没有等价框架,公开 API 里没有 logback 这种东西,只能自己用 @kit.CoreFileKit 的 fileIo 做

上面这张图是模拟器里真实跑出来的,归档文件停在 65469 字节,离 64KB(65536)上限还差 67 字节。轮转是在判断出下一次写入会超限的时候触发的,所以归档不会真的越过上限;新的 active 文件从 5696 字节继续写
三、五个方法的映射
| 方法 | 鸿蒙侧实现 | 状态 |
|---|---|---|
configure(options) | 解析沙箱目录与前缀,保存轮转参数 | 已实现 |
write(level, msg) | fileIo.writeSync 追加,按大小和跨天轮转 | 已实现 |
getLogFilePaths() | fileIo.listFileSync 加 stat,按 mtime 倒序 | 已实现 |
deleteLogFiles() | 关闭 fd 后逐个 unlink | 已实现 |
sendLogFilesByEmail(options) | 无系统邮件 composer,记录 warn 后 reject | 按能力边界处理 |
四、核心实现
4.1 落盘,保持 fd 不反复开关
write 是同步方法,每条日志都 open/close 一次太浪费,这里缓存了 fd
private ensureOpen(): void {
if (this.fileFd >= 0) {
return;
}
const path: string = this.activeFilePath();
const file = fs.openSync(path, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE | fs.OpenMode.APPEND);
this.fileFd = file.fd;
this.activeDay = this.today();
try {
this.fileBytes = fs.statSync(path).size;
} catch (e) {
this.fileBytes = 0;
}
}
fileBytes 是内存里维护的字节数,每次写完累加,不用每条都 stat 一次拿文件大小
字节数用 util.TextEncoder 算,不是 msg.length,因为中文一个字符三字节,按字符数算会低估,日志里带中文的时候轮转阈值就不准了
4.2 轮转,先归档再清理
触发条件两个,一是本次写入会超过 maximumFileSize,二是 dailyRolling 打开时跨了天
private rotate(): void {
this.closeFile();
const active: string = this.activeFilePath();
const archive: string = `${this.logsDirectory}/${this.logPrefix}-${this.timeStamp()}${LOG_SUFFIX}`;
fs.renameSync(active, archive);
this.pruneArchives();
}
归档名用了 yyyy-MM-dd-HHmmssSSS,精确到毫秒,因为按大小轮转可能一秒内触发两次,只到秒会撞名
pruneArchives 按 mtime 升序排,超出 maximumNumberOfFiles 的部分从最旧的开始删
这里有个实现细节需要说明,maximumFileSize 判断用的是 fileBytes > 0 && fileBytes + bytes > max,加 fileBytes > 0 是因为单行日志如果超过上限,不加这个判断会一直轮转却写不进去,变成死循环
4.3 列举与删除
getLogFilePaths 返回绝对路径数组,按修改时间倒序,最新的在前,方便应用直接拿第一个展示
deleteLogFiles 先关 fd 再删,不然鸿蒙这边文件被打开的时候 unlink 可能失败,删完把 fileBytes 归零,下次 write 会重新建文件
4.4 邮件发送,写清楚为什么不支持
上游这个方法在 Android 是起一个 ACTION_SEND_MULTIPLE 的 Intent,带上 zip 或者多个 log 文件,交给系统邮件客户端
鸿蒙公开 API 里没有这个能力,没有接受文件附件的系统邮件 composer,只有分享面板,语义不一样
处理方式是明确不支持,并且把日志还在哪、怎么取出来一起写进日志
async sendLogFilesByEmail(options: FileLogger.SendByEmailOptions): Promise<void> {
const paths: string[] = await this.getLogFilePaths();
const message: string =
`sendLogFilesByEmail is not supported on HarmonyOS: the public API exposes no mail composer ` +
`that accepts file attachments. ${paths.length} log file(s) remain in ${this.logsDirectory}.`;
this.logger.warn(message);
// 上游在设备没有邮件客户端时也是 reject,这里对齐同样的语义:
// 调用方必须能感知发送没有发生,而不是拿到一个假的成功
throw new RNOHError({
whatHappened: message,
howCanItBeFixed: ['Read the paths returned by getLogFilePaths() and share or upload the files from the app layer'],
});
}

调用之后 hilog 里能看到这句话,日志文件数量、目录位置都对上,同时 Promise 会 reject,调用方 try/catch 能接住,日志仍然在沙箱里,用 getLogFilePaths() 取路径自己分享或者上传
这里没有选 resolve 是因为上游 Android 在没有邮件客户端的时候 startActivity 抛异常,最后走的是 promise.reject,鸿蒙侧如果 resolve 回去,调用方会以为邮件发出去了,静默丢日志
example 里这个按钮接的是 try/catch,点下去 UI 上直接显示 rejected,和 Android 上没有邮件客户端时的表现一致

五、跑起来验证
example 工程就是上一篇那套结构,RN 0.84.1 加 RNOH 0.84.3,走自动链接
cd example/harmony
RNOH_C_API_ARCH=1 ./hvigorw assembleApp --no-daemon # HAP
./hvigorw assembleHar -p module=file_logger --no-daemon # 独立 file_logger.har
HAP 第一次全量构建用了 3 分 50 秒,HAR 单独出是 7.4KB
装上去第一次起来,Files 是 0,Log 里只有一行 configured, 0 log file(s)
这是因为 example 的 useEffect 里先 configure 再 refreshPaths,列目录的时候还没有任何写入,所以看到的是空目录,configure 本身是成功的

点 Info/Warn/Error 各一次,再点两次 Burst 400,一共 800 行,64KB 阈值被突破,文件列表变成两个,一个归档一个 active

直接去设备上看文件内容,归档文件的最后一行是 burst line 335,active 文件的第一行是 burst line 336
=== 归档文件末尾 ===
2026-10-28T10:57:12.968Z [INFO] burst line 334 xxxxxxxx...
2026-10-28T10:57:12.968Z [INFO] burst line 335 xxxxxxxx...
=== latest 文件开头 ===
2026-10-28T10:57:12.968Z [INFO] burst line 336 xxxxxxxx...
2026-10-28T10:57:12.969Z [INFO] burst line 337 xxxxxxxx...
首尾相接说明轮转没有丢行,这是验证轮转实现对不对最直接的办法
删除也验过,点完 Delete 之后目录是空的,再点一次 Info,fldemo-latest.log 又被建出来,46 字节,正好一行
六、几条经验
- 按字节算而不是按字符算,中文日志按
length会低估三倍,轮转阈值全错 - 归档名要带毫秒,同秒多次轮转会撞名,rename 失败后文件就停在原地不切了
- fd 要缓存,但轮转和删除前必须关,否则 rename/unlink 的行为在鸿蒙上不可靠
- 单行超过上限要单独处理,
fileBytes > 0这个前置条件是防死循环的 - 平台没有的能力别硬凑,方法名照留、日志写清楚,比返回一个假的成功好
七、仓库与文档
| 资源 | 地址 |
|---|---|
| 适配源码仓库 | https://atomgit.com/lhwLHWjackL/react-native-file-logger |
| 社区使用文档 | CPF-RN/usage-docs(随 PR 提交) |
待办:真机复验。模拟器上文件读写、轮转、删除都已跑通,剩下的是长期运行下的稳定性观察,比如连续写入几小时、系统回收后台后的 fd 行为
踩的坑和上一篇不一样,这篇主要坑在文件系统语义上,不在 API 映射上
欢迎加入 CPF-RN 社区:https://atomgit.com/CPF-RN
本文为 AtomGit 开源鸿蒙跨平台技术征文参赛作品,所有代码与截图来自真实适配过程
更多推荐


所有评论(0)