Electron鸿蒙PC上写日志文件,我被权限和路径坑了两次
上周给公司内部的 Electron 运维工具接鸿蒙 PC 适配,其中一个看似最简单的功能------把运行日志写到本地文件------居然折腾了我将近一天。Windows 上 fs.appendFileSync 一把梭的事,到了鸿蒙 PC 上连日志目录在哪都找不到。这篇文章记录一下我趟过的坑,省得后来人再踩一遍。
坑一:鸿蒙 PC 上没有"用户目录"这个概念
我在 Windows 上的日志路径是这么写的:
javascript
const path = require('path');
const os = require('os');
const logDir = path.join(os.homedir(), '.myapp', 'logs');
在 Windows 和 macOS 上跑得好好的,代码挪到鸿蒙 PC 上一执行,os.homedir() 返回的是 /,而且应用根本没有写 / 的权限。我第一次看到日志文件没生成,还以为是 fs 模块没加载成功,调试了半个多小时才发现路径有问题。
鸿蒙 PC 的应用沙箱模型和 Android 更接近,每个应用有自己的私有目录。Electron 在鸿蒙上的用户数据路径需要通过 app.getPath('userData') 获取,但这个路径在鸿蒙环境下返回的并不是你直觉上认为的那个位置。
实际可用的日志目录应该这么拿:
javascript
const { app } = require('electron');
const path = require('path');
const fs = require('fs');
function getLogDir() {
// 鸿蒙 PC 上 userData 指向应用沙箱内部
const userData = app.getPath('userData');
const logDir = path.join(userData, 'logs');
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, { recursive: true });
}
return logDir;
}
实测在鸿蒙 PC 上,app.getPath('userData') 返回的是类似 /data/app/el2/100/base/com.example.myapp/haps/entry/files/electron 这样的路径。这个目录应用有读写权限,但用户通过文件管理器是看不到的------这也是我后面要讲的第二个坑的伏笔。
坑二:沙箱外写入需要申请权限
有些场景下,日志需要放在用户能找到的位置,比如 /Documents/myapp/logs,方便用户导出反馈问题。我一开始直接写了:
javascript
const externalLogDir = '/storage/Documents/myapp/logs';
fs.mkdirSync(externalLogDir, { recursive: true });
结果抛了个 EACCES: permission denied。鸿蒙 PC 从 API 9 开始强化了存储权限管理,写外部存储需要显式申请 ohos.permission.WRITE_MEDIA 或 ohos.permission.WRITE_DOCUMENTS 权限。
Electron 应用申请鸿蒙权限不是走常规的 Android 式动态申请,而是要在 module.json5 里先声明:
json
{
"module": {
"requestPermissions": [
{
"name": "ohos.permission.WRITE_DOCUMENTS",
"reason": "$string:write_documents_reason"
}
]
}
}
然后在 Electron 主进程里,通过 libelectron 提供的桥接 API 触发权限弹窗:
javascript
const { ipcMain } = require('electron');
// 假设 libelectron 暴露了权限申请接口
async function requestStoragePermission() {
if (process.platform === 'harmonyos') {
// 通过 IPC 调用鸿蒙原生权限接口
const result = await ipcMain.invoke('harmonyos:requestPermission',
'ohos.permission.WRITE_DOCUMENTS'
);
return result.granted;
}
return true;
}
老实说,这部分文档写得比较散。我翻了 libelectron 的示例代码和鸿蒙官方文档,拼凑了三四次才跑通。权限弹窗在鸿蒙 PC 上的样式也和手机端不一样,弹出来的是桌面风格的模态框,第一次看到我差点以为应用崩溃了。
坑三:同步写入会阻塞渲染进程
解决路径和权限之后,我直接把原来的日志函数搬过来了:
javascript
function log(message) {
const logFile = path.join(getLogDir(), `app-${new Date().toISOString().split('T')[0]}.log`);
const line = `[${new Date().toISOString()}] ${message}\n`;
fs.appendFileSync(logFile, line);
}
在 Windows 上量不大的时候没什么感觉,但在鸿蒙 PC 上,这个同步写入居然会让 UI 卡顿。 Electron 的主进程和渲染进程在鸿蒙上的 IPC 性能比 Windows 差一些,主进程被 appendFileSync 阻塞时,渲染进程的鼠标事件响应明显变慢。
我一开始怀疑是鸿蒙的文件系统性能问题,换了个方式测了一下------改成异步写入后卡顿立刻消失。说到底,appendFileSync 在任何平台上都不是好习惯,只是鸿蒙 PC 上惩罚来得更明显。
正确的做法是用流或者异步追加:
javascript
const fs = require('fs');
const path = require('path');
class AsyncLogger {
constructor(logDir) {
this.logDir = logDir;
this.stream = null;
this.currentDate = '';
this.ensureStream();
}
ensureStream() {
const today = new Date().toISOString().split('T')[0];
if (this.currentDate !== today) {
if (this.stream) {
this.stream.end();
}
this.currentDate = today;
const logFile = path.join(this.logDir, `app-${today}.log`);
this.stream = fs.createWriteStream(logFile, { flags: 'a' });
}
}
write(message) {
this.ensureStream();
const line = `[${new Date().toISOString()}] ${message}\n`;
this.stream.write(line);
}
close() {
if (this.stream) {
this.stream.end();
}
}
}
const logger = new AsyncLogger(getLogDir());
// 使用
logger.write('应用启动完成');
logger.write('检测到鸿蒙PC环境,切换日志路径');
这样写还有个好处:按天自动切分日志文件,避免单文件过大。我后面又给这个类加了日志级别和最大保留天数,算是一个能直接拿去用的基础版本。
坑四:应用更新后 userData 路径会变
这是最难发现的一个坑,我直到做应用更新测试时才撞上。
鸿蒙 PC 上每个版本的 Electron 应用,如果 bundleName 没变但 versionCode 升级了,app.getPath('userData') 返回的路径在不同版本间是连续的,这没问题。但如果你在开发阶段频繁卸载重装,或者改了 module.json5 里的 bundleName,userData 目录就会变成全新的,之前的日志全丢了。
更坑的是,鸿蒙的卸载逻辑默认会清理应用沙箱目录。用户卸载再重装应用,之前存的日志和配置会全部消失。Windows 上卸载软件时用户数据通常还留在 AppData 里,这个习惯在鸿蒙上完全不适用。
我的 workaround 是在应用里加了一个导出功能,允许用户把日志打包到 Documents 目录。同时在更新流程里,如果检测到旧版本的日志目录存在,做一次迁移:
javascript
const { app } = require('electron');
const fs = require('fs');
const path = require('path');
function migrateLegacyLogs() {
const userData = app.getPath('userData');
// 旧版本可能的残留路径(根据实际 bundleName 调整)
const legacyPaths = [
'/data/app/el2/100/base/com.example.myapp.old/haps/entry/files/electron/logs'
];
const currentLogDir = path.join(userData, 'logs');
for (const legacy of legacyPaths) {
if (fs.existsSync(legacy) && legacy !== currentLogDir) {
const files = fs.readdirSync(legacy);
for (const file of files) {
const src = path.join(legacy, file);
const dest = path.join(currentLogDir, file);
if (!fs.existsSync(dest)) {
fs.copyFileSync(src, dest);
}
}
// 迁移完留个标记,避免重复执行
fs.writeFileSync(path.join(currentLogDir, '.migrated'), Date.now().toString());
console.log('日志已从旧路径迁移');
}
}
}
这个方案不算完美,但至少能保证用户在正常升级流程里不会丢失日志。真正要解决这个问题,可能需要鸿蒙应用市场提供保留用户数据的更新机制------据我所知目前还没这功能。
完整可用的日志模块
把我上面提到的方案整合了一下,写了一个可以直接复制到项目里用的日志模块:
javascript
// logger.js
const fs = require('fs');
const path = require('path');
const { app } = require('electron');
class HarmonyLogger {
constructor(options = {}) {
this.logDir = options.logDir || this.getDefaultLogDir();
this.maxDays = options.maxDays || 7;
this.stream = null;
this.currentDate = '';
if (!fs.existsSync(this.logDir)) {
fs.mkdirSync(this.logDir, { recursive: true });
}
this.ensureStream();
this.cleanupOldLogs();
}
getDefaultLogDir() {
if (process.platform === 'harmonyos') {
return path.join(app.getPath('userData'), 'logs');
}
return path.join(require('os').homedir(), '.myapp', 'logs');
}
ensureStream() {
const today = new Date().toISOString().split('T')[0];
if (this.currentDate !== today) {
if (this.stream) this.stream.end();
this.currentDate = today;
const logFile = path.join(this.logDir, `app-${today}.log`);
this.stream = fs.createWriteStream(logFile, { flags: 'a' });
}
}
log(level, message) {
this.ensureStream();
const timestamp = new Date().toISOString();
const line = `[${timestamp}] [${level}] ${message}\n`;
this.stream.write(line);
}
info(message) { this.log('INFO', message); }
warn(message) { this.log('WARN', message); }
error(message) { this.log('ERROR', message); }
cleanupOldLogs() {
const files = fs.readdirSync(this.logDir);
const cutoff = Date.now() - (this.maxDays * 24 * 60 * 60 * 1000);
for (const file of files) {
if (!file.startsWith('app-') || !file.endsWith('.log')) continue;
const filePath = path.join(this.logDir, file);
const stats = fs.statSync(filePath);
if (stats.mtimeMs < cutoff) {
fs.unlinkSync(filePath);
}
}
}
close() {
if (this.stream) this.stream.end();
}
}
module.exports = { HarmonyLogger };
用法很简单:
javascript
const { HarmonyLogger } = require('./logger');
const logger = new HarmonyLogger({ maxDays: 14 });
logger.info('应用启动');
logger.warn('检测到旧版本配置,正在迁移');
logger.error('IPC 通信超时: renderer-123');
// 应用退出时
app.on('before-quit', () => logger.close());
一点个人建议
如果你也在做 Electron 到鸿蒙 PC 的迁移,我建议把文件操作相关的代码单独抽一个适配层,别散在业务代码里。鸿蒙的文件权限模型和路径规则与桌面系统差异很大,集中管理方便后续调整。另外,尽早做卸载重装测试------很多"数据丢失"的问题不是 bug,而是沙箱机制的正常行为,提前设计好数据导出和迁移策略能省不少事。
鸿蒙 PC 的桌面生态还在快速迭代,libelectron 的 API 也在持续更新。我这篇文章基于 2026 年 5 月的版本经验,如果后面有变化,欢迎在评论区交流。
本文遵循 MIT 协议。转载请注明原作者及出处,商业用途请获得授权。