鸿蒙 Electron 本地文件操作:读写文件、文件夹遍历、路径处理

Electron 作为跨平台桌面应用开发框架,其 fs 模块是本地文件操作的核心,但在鸿蒙(HarmonyOS)系统中,由于系统权限管理机制和文件系统架构的差异,直接使用原生 fs 模块会面临权限适配、路径识别等问题。本文将聚焦鸿蒙环境下 Electron 应用的本地文件操作,从“适配原理-权限申请-核心功能实现-实战案例”全流程拆解,重点解决文件读写、文件夹遍历、路径处理等核心需求。

一、核心认知:鸿蒙与 Electron 文件系统适配要点

鸿蒙系统采用“沙箱机制+权限分级”的文件管理模式,而 Electron 基于 Chromium 内核,其 fs 模块默认适配 Windows/macOS/Linux 传统文件系统。两者适配的核心是解决“沙箱权限突破”和“路径格式兼容”两大问题。

1. 适配核心:Electron 与鸿蒙文件系统的桥梁

鸿蒙系统为第三方应用提供了 @ohos.file.fs 原生模块用于文件操作,Electron 应用无法直接调用鸿蒙原生 API,需通过以下两种方式实现适配:

  • 方式一:Electron 鸿蒙适配层(推荐):华为提供的 @huawei/electron-adapter 插件,对 Electron 原生 fs 模块进行封装,自动适配鸿蒙权限校验和路径转换,开发者无需修改原有 fs 调用逻辑;

  • 方式二:原生 API 桥接:通过 Electron 的 nativeImage 或自定义 Node.js 扩展,桥接鸿蒙 @ohos.file.fs 模块,适用于复杂场景(如文件加密、特殊格式处理)。

本文采用方式一(适配层)实现,兼顾开发效率和兼容性,适合 90% 以上的常规文件操作场景。

2. 关键差异:鸿蒙与传统系统的文件操作区别

维度

传统系统(Windows/macOS)

鸿蒙系统

适配方案

权限管理

文件操作依赖系统用户权限,Electron 可通过管理员权限突破限制

基于应用签名的权限分级,需在配置文件中声明权限

package.json 中声明鸿蒙文件权限,适配层自动校验

路径格式

支持绝对路径(如 C:/a.txt)和相对路径

推荐使用应用沙箱内路径或公共目录路径,绝对路径需转换为 URI 格式

适配层自动将传统路径转换为鸿蒙 URI(如 file:///data/storage/...)

文件访问范围

无默认限制,可访问任意目录(受用户权限约束)

默认仅可访问应用沙箱目录,访问公共目录需额外权限

声明 ohos.permission.READ_USER_STORAGE 等权限,适配层处理目录访问逻辑

二、前置操作:环境搭建与权限申请

在进行文件操作前,需完成 Electron 项目的鸿蒙适配环境搭建,并申请必要的文件操作权限,这是避免后续“权限不足”“路径无效”等问题的关键。

1. 环境搭建:安装鸿蒙适配依赖

假设已创建基础 Electron 项目(可通过 npm init electron-app@latest my-file-app 快速创建),需额外安装鸿蒙适配插件:

鸿蒙 Electron 适配层安装与配置

安装依赖

# 安装鸿蒙 Electron 适配层
npm install @huawei/electron-adapter --save

# 可选:安装路径处理工具(简化路径转换)
npm install path --save

主进程配置

在主进程入口文件(main.js)中进行如下配置:

const { app, BrowserWindow } = require('electron');
const adapter = require('@huawei/electron-adapter');
const path = require('path');

// 初始化鸿蒙适配层(必须在 app.on('ready') 前执行)
adapter.init({
  appType: 'file-operation', // 应用类型,用于权限适配
  sandbox: true              // 启用沙箱适配(默认开启)
});

let mainWindow;

function createWindow() {
  mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      nodeIntegration: true,    // 开启 Node 集成(允许渲染进程使用 fs 模块)
      contextIsolation: false   // 关闭上下文隔离(开发环境简化配置,生产环境需用 preload 桥接)
    }
  });
  
  mainWindow.loadFile('index.html');
}

app.whenReady().then(createWindow);

// 其他生命周期函数...

2. 权限申请:声明文件操作权限

鸿蒙系统要求应用在 package.json 中声明文件操作相关权限,不同操作对应的权限如下:

操作类型

所需权限

权限说明

读取应用沙箱内文件

无(默认权限)

应用私有目录,无需额外声明

读取公共目录文件(如文档、图片)

ohos.permission.READ_USER_STORAGE

用户可访问的公共存储目录读取权限

写入公共目录文件

ohos.permission.WRITE_USER_STORAGE

用户可访问的公共存储目录写入权限

遍历系统目录

ohos.permission.MANAGE_EXTERNAL_STORAGE

管理外部存储权限,需用户手动授权(仅支持系统应用或企业应用)

在项目根目录的 package.json 中添加权限声明:

{
  "name": "my-file-app",
  "version": "1.0.0",
  "main": "main.js",
  "harmony": {
    "permissions": [
      "ohos.permission.READ_USER_STORAGE",
      "ohos.permission.WRITE_USER_STORAGE"
    ],
    "abilityConfig": {
      "type": "page",
      "uri": "pages/index/index"
    }
  },
  "scripts": {
    "start": "electron .",
    "build:harmony": "electron-builder --harmony"
  }
}

**注意**:开发环境下,适配层会自动模拟权限授权;生产环境打包后,应用首次启动时会向用户申请权限,需在 UI 中提示用户授权的必要性。

三、核心功能实现:文件读写、遍历与路径处理

基于适配层和权限配置,可直接使用 Electron 原生 fs 模块语法实现文件操作,适配层会自动完成与鸿蒙系统的兼容处理。以下功能均在渲染进程(renderer.js)中实现,需确保主进程已开启 nodeIntegration

1. 路径处理:适配鸿蒙沙箱与公共目录

鸿蒙系统中,文件路径分为“应用沙箱路径”和“公共目录路径”,适配层可将传统路径转换为鸿蒙支持的格式,核心是掌握路径获取方法。

(1)获取关键路径
const fs = require('fs');
const path = require('path');
const adapter = require('@huawei/electron-adapter');

// 1. 获取应用沙箱路径(用于存储私有数据,无需额外权限)
const sandboxPath = adapter.getSandboxPath(); 
// 示例输出:/data/storage/el2/base/haps/entry/files

// 2. 获取公共目录路径(如文档目录,需要读写权限)
const docPath = adapter.getPublicPath('document'); 
// 示例输出:/data/storage/el2/base/haps/entry/documents

// 3. 构建文件路径(使用path模块确保跨平台兼容性)
const sandboxFile = path.join(sandboxPath, 'private.log'); // 沙箱内文件路径
const publicFile = path.join(docPath, 'public.log'); // 公共目录文件路径

// 4. 路径格式转换(适配层自动处理,也可手动转换)
const harmonyUri = adapter.pathToUri(publicFile); 
// 示例输出:file:///data/storage/el2/base/haps/entry/documents/public.log

(2)路径有效性校验

鸿蒙系统对路径访问有严格限制,可通过适配层校验路径是否可访问:

/**
 * 检查指定路径是否可访问
 * @param {string} targetPath - 需要检查的目标路径
 * @returns {boolean} 路径可访问时返回true
 */
function checkPathAccess(targetPath) {
    try {
        // 使用适配器提供的路径权限检查方法
        return adapter.checkPathPermission(targetPath);
    } catch (error) {
        console.error('路径检查失败:', error.message);
        return false;
    }
}

// 示例:检查公共文件路径的可访问性
const isAccessible = checkPathAccess(publicFile);
if (isAccessible) {
    console.log('路径可访问,可进行文件操作');
} else {
    console.log('路径不可访问,请检查权限或路径有效性');
}

2. 文件读写:文本/二进制文件操作

Electron 原生 fs 模块的 readFilewriteFile 等方法在适配层支持下,可直接用于鸿蒙系统的文件读写,支持同步和异步两种方式。

(1)文本文件读写(如日志文件)

/**

  • 异步写入文本文件
  • @param {string} filePath - 目标文件路径
  • @param {string} content - 待写入内容
  • @returns {Promise<boolean>} 返回写入状态 */ async function writeTextFile(filePath, content) { try { await fs.promises.writeFile(filePath, content, 'utf8'); console.log(文件写入成功:${filePath}); return true; } catch (error) { console.error('写入失败:', error.message); return false; } }

/**

  • 异步读取文本文件
  • @param {string} filePath - 目标文件路径
  • @returns {Promise<string>} 返回文件内容 */ async function readTextFile(filePath) { try { if (!fs.existsSync(filePath)) { throw new Error(文件不存在:${filePath}); } const content = await fs.promises.readFile(filePath, 'utf8'); console.log(文件读取成功:${filePath}); return content; } catch (error) { console.error('读取失败:', error.message); return ''; } }

// 使用示例:记录并读取日志 const logContent = [${new Date().toLocaleString()}] 应用启动成功\n; writeTextFile(publicFile, logContent) .then(() => readTextFile(publicFile)) .then(content => { console.log('日志内容:', content); });

(2)二进制文件读写(如图片、音频)

二进制文件读写无需指定编码,直接处理 Buffer 数据:

/**
 * 异步写入二进制文件(如图片)
 * @param {string} filePath - 目标文件路径
 * @param {Buffer} buffer - 二进制数据
 * @returns {Promise<boolean>} 写入是否成功
 */
async function writeBinaryFile(filePath, buffer) {
  try {
    await fs.promises.writeFile(filePath, buffer);
    console.log(`文件写入成功:${filePath}`);
    return true;
  } catch (error) {
    console.error(`文件写入失败:${error.message}`);
    return false;
  }
}

/**
 * 异步读取二进制文件
 * @param {string} filePath - 文件路径
 * @returns {Promise<Buffer|null>} 读取的二进制数据,失败返回null
 */
async function readBinaryFile(filePath) {
  try {
    if (!fs.existsSync(filePath)) {
      throw new Error(`文件不存在:${filePath}`);
    }
    const buffer = await fs.promises.readFile(filePath);
    console.log(`文件读取成功,大小:${buffer.length}字节`);
    return buffer;
  } catch (error) {
    console.error(`文件读取失败:${error.message}`);
    return null;
  }
}

// 示例:读取本地图片并复制到目标目录
const localImagePath = path.join(__dirname, 'assets/logo.png');
readBinaryFile(localImagePath).then(buffer => {
  if (buffer) {
    const targetImagePath = path.join(docPath, 'logo_copy.png');
    writeBinaryFile(targetImagePath, buffer);
  }
});

3. 文件夹遍历:获取目录下所有文件/子目录

使用 fs.readdir 方法遍历目录,结合 fs.stat 区分文件和子目录,实现递归遍历功能。

/**
 * 递归遍历目录并获取所有文件信息
 * @param {string} dirPath - 目标目录路径
 * @returns {Promise<Array>} 包含文件信息的数组
 */
async function traverseDir(dirPath) {
    const fileList = [];
    
    try {
        // 验证目录是否存在
        if (!fs.existsSync(dirPath)) {
            throw new Error(`目录不存在:${dirPath}`);
        }

        // 读取目录内容
        const entries = await fs.promises.readdir(dirPath);
        
        for (const entry of entries) {
            const entryPath = path.join(dirPath, entry);
            const stat = await fs.promises.stat(entryPath);

            if (stat.isFile()) {
                // 处理文件
                fileList.push({
                    type: 'file',
                    path: entryPath,
                    name: entry,
                    size: stat.size,
                    mtime: stat.mtime.toLocaleString()
                });
            } else if (stat.isDirectory()) {
                // 处理子目录
                const subDirFiles = await traverseDir(entryPath);
                fileList.push({
                    type: 'dir',
                    path: entryPath,
                    name: entry,
                    children: subDirFiles
                });
            }
        }
        
        return fileList;
    } catch (error) {
        console.error('目录遍历失败:', error.message);
        return [];
    }
}

// 使用示例
traverseDir(docPath).then(fileList => {
    console.log('目录遍历结果:', JSON.stringify(fileList, null, 2));
});

**注意**:遍历公共目录时需确保已获取 READ_USER_STORAGE 权限,遍历系统目录需 MANAGE_EXTERNAL_STORAGE 权限,否则会返回空列表或报错。

四、实战案例:本地日志记录工具开发

结合上述核心功能,开发一个鸿蒙 Electron 环境下的本地日志记录工具,实现“日志写入、日志读取、日志目录遍历、日志清理”完整功能,贴近实际开发需求。

1. 功能设计与架构

  • 核心功能:按日期生成日志文件、写入不同级别日志(info/warn/error)、读取指定日期日志、遍历所有日志文件、清理7天前日志;

  • 架构分层:UI 层(日志操作界面)、服务层(日志核心逻辑)、工具层(文件操作工具);

  • 日志存储路径:应用沙箱下的 logs 目录(无需额外权限,适合存储应用私有日志)。

2. 工具层:封装日志文件操作工具

创建 utils/logFileUtil.js,封装日志读写、遍历、清理方法:

const fs = require('fs');
const path = require('path');
const adapter = require('@huawei/electron-adapter');

// 日志存储目录(应用沙箱下的logs目录)
const logDir = path.join(adapter.getSandboxPath(), 'logs');

// 确保日志目录存在
function ensureLogDir() {
  if (!fs.existsSync(logDir)) {
    fs.mkdirSync(logDir, { recursive: true });
    console.log(`日志目录已创建:${logDir}`);
  }
}

// 初始化日志目录
ensureLogDir();

/**
 * 获取当日日志文件路径
 * @returns {string} 日志文件路径
 */
function getTodayLogPath() {
  const date = new Date().toISOString().split('T')[0]; // 格式:YYYY-MM-DD
  return path.join(logDir, `${date}.log`);
}

/**
 * 写入日志(支持不同级别)
 * @param {string} level - 日志级别(info/warn/error)
 * @param {string} message - 日志内容
 */
async function writeLog(level, message) {
  try {
    const logPath = getTodayLogPath();
    const logContent = `[${new Date().toLocaleString()}] [${level.toUpperCase()}] ${message}\n`;
    await fs.promises.appendFile(logPath, logContent, 'utf8');
    return true;
  } catch (error) {
    console.error('日志写入失败:', error.message);
    return false;
  }
}

/**
 * 读取指定日期的日志
 * @param {string} date - 日期(格式:YYYY-MM-DD)
 * @returns {Promise<string>} 日志内容
 */
async function readLogByDate(date) {
  try {
    const logPath = path.join(logDir, `${date}.log`);
    if (!fs.existsSync(logPath)) {
      return `无 ${date} 的日志记录`;
    }
    return await fs.promises.readFile(logPath, 'utf8');
  } catch (error) {
    console.error('日志读取失败:', error.message);
    return `日志读取失败:${error.message}`;
  }
}

/**
 * 遍历所有日志文件
 * @returns {Promise<Array>} 日志文件列表
 */
async function listAllLogs() {
  return await traverseDir(logDir);
}

/**
 * 清理7天前的日志
 * @returns {Promise<string>} 清理结果
 */
async function cleanOldLogs() {
  try {
    const sevenDaysAgo = new Date();
    sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
    const entries = await fs.promises.readdir(logDir);
    let deletedCount = 0;

    for (const entry of entries) {
      const entryPath = path.join(logDir, entry);
      const stat = await fs.promises.stat(entryPath);

      if (stat.isFile() && entry.endsWith('.log')) {
        const logDate = entry.split('.')[0];
        const logDateObj = new Date(logDate);

        if (logDateObj < sevenDaysAgo) {
          await fs.promises.unlink(entryPath);
          deletedCount++;
        }
      }
    }
    return `清理完成,共删除 ${deletedCount} 个旧日志文件`;
  } catch (error) {
    console.error('日志清理失败:', error.message);
    return `日志清理失败:${error.message}`;
  }
}

module.exports = {
  writeLog,
  readLogByDate,
  listAllLogs,
  cleanOldLogs
};

3. UI 层:实现日志操作界面

创建 index.html 作为界面,提供日志写入、读取、清理的交互入口:


<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>鸿蒙 Electron 日志工具</title>
    <style>
        .container {
            width: 800px;
            margin: 20px auto;
            padding: 20px;
            border: 1px solid #eee;
        }
        .operation {
            margin: 10px 0;
            padding: 10px;
            border-bottom: 1px dashed #eee;
        }
        button {
            padding: 8px 16px;
            margin-right: 10px;
            cursor: pointer;
        }
        textarea {
            width: 100%;
            height: 150px;
            margin-top: 10px;
            padding: 10px;
        }
        .log-list {
            max-height: 200px;
            overflow-y: auto;
            margin-top: 10px;
            border: 1px solid #eee;
            padding: 10px;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>本地日志记录工具</h1>
        
        <div class="operation">
            <h3>1. 写入日志</h3>
            <input type="text" id="logMessage" placeholder="请输入日志内容" style="width: 500px; padding: 8px;">
            <button onclick="writeInfoLog()">Info 日志</button>
            <button onclick="writeWarnLog()">Warn 日志</button>
            <button onclick="writeErrorLog()">Error 日志</button>
            <div id="writeResult" style="margin-top: 10px; color: green;"></div>
        </div>

        <div class="operation">
            <h3>2. 读取日志</h3>
            <input type="text" id="logDate" placeholder="请输入日期(格式:YYYY-MM-DD)" style="width: 200px; padding: 8px;">
            <button onclick="readLog()">读取日志</button>
            <textarea id="logContent" placeholder="日志内容将显示在这里"></textarea>
        </div>

        <div class="operation">
            <h3>3. 日志管理</h3>
            <button onclick="listAllLogs()">查看所有日志文件</button>
            <button onclick="cleanOldLogs()">清理7天前日志</button>
            <div id="manageResult" style="margin-top: 10px; color: green;"></div>
            <div class="log-list" id="logFileList"></div>
        </div>
    </div>

    <script src="renderer.js"></script>
</body>
</html>

4. 渲染层:绑定界面与工具方法

创建 renderer.js,实现界面交互逻辑与工具方法的绑定:

const { writeLog, readLogByDate, listAllLogs, cleanOldLogs } = require('./utils/logFileUtil');

// 写入 Info 日志
async function writeInfoLog() {
  const message = document.getElementById('logMessage').value;
  if (!message) {
    showWriteResult('请输入日志内容', 'red');
    return;
  }
  const success = await writeLog('info', message);
  showWriteResult(success ? 'Info 日志写入成功' : 'Info 日志写入失败', success ? 'green' : 'red');
  document.getElementById('logMessage').value = '';
}

// 写入 Warn 日志
async function writeWarnLog() {
  const message = document.getElementById('logMessage').value;
  if (!message) {
    showWriteResult('请输入日志内容', 'red');
    return;
  }
  const success = await writeLog('warn', message);
  showWriteResult(success ? 'Warn 日志写入成功' : 'Warn 日志写入失败', success ? 'green' : 'red');
  document.getElementById('logMessage').value = '';
}

// 写入 Error 日志
async function writeErrorLog() {
  const message = document.getElementById('logMessage').value;
  if (!message) {
    showWriteResult('请输入日志内容', 'red');
    return;
  }
  const success = await writeLog('error', message);
  showWriteResult(success ? 'Error 日志写入成功' : 'Error 日志写入失败', success ? 'green' : 'red');
  document.getElementById('logMessage').value = '';
}

// 读取指定日期日志
async function readLog() {
  const date = document.getElementById('logDate').value;
  if (!date) {
    document.getElementById('logContent').value = '请输入日期';
    return;
  }
  const content = await readLogByDate(date);
  document.getElementById('logContent').value = content;
}

// 查看所有日志文件
async function listAllLogs() {
  const fileList = await listAllLogs();
  let html = '<h4>日志文件列表:</h4>';
  fileList.forEach(item => {
    if (item.type === 'file') {
      html += `<p>文件:${item.name}(大小:${formatSize(item.size)},修改时间:${item.mtime})</p>`;
    }
  });
  document.getElementById('logFileList').innerHTML = html;
  showManageResult('日志文件列表加载成功');
}

// 清理7天前日志
async function cleanOldLogs() {
  const result = await cleanOldLogs();
  showManageResult(result);
  listAllLogs(); // 重新加载日志列表
}

// 显示写入结果
function showWriteResult(message, color) {
  const el = document.getElementById('writeResult');
  el.textContent = message;
  el.style.color = color;
  setTimeout(() => el.textContent = '', 3000);
}

// 显示管理结果
function showManageResult(message) {
  const el = document.getElementById('manageResult');
  el.textContent = message;
  setTimeout(() => el.textContent = '', 3000);
}

// 格式化文件大小
function formatSize(size) {
  if (size < 1024) return `${size} B`;
  if (size < 1024 * 1024) return `${(size / 1024).toFixed(2)} KB`;
  return `${(size / (1024 * 1024)).toFixed(2)} MB`;
}

5. 运行与测试

  1. 执行 npm start 启动应用,适配层会自动初始化并申请必要权限;

  2. 在界面输入日志内容,点击不同级别按钮写入日志,可在应用沙箱的 logs 目录下查看生成的日志文件;

  3. 输入日期读取对应日志,点击“查看所有日志文件”可遍历日志目录,点击“清理7天前日志”可删除旧日志。

五、常见问题与解决方案

1. 问题:文件写入失败,提示“权限不足”

**原因**:未在 package.json 中声明对应权限,或用户未授权。

**解决方案**:

  • 检查 package.jsonharmony.permissions 节点,确保已添加 READ_USER_STORAGEWRITE_USER_STORAGE

  • 生产环境下,在应用启动时通过 UI 提示用户授权,可调用适配层的 adapter.requestPermission 方法主动申请权限。

2. 问题:路径转换失败,提示“无效的 URI”

**原因**:使用了鸿蒙不支持的绝对路径(如 Windows 下的 C 盘路径),未通过适配层转换。

**解决方案**:

  • 优先使用 adapter.getSandboxPath()adapter.getPublicPath() 获取路径;

  • 自定义路径需通过 adapter.pathToUri() 转换为鸿蒙支持的 URI 格式。

3. 问题:遍历目录时返回空列表

**原因**:目录不存在、权限不足,或目录下无文件。

**解决方案**:

  • 调用遍历方法前,通过 fs.existsSync(dirPath) 检查目录是否存在;

  • 检查权限是否足够,公共目录需 READ_USER_STORAGE 权限;

  • 打印目录路径,通过鸿蒙开发者工具检查该目录下是否有文件。

六、总结

鸿蒙环境下的 Electron 本地文件操作,核心是通过 @huawei/electron-adapter 适配层解决权限和路径兼容问题,开发者可复用 Electron 原生 fs 模块的语法,降低学习和开发成本。本文通过“环境搭建-权限申请-核心功能实现-实战案例”的流程,覆盖了文件读写、文件夹遍历、路径处理等核心需求,其中日志工具案例可直接复用或扩展到实际项目中。

后续开发中,若需处理复杂场景(如文件加密、大文件分片读写),可结合鸿蒙原生 @ohos.file.fs 模块,通过 Electron 桥接方式实现更灵活的文件操作。如需适配层的详细 API 文档或生产环境打包指南,可参考华为 HarmonyOS 开发者官网的 Electron 适配专题。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐