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 make

package 生成可运行的应用目录;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 → 打包 → 签名与公证 → 自动更新。

参考