第 5 章:系统交互与文件操作
本章将带你掌握 Electron 应用与操作系统深度交互的核心能力。从文件读写、对话框操作,到系统通知、剪贴板控制——这些能力让 Web 页面真正拥有了"原生应用"的灵魂。
5.1 Node.js fs 模块在 Electron 中的使用
Electron 的设计允许渲染进程通过预加载脚本安全地访问 Node.js API。这意味着你熟悉的 fs 模块,可以通过 contextBridge 暴露给渲染进程使用——当然,这需要遵循安全最佳实践。
把主进程想象成 Node.js 服务器,渲染进程就是前端页面。在 Electron 中,前端页面通过预加载脚本暴露的安全 API 来访问文件系统等 Node.js 能力,不需要通过 HTTP API 中转。
三种文件读取方式对比
| 方式 | API | 特点 | 适用场景 |
|---|---|---|---|
| 同步 | fs.readFileSync | 阻塞执行,代码简单 | 配置文件读取、启动时初始化 |
| 回调异步 | fs.readFile | 传统 Node.js 风格 | 兼容旧代码 |
| Promise | fs.promises.readFile | 支持 async/await | 现代项目首选 |
同步方式读取文件
// 同步读取:适合小文件、配置文件
const fs = require('fs');
const path = require('path');
// 读取应用配置文件
const configPath = path.join(__dirname, 'config.json');
try {
const data = fs.readFileSync(configPath, 'utf-8');
const config = JSON.parse(data);
console.log('配置加载成功:', config);
} catch (err) {
console.error('读取失败:', err.message);
}
Promise 方式(推荐)
// 使用 fs.promises,支持 async/await
const fsp = require('fs').promises;
const path = require('path');
async function readAppData() {
const dataPath = path.join(app.getPath('userData'), 'data.json');
try {
const data = await fsp.readFile(dataPath, 'utf-8');
return JSON.parse(data);
} catch (err) {
// 文件不存在时返回默认值
if (err.code === 'ENOENT') {
return { version: 1, items: [] };
}
throw err;
}
}
async function saveAppData(data) {
const dataPath = path.join(app.getPath('userData'), 'data.json');
// 确保目录存在
await fsp.mkdir(path.dirname(dataPath), { recursive: true });
await fsp.writeFile(dataPath, JSON.stringify(data, null, 2), 'utf-8');
}
5.2 在渲染进程中使用 fs(通过 Preload 脚本暴露 API)
虽然 Electron 允许在渲染进程中直接 require('fs'),但出于安全考虑,最佳实践是通过 Preload 脚本有选择地暴露 API。这类似于前端项目中"封装 API 层"的做法。
开启
nodeIntegration: true 虽然方便,但会让渲染进程拥有完整的 Node.js 能力,存在安全风险。生产环境务必使用 contextBridge。
const { contextBridge } = require('electron');
const fs = require('fs').promises;
const path = require('path');
// 只暴露必要的文件操作 API
contextBridge.exposeInMainWorld('electronAPI', {
// 读取文件
readFile: (filePath) => fs.readFile(filePath, 'utf-8'),
// 写入文件
writeFile: (filePath, content) => fs.writeFile(filePath, content, 'utf-8'),
// 读取目录
readDir: (dirPath) => fs.readdir(dirPath, { withFileTypes: true }),
// 检查文件是否存在
fileExists: (filePath) => fs.access(filePath).then(() => true).catch(() => false),
// 获取应用数据目录
getUserDataPath: () => require('electron').app.getPath('userData'),
// 路径拼接(避免渲染进程直接使用 path)
joinPath: (...segments) => path.join(...segments)
});
// 在渲染进程中使用暴露的 API
async function loadNotes() {
const userData = await window.electronAPI.getUserDataPath();
const notesPath = await window.electronAPI.joinPath(userData, 'notes.json');
const exists = await window.electronAPI.fileExists(notesPath);
if (exists) {
const content = await window.electronAPI.readFile(notesPath);
return JSON.parse(content);
}
return [];
}
async function saveNotes(notes) {
const userData = await window.electronAPI.getUserDataPath();
const notesPath = await window.electronAPI.joinPath(userData, 'notes.json');
await window.electronAPI.writeFile(notesPath, JSON.stringify(notes, null, 2));
}
5.3 对话框 Dialog
Electron 的 dialog 模块提供了原生系统对话框,包括文件选择、保存、消息提示等。这比浏览器自带的 <input type="file"> 强大得多。
打开文件对话框
const { dialog, BrowserWindow } = require('electron');
// 打开单个文件
async function openFile() {
const win = BrowserWindow.getFocusedWindow();
const result = await dialog.showOpenDialog(win, {
title: '选择文件',
defaultPath: app.getPath('documents'),
buttonLabel: '选择',
filters: [
{ name: '文本文件', extensions: ['txt', 'md'] },
{ name: '图片', extensions: ['jpg', 'png', 'gif'] },
{ name: '所有文件', extensions: ['*'] }
],
properties: ['openFile'] // 可多选:['openFile', 'multiSelections']
});
if (!result.canceled) {
console.log('选择的文件:', result.filePaths);
return result.filePaths[0];
}
return null;
}
保存文件对话框
// 保存文件对话框
async function saveFile(defaultName, content) {
const win = BrowserWindow.getFocusedWindow();
const result = await dialog.showSaveDialog(win, {
title: '保存文件',
defaultPath: defaultName,
filters: [
{ name: 'Markdown', extensions: ['md'] },
{ name: '文本文件', extensions: ['txt'] }
]
});
if (!result.canceled) {
await fs.promises.writeFile(result.filePath, content, 'utf-8');
return result.filePath;
}
return null;
}
消息对话框
// 信息提示
const result = await dialog.showMessageBox(win, {
type: 'info', // 'none' | 'info' | 'error' | 'question' | 'warning'
title: '保存成功',
message: '文件已保存到指定位置',
detail: '你可以在文档目录中找到它',
buttons: ['确定', '打开所在文件夹'],
defaultId: 0,
cancelId: 0
});
if (result.response === 1) {
// 用户点击"打开所在文件夹"
shell.showItemInFolder(filePath);
}
在 macOS 上,对话框默认会以"sheet"形式从窗口顶部滑下,体验更原生。Windows 上则是独立的模态窗口。
5.4 系统通知 Notification
Electron 的通知 API 与 Web Notification API 几乎一致,但会调用系统原生的通知中心,而不是浏览器自己的实现。
// 在渲染进程中发送系统通知
function sendNotification(title, body, iconPath) {
// 检查通知权限(Electron 中默认允许)
if (Notification.permission === 'granted') {
const notification = new Notification(title, {
body: body,
icon: iconPath, // 支持本地文件路径
silent: false, // 是否播放提示音
timeoutType: 'default' // 'default' | 'never'
});
notification.onclick = () => {
console.log('用户点击了通知');
// 可以在这里聚焦窗口
window.electronAPI.focusWindow();
};
notification.onclose = () => {
console.log('通知关闭');
};
}
}
// 使用示例
sendNotification(
'下载完成',
'文件 "report.pdf" 已下载到下载文件夹',
'/path/to/app-icon.png'
);
Windows 10/11 的通知需要在系统设置中开启应用通知权限。如果通知不显示,检查"设置 > 系统 > 通知和操作"中你的应用是否被允许。
5.5 剪贴板操作 Clipboard
Electron 的 clipboard 模块支持丰富的剪贴板操作,远超浏览器的 navigator.clipboard API。
| 操作 | API | 说明 |
|---|---|---|
| 读取纯文本 | clipboard.readText() | 获取剪贴板中的文本 |
| 写入纯文本 | clipboard.writeText(text) | 设置剪贴板文本 |
| 读取 HTML | clipboard.readHTML() | 获取富文本 HTML |
| 写入 HTML | clipboard.writeHTML(html) | 设置富文本 |
| 读取图片 | clipboard.readImage() | 返回 NativeImage 对象 |
| 写入图片 | clipboard.writeImage(image) | 将图片放入剪贴板 |
| 读取文件列表 | clipboard.readBuffer('FileNameW') | 获取拖拽/复制的文件路径 |
const { clipboard, nativeImage } = require('electron');
// 文本操作
clipboard.writeText('Hello from Electron!');
const text = clipboard.readText();
// HTML 富文本
clipboard.writeHTML('加粗文本 和 斜体');
// 图片操作
const image = nativeImage.createFromPath('/path/to/image.png');
clipboard.writeImage(image);
// 读取剪贴板图片并保存
const clipboardImage = clipboard.readImage();
if (!clipboardImage.isEmpty()) {
const buffer = clipboardImage.toPNG();
fs.writeFileSync('clipboard-screenshot.png', buffer);
}
5.6 执行系统命令 child_process
通过 child_process 模块,Electron 可以执行系统命令、调用外部程序,这是实现自动化和系统集成的关键能力。
const { exec, execFile, spawn } = require('child_process');
const util = require('util');
const execPromise = util.promisify(exec);
// ===== exec:执行 shell 命令(适合简单命令)=====
async function runCommand() {
try {
const { stdout, stderr } = await execPromise('ls -la', {
cwd: '/home/user',
timeout: 5000,
maxBuffer: 1024 * 1024 // 1MB 输出缓冲
});
console.log('输出:', stdout);
} catch (error) {
console.error('命令失败:', error);
}
}
// ===== execFile:执行可执行文件(更安全,不经过 shell)=====
execFile('node', ['--version'], (error, stdout) => {
if (error) return console.error(error);
console.log('Node 版本:', stdout.trim());
});
// ===== spawn:适合长时间运行、大输出、需要实时数据 ====
function spawnProcess() {
const child = spawn('ping', ['-c', '4', 'baidu.com']);
child.stdout.on('data', (data) => {
console.log(`输出: ${data}`);
});
child.stderr.on('data', (data) => {
console.error(`错误: ${data}`);
});
child.on('close', (code) => {
console.log(`进程退出,代码: ${code}`);
});
// 5秒后终止进程
setTimeout(() => child.kill('SIGTERM'), 5000);
}
永远不要将用户输入直接拼接到 exec 命令中!这会导致命令注入漏洞。优先使用 execFile 或 spawn,并传递参数数组。
5.7 文件拖拽支持(Vue 组件实现)
Electron 应用可以接收从系统拖拽进来的文件,这在传统 Web 应用中受限(浏览器安全策略)。在 Electron 中,你可以轻松实现拖拽打开文件的功能。
<template>
<div
class="drop-zone"
:class="{ 'drag-over': isDragOver }"
@dragenter.prevent="onDragEnter"
@dragover.prevent="onDragOver"
@dragleave="onDragLeave"
@drop.prevent="onDrop"
>
<div v-if="!files.length" class="placeholder">
<i class="icon-upload"></i>
<p>拖拽文件到此处,或 <span @click="selectFile">点击选择</span></p>
</div>
<div v-else class="file-list">
<div v-for="file in files" :key="file.path" class="file-item">
<img v-if="isImage(file)" :src="file.path" class="preview">
<div class="file-info">
<span class="file-name">{{ file.name }}</span>
<span class="file-size">{{ formatSize(file.size) }}</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
const isDragOver = ref(false);
const files = ref([]);
function onDragEnter() {
isDragOver.value = true;
}
function onDragOver() {
isDragOver.value = true;
}
function onDragLeave() {
isDragOver.value = false;
}
function onDrop(event) {
isDragOver.value = false;
// Electron 中 dataTransfer.files 包含完整路径
const droppedFiles = Array.from(event.dataTransfer.files);
files.value = droppedFiles.map(file => ({
name: file.name,
path: file.path, // Electron 特有:包含完整系统路径
size: file.size,
type: file.type
}));
// 读取文件内容
readFiles(files.value);
}
async function readFiles(fileList) {
for (const file of fileList) {
try {
const content = await window.electronAPI.readFile(file.path);
console.log(`文件 ${file.name} 内容:`, content.substring(0, 200));
} catch (err) {
console.error(`读取 ${file.name} 失败:`, err);
}
}
}
function isImage(file) {
return file.type.startsWith('image/');
}
function formatSize(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
</script>
<style scoped>
.drop-zone {
border: 2px dashed #ccc;
border-radius: 8px;
padding: 40px;
text-align: center;
transition: all 0.3s;
}
.drop-zone.drag-over {
border-color: #409eff;
background: #f0f9ff;
}
.file-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
border-bottom: 1px solid #eee;
}
.preview {
width: 60px;
height: 60px;
object-fit: cover;
border-radius: 4px;
}
</style>
在浏览器中,
dataTransfer.files 只有文件名和类型,没有路径。在 Electron 中,file.path 直接就是完整的系统路径,可以直接用于 fs 操作。
5.8 跨平台路径处理(path 模块)
Windows 使用反斜杠 \,macOS/Linux 使用正斜杠 /。Node.js 的 path 模块让你无需关心这些差异。
const path = require('path');
const { app } = require('electron');
// ===== 路径拼接(自动处理分隔符)=====
const configPath = path.join(__dirname, 'config', 'app.json');
// Windows: C:\project\config\app.json
// macOS: /project/config/app.json
// ===== 获取标准目录 =====
const paths = {
home: app.getPath('home'), // 用户主目录
documents: app.getPath('documents'), // 文档目录
downloads: app.getPath('downloads'), // 下载目录
userData: app.getPath('userData'), // 应用数据目录(推荐存储配置)
temp: app.getPath('temp'), // 临时目录
exe: app.getPath('exe'), // 当前可执行文件路径
logs: app.getPath('logs') // 日志目录
};
// ===== 路径解析 =====
path.basename('/foo/bar/baz.txt'); // 'baz.txt'
path.dirname('/foo/bar/baz.txt'); // '/foo/bar'
path.extname('/foo/bar/baz.txt'); // '.txt'
path.parse('/foo/bar/baz.txt');
// { root: '/', dir: '/foo/bar', base: 'baz.txt', ext: '.txt', name: 'baz' }
// ===== 路径规范化 =====
path.normalize('/foo/bar//baz/asdf/quux/..'); // '/foo/bar/baz/asdf'
path.resolve('foo/bar', '/tmp/file/', '..', 'a/../subfile');
// 相当于 cd 命令,返回绝对路径
应用配置和数据永远存放在
app.getPath('userData'),而不是应用安装目录。这样即使用户更新或重装应用,数据也不会丢失。
5.9 综合示例:文件管理器
让我们把本章所学整合成一个简单的文件管理器功能:
const fs = require('fs').promises;
const path = require('path');
const { dialog, shell } = require('electron');
class FileManager {
constructor() {
this.currentPath = '';
}
// 读取目录内容
async readDirectory(dirPath) {
this.currentPath = dirPath;
const entries = await fs.readdir(dirPath, { withFileTypes: true });
return entries.map(entry => ({
name: entry.name,
path: path.join(dirPath, entry.name),
isDirectory: entry.isDirectory(),
isFile: entry.isFile(),
size: entry.isFile() ? (await fs.stat(path.join(dirPath, entry.name))).size : 0,
modified: (await fs.stat(path.join(dirPath, entry.name))).mtime
}));
}
// 打开文件(使用系统默认程序)
async openFile(filePath) {
await shell.openPath(filePath);
}
// 在文件夹中显示
async showInFolder(filePath) {
shell.showItemInFolder(filePath);
}
// 删除文件(移动到回收站)
async trashFile(filePath) {
await shell.trashItem(filePath);
}
// 创建新文件夹
async createFolder(parentPath, name) {
const newPath = path.join(parentPath, name);
await fs.mkdir(newPath, { recursive: true });
return newPath;
}
// 复制文件
async copyFile(src, dest) {
await fs.copyFile(src, dest);
}
}
module.exports = { FileManager };
本章小结
本章涵盖了 Electron 系统交互的核心能力:
- fs 模块:同步/异步/Promise 三种文件操作方式
- Preload 脚本:安全地在渲染进程暴露 Node.js API
- Dialog:打开、保存、消息三种原生对话框
- Notification:系统级通知,跨平台一致体验
- Clipboard:文本、HTML、图片的读写操作
- child_process:执行系统命令,注意安全防护
- 拖拽:接收系统文件拖拽,获取完整路径
- path:跨平台路径处理,使用标准目录
掌握这些能力后,你的 Electron 应用已经具备了与操作系统深度集成的能力。下一章,我们将探索更强大的屏幕控制与自动化功能。