Electron鸿蒙PC上写日志文件,我被权限和路径坑了两次

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_MEDIAohos.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 协议。转载请注明原作者及出处,商业用途请获得授权。

相关推荐
TrisighT1 天前
一个下午搞定 ArkTS 折叠面板?结果我从两点写到晚上九点
harmonyos·arkts·arkui
薛定喵的谔2 天前
Term Proxy — 用 Tauri 2 打造跨平台终端配置管理工具
electron·ai编程·全栈
逸铭2 天前
Day 5:三栏布局——左账号 / 中聊天 / 右工具
vue.js·electron
Mahut3 天前
我用 Electron + FFmpeg 做了一个本地视频处理工作站 ClipForge
前端·ffmpeg·electron
花椒技术4 天前
HJPusher / HJPlayer SDK 实践:我们为什么把直播推播链路拆成一套可复用能力
设计模式·harmonyos·直播
一维Ace4 天前
HarmonyOS ArkTS 按钮组件全解:Button、Toggle 状态交互实战
harmonyos
anyup5 天前
来简单聊聊鸿蒙开发,万元奖金的事~
前端·华为·harmonyos
逸铭5 天前
Day 2:10 分钟搭 Electron + Vite + Vue 3——AnchorChat 的第一个窗口
electron·客户端
Georgewu5 天前
【无测试机别害怕】华为云鸿蒙云手机南:从零到联调全流程详解
harmonyos