《鸿蒙 Electron 设备信息获取:型号、系统版本、硬件权限申请全流程》
·
我将从鸿蒙 Electron 本地文件操作的核心模块切入,先讲权限配置与路径处理基础,再详解文件读写、文件夹遍历的具体 API 用法,最后通过日志工具实战案例串联全流程,确保覆盖从权限申请到文件操作的完整实现。
<canvas_command><action>create</action><identifier>hongmeng-electron-file-operation-guide</identifier>
<title>鸿蒙 Electron 本地文件操作:读写文件、文件夹遍历、路径处理</title><genre>实战教程</genre><type>type/ddxml</type><content><title id="0">鸿蒙 Electron 本地文件操作:读写文件、文件夹遍历、路径处理</title><p id="1">本地文件操作是鸿蒙 Electron 应用的核心能力之一,无论是日志记录、配置保存还是数据导出,都离不开文件系统的交互。本文聚焦 Electron 原生 <code id="2">fs</code> 模块与鸿蒙文件系统的适配要点,从“权限配置→路径处理→文件读写→文件夹遍历”逐步展开,最后通过“本地日志记录工具”实战案例,完整实现文件操作全流程,解决鸿蒙环境下文件权限、路径兼容等核心问题。</p>
plaintext
<callout id="3" emoji="⚠️">
关键前提:鸿蒙 Electron 中文件操作依赖 Node.js <code id="4">fs</code> 模块(主进程),需提前配置存储权限;渲染进程需通过 IPC 通信调用,禁止直接操作文件系统。
</callout>
<h1 id="5">一、鸿蒙文件系统基础:权限与路径规范</h1>
<p id="6">鸿蒙系统对文件访问有严格的权限管控和路径规范,开发前需明确核心规则,避免操作失败。</p>
<h2 id="7">1.1 文件权限配置(config.json)</h2>
<p id="8">文件操作需声明存储权限,在项目根目录 <code id="9">config.json</code> 的 <code id="10">reqPermissions</code> 节点添加以下配置:</p>
<pre id="11"><code id="12" lang="json">{
"module": {
"reqPermissions": [
{
"name": "ohos.permission.READ_USER_STORAGE",
"reason": "用于读取本地配置文件和日志信息",
"usedScene": {
"ability": ["MainAbility"],
"when": "always"
}
},
{
"name": "ohos.permission.WRITE_USER_STORAGE",
"reason": "用于记录日志和保存配置数据",
"usedScene": {
"ability": ["MainAbility"],
"when": "always"
}
}
]
}
plaintext
<h2 id="13">1.2 鸿蒙文件路径规范</h2>
<p id="14">鸿蒙系统划分了不同的文件存储区域,推荐使用以下路径进行文件操作(避免访问敏感目录):</p>
<table id="15">
<thead id="16">
<tr id="17">
<th id="18">路径类型</th>
<th id="19">获取方式(Electron API)</th>
<th id="20">适用场景</th>
<th id="21">权限要求</th>
</tr>
</thead>
<tbody id="22">
<tr id="23">
<td id="24">应用数据目录</td>
<td id="25"><code id="26">app.getPath('userData')</code></td>
<td id="27">配置文件、日志文件</td>
<td id="28">默认权限(无需动态申请)</td>
</tr>
<tr id="29">
<td id="30">文档目录</td>
<td id="31"><code id="32">app.getPath('documents')</code></td>
<td id="33">用户文档、导出数据</td>
<td id="34">需 WRITE_USER_STORAGE 权限</td>
</tr>
<tr id="35">
<td id="36">临时目录</td>
<td id="37"><code id="38">app.getPath('temp')</code></td>
<td id="39">临时文件、缓存数据</td>
<td id="40">默认权限</td>
</tr>
</tbody>
</table>
<callout id="41" emoji="💡">
避坑点:鸿蒙系统禁止直接访问根目录(<code id="42">/</code>)和系统目录(<code id="43">/system</code>),优先使用 Electron 提供的路径 API 获取合法路径,避免硬编码路径导致跨设备兼容问题。
</callout>
<h1 id="44">二、核心操作:文件读写与路径处理</h1>
<p id="45">Electron 主进程通过 Node.js <code id="46">fs/promises</code> 模块(异步非阻塞)实现文件操作,结合鸿蒙路径规范,以下是核心操作的具体实现。</p>
<h2 id="47">2.1 路径处理工具函数</h2>
<p id="48">封装路径处理工具,解决鸿蒙不同设备路径差异问题:</p>
<pre id="49"><code id="50" lang="typescript">// src/main/utils/pathUtil.ts
import { app } from '@hmos/electron';
import path from 'path';
/**
* 获取应用专属日志目录
* @returns 日志目录绝对路径
*/
export function getLogDirPath(): string {
const userDataPath = app.getPath('userData');
return path.join(userDataPath, 'logs');
}
/**
* 获取指定日期的日志文件路径
* @param date 日期(默认当天)
* @returns 日志文件绝对路径
*/
export function getLogFilePath(date: Date = new Date()): string {
const logDir = getLogDirPath();
const dateStr = date.toISOString().split('T')[0]; // 格式:YYYY-MM-DD
const logFileName = `app-${dateStr}.log`;
return path.join(logDir, logFileName);
}
/**
* 确保目录存在(不存在则创建)
* @param dirPath 目录路径
*/
export async function ensureDirExists(dirPath: string): Promise<void> {
const fs = await import('fs/promises');
try {
await fs.access(dirPath);
} catch {
await fs.mkdir(dirPath, { recursive: true });
}
}
// src/main/utils/fileUtil.ts
import { ensureDirExists, getLogFilePath, getLogDirPath } from './pathUtil';
/**
* 写入日志内容到文件
* @param content 日志内容
* @param level 日志级别(info/warn/error)
*/
export async function writeLog(content: string, level: 'info' | 'warn' | 'error' = 'info'): Promise<void> {
const fs = await import('fs/promises');
await ensureDirExists(getLogDirPath());
const timestamp = new Date().toISOString();
const logContent = `[${timestamp}] [${level.toUpperCase()}] ${content}\n`;
await fs.appendFile(getLogFilePath(), logContent, 'utf8');
}
/**
* 读取指定日期的日志文件内容
* @param date 日期(默认当天)
* @returns 日志内容数组(按行分割)
*/
export async function readLogFile(date: Date = new Date()): Promise<string[]> {
const fs = await import('fs/promises');
const logFilePath = getLogFilePath(date);
try {
const content = await fs.readFile(logFilePath, 'utf8');
return content.split(/\r?\n/).filter(line => line.trim() !== '');
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
return [`日志文件不存在(${logFilePath})`];
}
throw new Error(`读取日志失败:${(error as Error).message}`);
}
}
// src/main/utils/fileUtil.ts(续)
import { getLogDirPath } from './pathUtil';
// src/main/index.ts
import { app, BrowserWindow, ipcMain } from '@hmos/electron';
import path from 'path';
import { writeLog, readLogFile, listLogFiles } from './utils/fileUtil';
import { getLogFilePath } from './utils/pathUtil';
// 创建窗口逻辑(省略)
function createWindow() { /* ... */ }
// 监听写入日志请求
ipcMain.handle('write-log', async (_, content: string, level: string) => {
await writeLog(content, level as 'info' | 'warn' | 'error');
return { success: true };
});
// 监听读取日志请求
ipcMain.handle('read-log', async (_, dateStr: string) => {
const date = new Date(dateStr);
if (isNaN(date.getTime())) {
throw new Error('无效的日期格式');
}
const content = await readLogFile(date);
return { success: true, data: content };
});
// 监听日志文件列表请求
ipcMain.handle('list-log-files', async () => {
const files = await listLogFiles();
return { success: true, data: files };
});
// 监听删除日志文件请求
ipcMain.handle('delete-log-file', async (_, filePath: string) => {
const fs = await import('fs/promises');
try {
await fs.unlink(filePath);
return { success: true };
} catch (error) {
return { success: false, message: (error as Error).message };
}
});
// 应用启动逻辑(省略)
app.whenReady().then(createWindow);
四、实战案例:本地日志记录工具
基于上述功能实现一个完整的日志工具,包含日志写入、日志查看、日志管理三大功能,使用 Vue 3 开发渲染界面。
4.1 日志写入组件(LogWriter.vue)
<template>
<div class="log-writer-card">
<h3 class="card-title">日志写入测试</h3>
<textarea
v-model="logContent"
placeholder="请输入日志内容..."
class="log-input"
></textarea>
<div class="log-level-selector">
<label>日志级别:</label>
<select v-model="selectedLevel" class="level-select">
<option value="info">Info</option>
<option value="warn">Warn</option>
<option value="error">Error</option>
</select>
</div>
<button @click="submitLog" class="submit-btn">写入日志</button>
<div class="status-msg" v-if="statusMsg" :class="statusType">
{{ statusMsg }}
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const logContent = ref('');
const selectedLevel = ref('info');
const statusMsg = ref('');
const statusType = ref('success');
async function submitLog() {
if (!logContent.value.trim()) {
statusMsg.value = '日志内容不能为空';
statusType.value = 'error';
return;
}
try {
await window.fileApi.writeLog(logContent.value, selectedLevel.value);
statusMsg.value = '日志写入成功!';
statusType.value = 'success';
logContent.value = ''; // 清空输入框
} catch (error) {
statusMsg.value = `写入失败:${(error as Error).message}`;
statusType.value = 'error';
}
// 3 秒后清空状态提示
setTimeout(() => (statusMsg.value = ''), 3000);
}
</script>
<style scoped>
.log-writer-card {
border: 1px solid #eee;
border-radius: 0.8rem;
padding: 1.5rem;
margin-bottom: 2rem;
}
.card-title {
margin: 0 0 1rem 0;
color: #007dff;
}
.log-input {
width: 100%;
height: 120px;
padding: 0.8rem;
border: 1px solid #ddd;
border-radius: 0.4rem;
resize: none;
margin-bottom: 1rem;
}
.log-level-selector {
margin-bottom: 1rem;
display: flex;
align-items: center;
gap: 0.8rem;
}
.level-select {
padding: 0.4rem;
border: 1px solid #ddd;
border-radius: 0.4rem;
}
.submit-btn {
background-color: #007dff;
color: white;
border: none;
border-radius: 0.4rem;
padding: 0.8rem 1.5rem;
cursor: pointer;
}
.submit-btn:hover {
background-color: #0066cc;
}
.status-msg {
margin-top: 1rem;
padding: 0.8rem;
border-radius: 0.4rem;
}
.success {
background-color: #e8f5e9;
color: #2e7d32;
}
.error {
background-color: #ffebee;
color: #c62828;
}
</style>
## 4.2 日志查看组件(LogViewer.vue)
```vue
<template>
<div class="log-viewer-card">
<h3 class="card-title">日志查看</h3>
<div class="date-selector">
<label>选择日期:</label>
<input type="date" v-model="selectedDate" class="date-input" />
<button @click="loadLog" class="load-btn">加载日志</button>
</div>
<div class="log-content" v-if="logContent.length">
<div
v-for="(line, index) in logContent"
:key="index"
class="log-line"
:class="getLogLevelClass(line)"
>
{{ line }}
</div>
</div>
<div class="empty-msg" v-if="logContent.length === 0 && loaded">
暂无日志数据
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const selectedDate = ref(new Date().toISOString().split('T')[0]);
const logContent = ref<string[]>([]);
const loaded = ref(false);
async function loadLog() {
try {
const result = await window.fileApi.readLog(selectedDate.value);
logContent.value = result.data || [];
loaded.value = true;
} catch (error) {
logContent.value = [`加载失败:${(error as Error).message}`];
loaded.value = true;
}
}
// 根据日志内容判断级别并返回样式类
function getLogLevelClass(line: string): string {
if (line.includes('[ERROR]')) return 'log-error';
if (line.includes('[WARN]')) return 'log-warn';
return 'log-info';
}
</script>
<style scoped>
.log-viewer-card {
border: 1px solid #eee;
border-radius: 0.8rem;
padding: 1.5rem;
margin-bottom: 2rem;
}
.date-selector {
margin-bottom: 1rem;
display: flex;
align-items: center;
gap: 0.8rem;
}
.date-input {
padding: 0.4rem;
border: 1px solid #ddd;
border-radius: 0.4rem;
}
.load-btn {
background-color: #007dff;
color: white;
border: none;
border-radius: 0.4rem;
padding: 0.4rem 1rem;
cursor: pointer;
}
.log-content {
background-color: #f9f9f9;
padding: 1rem;
border-radius: 0.4rem;
max-height: 400px;
overflow-y: auto;
}
.log-line {
padding: 0.2rem 0;
border-bottom: 1px solid #eee;
font-family: monospace;
font-size: 0.9rem;
}
.log-info {
color: #333;
}
.log-warn {
color: #f57c00;
}
.log-error {
color: #d32f2f;
}
.empty-msg {
text-align: center;
padding: 2rem;
color: #999;
}
</style>
## 4.3 日志管理组件(LogManager.vue)
```vue
<template>
<div class="log-manager-card">
<h3 class="card-title">日志文件管理</h3>
<button @click="refreshLogList" class="refresh-btn">刷新列表</button>
<table class="log-file-table" v-if="logFiles.length">
<thead>
<tr>
<th>文件名</th>
<th>大小(KB)</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="file in logFiles" :key="file.path">
<td>{{ file.name }}</td>
<td>{{ (file.size / 1024).toFixed(2) }}</td>
<td>{{ new Date(file.createTime).toLocaleString() }}</td>
<td>
<button @click="deleteLogFile(file.path)" class="delete-btn">删除</button>
</td>
</tr>
</tbody>
</table>
<div class="empty-msg" v-if="logFiles.length === 0 && loaded">暂无日志文件</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
const logFiles = ref<Array<{ name: string; path: string; size: number; createTime: number }>>([]);
const loaded = ref(false);
async function refreshLogList() {
try {
const result = await window.fileApi.listLogFiles();
logFiles.value = result.data || [];
loaded.value = true;
} catch (error) {
alert(`获取日志列表失败:${(error as Error).message}`);
}
}
async function deleteLogFile(filePath: string) {
if (!confirm('确定要删除该日志文件吗?')) return;
try {
const result = await window.fileApi.deleteLogFile(filePath);
if (result.success) {
refreshLogList();
alert('删除成功!');
} else {
alert(`删除失败:${result.message}`);
}
} catch (error) {
alert(`删除异常:${(error as Error).message}`);
}
}
onMounted(() => refreshLogList());
</script>
<style scoped>
.log-manager-card {
border: 1px solid #eee;
border-radius: 0.8rem;
padding: 1.5rem;
}
.refresh-btn {
background-color: #007dff;
color: white;
border: none;
border-radius: 0.4rem;
padding: 0.4rem 1rem;
cursor: pointer;
margin-bottom: 1rem;
}
.log-file-table {
width: 100%;
border-collapse: collapse;
}
.log-file-table th,
.log-file-table td {
border: 1px solid #ddd;
padding: 0.8rem;
text-align: left;
}
.log-file-table th {
background-color: #f5f5f5;
}
.delete-btn {
background-color: #d32f2f;
color: white;
border: none;
border-radius: 0.4rem;
padding: 0.4rem 0.8rem;
cursor: pointer;
}
.empty-msg {
text-align: center;
padding: 2rem;
color: #999;
}
</style>
## 4.4 主页面整合(LogTool.vue)
```vue
<template>
<div class="log-tool-container">
<h2 class="page-title">鸿蒙 Electron 日志工具</h2>
<LogWriter />
<LogViewer />
<LogManager />
</div>
</template>
<script setup lang="ts">
import LogWriter from './LogWriter.vue'
import LogViewer from './LogViewer.vue'
import LogManager from './LogManager.vue'
</script>
<style scoped>
.log-tool-container {
width: 90vw;
max-width: 1000px;
margin: 2rem auto;
padding: 1rem;
}
.page-title {
text-align: center;
color: #333;
margin-bottom: 2rem;
}
</style>
# 五、鸿蒙文件操作避坑指南
| 问题现象 | 根本原因 | 解决方案 |
|---------|---------|---------|
| 写入文件提示"permission denied" | 未声明 WRITE_USER_STORAGE 权限,或访问系统敏感目录 | 1. 配置 config.json 权限;2. 使用 app.getPath('userData') 等合法路径 |
| 读取文件显示乱码 | 未指定编码格式(默认 buffer) | 读取时添加编码参数:`readFile(path, 'utf8')` |
| 创建目录失败 | 未使用递归创建(父目录不存在) | 添加 `{ recursive: true }` 参数:`mkdir(path, { recursive: true })` |
| 路径拼接错误(跨设备) | 使用字符串拼接路径(如 `path + '/file'`) | 使用 path.join() 拼接路径:`path.join(dir, 'file')` |
| 大文件读取卡顿 | 一次性读取整个文件内容 | 使用流(stream)分段读取:`fs.createReadStream(path)` |
# 六、总结:鸿蒙文件操作核心要点
鸿蒙 Electron 本地文件操作需遵循"权限合规、路径规范、异步操作"三大原则:
1. **权限先行**:所有文件操作前必须在 config.json 声明对应权限,危险权限需动态申请;
2. **路径安全**:优先使用 Electron 提供的路径 API,避免硬编码路径,确保跨设备兼容;
3. **异步优先**:使用 fs/promises 异步 API,避免阻塞主进程;
更多推荐



所有评论(0)