electron
原生对话框与文件操作
By AI-Writer 9 min read
前言
桌面应用的核心价值之一是与操作系统深度集成。Electron 提供了丰富的原生对话框(文件选择、保存确认、消息提示)和 Shell 模块,让开发者无需自己实现这些复杂的原生交互。本文将详细介绍这些功能的用法和注意事项。
原生对话框
dialog 模块
dialog 模块只能从主进程使用,用于显示系统原生对话框:
typescript
// main.ts
import { dialog, BrowserWindow } from 'electron';
// 最好关联一个父窗口,否则 macOS 上可能不显示在应用前方
const parent = BrowserWindow.getFocusedWindow();打开文件对话框
typescript
// main.ts — 打开文件选择对话框
ipcMain.handle('dialog:open-file', async (event) => {
const result = await dialog.showOpenDialog({
title: '选择图片文件',
defaultPath: '/Users', // macOS 默认路径
buttonLabel: '打开',
filters: [
// 文件类型过滤器
{ name: '图片文件', extensions: ['jpg', 'jpeg', 'png', 'gif', 'webp'] },
{ name: '所有文件', extensions: ['*'] },
],
// 是否允许多选
properties: ['openFile', 'multiSelections'],
// 是否显示隐藏文件
showHiddenFiles: false,
});
return result;
});
// result = { canceled: false, filePaths: ['/path/to/file.png'] }打开文件夹对话框
typescript
ipcMain.handle('dialog:open-directory', async () => {
const result = await dialog.showOpenDialog({
title: '选择项目目录',
properties: ['openDirectory', 'createDirectory'],
});
if (!result.canceled) {
return result.filePaths[0];
}
return null;
});保存文件对话框
typescript
ipcMain.handle('dialog:save-file', async () => {
const result = await dialog.showSaveDialog({
title: '保存文件',
defaultPath: '/Users/Downloads/report.pdf',
filters: [
{ name: 'PDF 文件', extensions: ['pdf'] },
{ name: 'Word 文档', extensions: ['docx'] },
],
});
if (!result.canceled && result.filePath) {
// 在此将内容写入 result.filePath
return { success: true, path: result.filePath };
}
return { success: false };
});消息对话框
typescript
// 确认对话框
const { response, checkboxChecked } = await dialog.showMessageBox({
type: 'question', // 'none' | 'info' | 'error' | 'question' | 'warning'
buttons: ['保存', '不保存', '取消'],
defaultId: 0, // 默认聚焦的按钮索引
cancelId: 2, // 按 ESC 时返回的索引
title: '确认退出',
message: '是否保存未提交的更改?',
detail: '您有未保存的内容,退出将丢失。',
checkboxLabel: '不再提示', // macOS/Windows 可显示复选框
checkboxChecked: false,
});
// response: 0 = 保存, 1 = 不保存, 2 = 取消
if (response === 0) {
await saveFile();
}
app.quit();错误对话框
typescript
dialog.showErrorBox('错误', '发生了未知错误,请联系开发者。');
// 这在 Electron 的早期阶段(app.whenReady 之前也可使用)Shell 模块
shell 模块用于操作系统级别的操作,在主进程和渲染进程中都可以使用(渲染进程需通过 preload 暴露)。
打开外部链接
typescript
// main.ts
import { shell } from 'electron';
// 打开外部浏览器
await shell.openExternal('https://example.com');
// 打开本地文件(使用系统默认应用)
await shell.openPath('/path/to/file.pdf');
// 等同于 macOS: open /path/to/file.pdf
// Windows: cmd /c start /path/to/file.pdftypescript
// ❌ 安全问题:不要直接暴露 shell.openExternal 给渲染进程
// 恶意网页可以打开任意 URL
// ✅ preload.ts — 白名单控制
contextBridge.exposeInMainWorld('electronAPI', {
openExternal: (url: string) => {
// 白名单校验
const allowed = ['https://docs.example.com', 'https://help.example.com'];
if (allowed.some((prefix) => url.startsWith(prefix))) {
return shell.openExternal(url);
}
throw new Error('URL 不在白名单中');
},
});文件资源管理器操作
typescript
// 在文件管理器中显示文件(选中文件)
shell.showItemInFolder('/path/to/file.txt');
// 显示文件夹内容(不选中任何文件)
shell.openPath('/path/to/folder');Shell 操作整合示例
typescript
// main.ts — 完整的文件操作 API
import { shell, dialog, app, BrowserWindow } from 'electron';
import fs from 'fs';
import path from 'path';
ipcMain.handle('file:open-in-explorer', async (_event, filePath: string) => {
if (!fs.existsSync(filePath)) {
throw new Error(`文件不存在: ${filePath}`);
}
shell.showItemInFolder(filePath);
});
ipcMain.handle('file:open-with-system', async (_event, filePath: string) => {
if (!fs.existsSync(filePath)) {
throw new Error(`文件不存在: ${filePath}`);
}
return shell.openPath(filePath);
});
ipcMain.handle('file:open-external-link', async (_event, url: string) => {
// 验证 URL 格式
try {
const parsed = new URL(url);
if (['http:', 'https:'].includes(parsed.protocol)) {
return shell.openExternal(url);
}
throw new Error('仅支持 http/https 链接');
} catch {
throw new Error('无效的 URL');
}
});路径处理与用户数据目录
app.getPath
app.getPath 返回系统标准目录的绝对路径:
typescript
import { app } from 'electron';
console.log('用户数据目录:', app.getPath('userData'));
// macOS: ~/Library/Application Support/MyApp
// Windows: C:\Users\Alice\AppData\Roaming\MyApp
// Linux: ~/.config/MyApp
console.log('下载目录:', app.getPath('downloads'));
console.log('文档目录:', app.getPath('documents'));
console.log('临时目录:', app.getPath('temp'));
console.log('桌面目录:', app.getPath('desktop'));
console.log('应用日志目录:', app.getPath('logs'));跨平台路径拼接
typescript
import path from 'path';
import { app } from 'electron';
// 始终使用 path.join 拼接路径,避免硬编码斜杠
const configDir = app.getPath('userData');
const configFile = path.join(configDir, 'config.json');
const logFile = path.join(configDir, 'logs', 'app.log');
// path.join 会自动处理平台差异
// path.join('a', 'b') → 'a/b' (macOS/Linux) 或 'a\b' (Windows)
// 注意:Electron 中使用 import 而不是 require 时
// Node.js 的 path 模块需要显式导入
import { join, dirname, basename, extname } from 'path';
const userDataPath = app.getPath('userData');
const configPath = join(userDataPath, 'settings.json');
const logDir = join(userDataPath, 'logs');
// dirname / basename / extname 实用函数
console.log(basename(configPath)); // 'settings.json'
console.log(extname(configPath)); // '.json'
console.log(dirname(configPath)); // '/Users/.../MyApp'文件系统操作
主进程文件读写
渲染进程的文件操作受限于安全沙箱,所有文件操作应在主进程中进行:
typescript
// main.ts — 完整的文件读写 IPC 处理
import fs from 'fs/promises';
import { ipcMain, dialog, app } from 'electron';
import path from 'path';
// 读取文件
ipcMain.handle('file:read', async (_event, filePath: string) => {
try {
// 安全检查:限制在用户数据目录内
const userDataDir = app.getPath('userData');
const resolvedPath = path.resolve(filePath);
if (!resolvedPath.startsWith(userDataDir)) {
throw new Error('路径超出允许范围');
}
const content = await fs.readFile(resolvedPath, 'utf-8');
return { success: true, data: content };
} catch (error) {
return { success: false, error: (error as Error).message };
}
});
// 写入文件
ipcMain.handle('file:write', async (_event, filePath: string, data: string) => {
try {
const userDataDir = app.getPath('userData');
const resolvedPath = path.resolve(filePath);
if (!resolvedPath.startsWith(userDataDir)) {
throw new Error('路径超出允许范围');
}
await fs.writeFile(resolvedPath, data, 'utf-8');
return { success: true };
} catch (error) {
return { success: false, error: (error as Error).message };
}
});
// 列出目录
ipcMain.handle('file:list', async (_event, dirPath: string) => {
try {
const entries = await fs.readdir(dirPath, { withFileTypes: true });
return entries.map((entry) => ({
name: entry.name,
isDirectory: entry.isDirectory(),
isFile: entry.isFile(),
}));
} catch (error) {
return { error: (error as Error).message };
}
});渲染进程中使用
typescript
// preload.ts
contextBridge.exposeInMainWorld('electronAPI', {
readFile: (path: string) => ipcRenderer.invoke('file:read', path),
writeFile: (path: string, data: string) =>
ipcRenderer.invoke('file:write', path, data),
listDir: (path: string) => ipcRenderer.invoke('file:list', path),
openFileDialog: () => ipcRenderer.invoke('dialog:open-file'),
openFolderDialog: () => ipcRenderer.invoke('dialog:open-directory'),
saveFileDialog: () => ipcRenderer.invoke('dialog:save-file'),
showConfirmDialog: (message: string, detail?: string) =>
ipcRenderer.invoke('dialog:confirm', message, detail),
});实用场景:图片保存功能
typescript
// main.ts — 完整的图片保存流程
ipcMain.handle('image:save-from-url', async (_event, url: string, suggestedName: string) => {
const downloadsDir = app.getPath('downloads');
// 让用户选择保存位置
const { canceled, filePath } = await dialog.showSaveDialog({
title: '保存图片',
defaultPath: path.join(downloadsDir, suggestedName),
filters: [{ name: '图片', extensions: ['png', 'jpg', 'webp'] }],
});
if (canceled || !filePath) return { success: false, reason: '用户取消' };
// 下载图片
const response = await fetch(url);
if (!response.ok) throw new Error('下载失败');
const buffer = await response.arrayBuffer();
await fs.writeFile(filePath, Buffer.from(buffer));
// 在文件管理器中显示
shell.showItemInFolder(filePath);
return { success: true, path: filePath };
});常见问题
dialog 在无头环境(CI)失败
typescript
// 在无 GUI 环境下,对话框会静默失败或抛出错误
// CI 环境使用 --no-sandbox 或虚拟显示
const { contextBridge } = require('electron');
// 建议在 CI 中 mock dialog 返回值进行测试Windows 路径中文编码
typescript
// Windows 上路径含中文时可能需要额外处理
import { Buffer } from 'buffer';
// 读取文件时指定编码
const content = await fs.readFile(nativePath, 'utf-8');小结
dialog.showOpenDialog/showSaveDialog用于文件和目录选择dialog.showMessageBox显示确认、警告、错误等消息对话框shell.openExternal打开外部 URL,showItemInFolder定位文件- 所有文件操作在主进程中进行,通过 IPC 暴露给渲染进程
- 使用
path.join处理跨平台路径,app.getPath获取系统标准目录 - 安全原则:永远不直接暴露文件写入权限,通过 preload 做路径限制和操作封装
下一篇文章我们将介绍 Node.js 原生模块调用,包括在 Electron 中使用 sqlite3、sharp 等原生模块,以及 electron-rebuild 编译方案。
#electron
#dialog
#shell
#文件系统
#app.getPath
评论
A
Written by
AI-Writer
Related Articles
electron
#1 Electron 入门与核心架构
了解 Electron 的双引擎架构(Chromium + Node.js)、主进程与渲染进程的职责分工,快速搭建第一个 Hello World 应用,掌握 BrowserWindow 基本配置
Read More electron
#8 React / Vue 与 Electron 集成
使用 electron-vite 工具链整合 Vite + React/Vue + Electron,配置路由(React Router / Vue Router),实现主进程与渲染进程状态共享(Zustand IPC 桥接)
Read More electron
#2 Electron Forge 开发环境与项目配置
使用 Electron Forge 脚手架快速创建项目、配置 Vite/Webpack 构建、开发热重载调试、解析 package.json 关键字段与标准目录结构
Read More