macOS之Electron搭建
本文从 Electron Forge 的 Vite + TypeScript 项目开始,接入 React,并实现一个安全的“选择文件”功能。目标是理解 Electron 的进程边界,而不是只让窗口跑起来。
目标与架构
完成后,项目包含:
- Electron 桌面窗口与 Vite 开发环境;
- React + TypeScript 渲染界面;
- 通过 preload 和 IPC 调用 macOS 文件选择器;
- 使用
npm start开发,npm run package打包,npm run make生成分发产物。
Electron 的关键是三层分工:
React UI(Renderer)
│ 调用受限 API
▼
Preload(contextBridge)
│ IPC
▼
Electron Main
│
▼
macOS:文件、菜单、通知、窗口等原生能力主进程负责应用生命周期、窗口和系统 API;每个窗口有独立的渲染进程,负责网页界面。preload 位于两者之间,只暴露经过设计的最小接口。
1. 准备环境
确认 Node.js 与 npm 可用:
node -v
npm -v
git --version建议使用当前受支持的 Node.js LTS 版本。编辑器可使用 VS Code、Cursor 或 WebStorm。
2. 创建 Forge + Vite + TypeScript 项目
npx create-electron-app@latest my-electron-app --template=vite-typescript
cd my-electron-app
npm start首次启动应出现 Electron 窗口。该模板已经提供 Electron、Vite 与 TypeScript;其生成文件会随 Forge 版本变化,后续以项目中已有的入口文件和配置为准。
3. 接入 React
停止开发进程后安装依赖:
npm install react react-dom
npm install -D @types/react @types/react-dom @vitejs/plugin-react在 vite.renderer.config.ts 中加入 React 插件:
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
});确保渲染进程的 TypeScript 配置含有 JSX 支持(有些模板会拆分为多个 tsconfig):
{
"compilerOptions": {
"jsx": "react-jsx"
}
}将渲染入口改为 src/renderer.tsx,并在 src/index.html 使用它:
<div id="root"></div>
<script type="module" src="/src/renderer.tsx"></script>创建 src/App.tsx:
function App() {
return <h1>Electron + React</h1>;
}
export default App;再创建渲染入口 src/renderer.tsx:
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
const root = document.getElementById('root');
if (!root) throw new Error('找不到 #root 挂载节点');
createRoot(root).render(
<StrictMode>
<App />
</StrictMode>,
);至此,React 只在 Renderer 中运行;不要在 React 组件里直接导入 electron、fs 或 child_process。
4. 安全模型:不要跳过 preload
推荐保持下列默认安全策略:
new BrowserWindow({
webPreferences: {
preload: /* Forge 生成的 preload 入口 */,
contextIsolation: true,
nodeIntegration: false,
sandbox: true,
},
});Forge 的 Vite 模板通常已创建窗口和 preload 入口;不要为了套用示例重写其加载开发服务器/打包文件的逻辑。只需在现有 main.ts 中注册业务 IPC,并在现有 preload.ts 中暴露受限 API。
不要这样做:
// 不安全:页面可发送任意 IPC 消息
contextBridge.exposeInMainWorld('electronAPI', ipcRenderer);而应按能力逐项开放,例如只提供 openFile()。
5. 实现第一个 IPC:选择文件
主进程:处理原生能力
在 src/main.ts 的现有 import 中加入 dialog 与 ipcMain,并在应用就绪后或创建窗口前注册处理器:
import { dialog, ipcMain } from 'electron';
ipcMain.handle('dialog:openFile', async () => {
const result = await dialog.showOpenDialog({
properties: ['openFile'],
});
return result.canceled ? null : result.filePaths[0];
});Preload:定义窄桥梁
在 src/preload.ts 中:
import { contextBridge, ipcRenderer } from 'electron';
contextBridge.exposeInMainWorld('electronAPI', {
openFile: (): Promise<string | null> => ipcRenderer.invoke('dialog:openFile'),
});TypeScript:声明全局 API
创建 src/global.d.ts:
export {};
declare global {
interface Window {
electronAPI: {
openFile(): Promise<string | null>;
};
}
}React:调用桥梁
将 App.tsx 改为:
import { useState } from 'react';
function App() {
const [filePath, setFilePath] = useState<string | null>(null);
const handleOpenFile = async () => {
setFilePath(await window.electronAPI.openFile());
};
return (
<main>
<h1>Electron + React</h1>
<button onClick={handleOpenFile}>选择文件</button>
{filePath && <p>当前文件:{filePath}</p>}
</main>
);
}
export default App;调用链为:
React 按钮 → window.electronAPI.openFile()
→ preload 的 ipcRenderer.invoke()
→ main 的 ipcMain.handle()
→ dialog.showOpenDialog()这是后续读取文件、访问 SQLite、显示通知或调用系统菜单的通用模式:系统权限与输入校验留在 Main;Renderer 只调用明确的业务 API。
6. 开发、打包与分发
# 本地开发(Electron + Vite)
npm start
# 打包应用本体
npm run package
# 生成平台分发产物
npm run makepackage 生成可运行的应用目录;make 在其基础上运行 Forge makers,生成 ZIP、DMG 等面向分发的文件,实际格式取决于 forge.config.ts 中已配置的 makers。
若需要 DMG,安装并在 forge.config.ts 的 makers 数组中配置:
npm install -D @electron-forge/maker-dmg{
name: '@electron-forge/maker-dmg',
config: {},
}在 Apple Silicon Mac 上,产物常为 darwin-arm64;Intel Mac 对应 x64。面向他人分发时,还需要 Apple Developer 签名与 notarization,否则 Gatekeeper 可能阻止或警告用户。
7. 建议的扩展结构
项目增长后,按进程拆分,而不是让 React 直接接触系统 API:
src/
├── main/
│ ├── main.ts
│ ├── ipc/
│ │ ├── file.ts
│ │ ├── database.ts
│ │ └── system.ts
│ └── services/
├── preload/
│ └── preload.ts
└── renderer/
├── main.tsx
├── App.tsx
├── components/
├── pages/
└── stores/学习路径建议:React 界面 → 三进程模型 → IPC → 文件系统/SQLite → 窗口、菜单和 Tray → 打包 → 签名与公证 → 自动更新。