桌面应用与 Web 应用最大的区别之一,就是可以自由地访问用户的文件系统。在 Electron 中,这一能力主要来自 Node.js 的 fs 模块。它让开发者能够在自己熟悉的 JavaScript 环境里完成文件的读取、写入、删除、遍历等操作,而无需学习任何平台相关的 API。
12.1.1 fs 模块的定位与使用场景
fs 是 Node.js 的内置模块,提供了完整的文件系统操作接口。在 Electron 中,它通常运行在主进程中(也可以通过预加载脚本安全地暴露给渲染进程)。你可以用它实现各种桌面端刚需功能:
- 读写本地配置文件(JSON、YAML、INI 等),保存用户偏好设置。
- 实现数据的本地持久化,例如笔记应用将文档存为 Markdown 文件,或图片管理工具批量处理本地图片。
- 日志记录,将应用运行日志写入本地文件,方便用户反馈问题时提供诊断信息。
- 文件的导入导出,比如将表格数据导出为 CSV,或从指定目录批量导入素材。
- 实现简单的数据库替代方案,当数据量不大时,直接读写本地 JSON 文件比引入 SQLite 更轻量。
这些操作在 Web 环境中通常需要通过 <input type="file"> 或 FileReader 来间接实现,且受限于浏览器沙箱,无法自由选择路径或覆盖已有文件。而 Electron 则提供了完整的高权限文件访问,使你的应用像 VS Code、Obsidian 等工具一样,能够直接管理用户本地的文件与文件夹。
12.1.2 基本用法示例
以下代码展示在主进程中如何读写文本文件。为方便理解,这里使用 fs 的同步写法,但在实际项目中建议使用异步 API 以避免阻塞主进程。
主进程(main.js):创建窗口并注册 IPC 处理函数
const { app, BrowserWindow, ipcMain, dialog } = require('electron');
const fs = require('fs');
const path = require('path');
function createWindow() {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
});
win.loadFile('index.html');
// 监听渲染进程发来的“读取文件”请求
ipcMain.handle('read-file', async (event, filePath) => {
try {
const content = fs.readFileSync(filePath, 'utf-8');
return { success: true, data: content };
} catch (error) {
return { success: false, message: error.message };
}
});
// 监听“保存文件”请求——先打开保存对话框,再写入内容
ipcMain.handle('save-file', async (event, content) => {
const { filePath } = await dialog.showSaveDialog(win, {
title: '保存文件',
defaultPath: 'untitled.txt',
filters: [{ name: '文本文件', extensions: ['txt'] }]
});
if (!filePath) return { success: false, message: '用户取消了保存' };
try {
fs.writeFileSync(filePath, content, 'utf-8');
return { success: true };
} catch (error) {
return { success: false, message: error.message };
}
});
}
app.whenReady().then(createWindow);
预加载脚本(preload.js):通过 contextBridge 暴露安全的 API
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('fileAPI', {
readFile: (filePath) => ipcRenderer.invoke('read-file', filePath),
saveFile: (content) => ipcRenderer.invoke('save-file', content)
});
渲染进程(renderer.js 或 Vue/React 组件):调用这些方法
// 读取文件
const result = await window.fileAPI.readFile('/Users/me/data.txt');
if (result.success) {
console.log('文件内容:', result.data);
} else {
console.error('读取失败:', result.message);
}
// 保存文件
const saveResult = await window.fileAPI.saveFile('这是要写入的内容');
console.log(saveResult.success ? '保存成功' : saveResult.message);
上述代码遵循了 Electron 推荐的安全实践:
- 所有文件操作都放在主进程执行,渲染进程只负责触发和接受结果。
- 通过
ipcMain.handle和ipcRenderer.invoke实现异步的双向通信,保证了用户界面的流畅。 - 使用
contextBridge精确控制暴露给渲染进程的接口,避免直接暴露fs或ipcRenderer。
12.1.3 桌面端的特殊考量
虽然 fs 的用法与 Node.js 服务端开发几乎一致,但在桌面应用中有些细节值得注意:
路径安全性
永远不要直接相信渲染进程传来的路径。攻击者可能通过恶意脚本尝试读取系统敏感文件(如 /etc/passwd 或 C:\Windows\System32\config\SAM)。你应该:
- 明确限制用户可以访问的目录范围(例如只允许应用数据目录或用户选定的文件夹)。
- 对路径进行规范化(
path.normalize)并检查是否包含..等跳转符。 - 使用
dialog.showOpenDialog让用户主动选择文件,而不是直接输入路径字符串,这样更加安全且符合用户习惯。
大文件的处理
对于大文件(几百 MB 以上),不要使用 readFileSync 或 writeFileSync,因为它们会将整个文件内容载入内存,可能导致应用卡顿或内存溢出。应改用 fs.createReadStream 和 fs.createWriteStream 配合管道(pipe)进行分块处理,并结合进度条提升用户体验。
文件锁与冲突
当你的应用正在写入文件时,用户可能通过其他程序同时修改该文件。Node.js 本身不提供文件锁功能,但对于关键数据,你可以使用 proper-lockfile 等库来防止并发写入冲突。另一个常见模式是“写入临时文件 + 原子替换”(先写入 *.tmp,成功后再重命名为目标文件),确保即使在写入过程中崩溃,原文件也不会损坏。
权限问题
在 macOS 和 Linux 上,某些目录需要管理员权限才能写入。不应假设应用的运行目录总是可写的,应使用 app.getPath('userData') 来存储应用专属数据,这个路径在当前用户下一定有读写权限。
跨平台路径处理
永远使用 path.join() 或 path.resolve() 拼接路径,不要手动用 + '/' + 拼接字符串,否则会在 Windows 上出现反斜杠与正斜杠混用的问题。此外,文件名大小写敏感性在 macOS(默认不敏感)和 Linux(敏感)上不同,设计文件系统逻辑时应避免依赖大小写区分。
12.1.4 扩展能力:文件监控与元数据
除了单纯的读写,fs 模块还提供了 fs.watch 和 fs.watchFile 用来监控文件变化。这在 Electron 中很有用,例如当外部程序修改了当前打开的文件时,你可以提示用户重新加载(类似 VS Code 的文件冲突提示)。示例:
const watcher = fs.watch(filePath, (eventType) => {
if (eventType === 'change') {
console.log('文件已被外部修改');
// 通知渲染进程刷新
}
});
对于文件元数据,可以使用 fs.stat 获取文件大小、创建时间、权限等。结合 fs.readdir 可以构建一个简单的文件管理器,显示目录下所有文件的详细信息。
12.1.5 常见问题与技巧
- 错误处理:每次文件操作都应当捕获异常,因为文件可能被占用、路径不存在、磁盘空间不足等。良好的错误信息能让用户明白发生了什么,而不是直接崩溃。
- 统一使用 UTF-8 编码:除非处理二进制文件,读写文本时务必指定
'utf-8',避免在不同系统或语言环境下出现乱码。 - 性能优化:频繁的小文件读写可以使用内存缓存,定期刷盘。也可以使用 worker_threads 将文件处理放到独立的 Node.js 工作线程,避免影响主进程的响应速度。
总的来说,fs 模块为 Electron 应用赋予了真正的本地文件操作能力,使其不再只是个“套壳网页”。合理利用这一能力,结合安全的 IPC 模式,你可以轻松构建出像本地笔记、代码编辑器、文件处理工具等真正好用的桌面软件。