《鸿蒙 Electron 本地文件操作:读写文件、文件夹遍历、路径处理》
本文探讨了在鸿蒙(HarmonyOS)系统中使用Electron进行本地文件操作的关键技术。针对鸿蒙特有的沙箱机制和权限管理体系,重点介绍了通过@huawei/electron-adapter适配层实现文件读写、路径转换和目录遍历的方法。文章包含环境配置、权限申请、核心功能实现及日志工具实战案例,解决了Electron在鸿蒙系统中的文件系统兼容性问题。开发者可复用传统Electron的fs模块语法
鸿蒙 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 可通过管理员权限突破限制 |
基于应用签名的权限分级,需在配置文件中声明权限 |
在 |
|
路径格式 |
支持绝对路径(如 C:/a.txt)和相对路径 |
推荐使用应用沙箱内路径或公共目录路径,绝对路径需转换为 URI 格式 |
适配层自动将传统路径转换为鸿蒙 URI(如 file:///data/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 模块的 readFile、writeFile 等方法在适配层支持下,可直接用于鸿蒙系统的文件读写,支持同步和异步两种方式。
(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. 运行与测试
-
执行
npm start启动应用,适配层会自动初始化并申请必要权限; -
在界面输入日志内容,点击不同级别按钮写入日志,可在应用沙箱的 logs 目录下查看生成的日志文件;
-
输入日期读取对应日志,点击“查看所有日志文件”可遍历日志目录,点击“清理7天前日志”可删除旧日志。
五、常见问题与解决方案
1. 问题:文件写入失败,提示“权限不足”
**原因**:未在 package.json 中声明对应权限,或用户未授权。
**解决方案**:
-
检查
package.json的harmony.permissions节点,确保已添加READ_USER_STORAGE或WRITE_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 适配专题。
更多推荐


所有评论(0)