React / Vue 与 Electron 集成
前言
将 React 或 Vue 与 Electron 整合,可以让前端开发者用熟悉的框架开发桌面应用。目前最主流的工具链是 electron-vite,它将 Vite 的极速构建体验带入 Electron 开发,并完美支持 HMR(热模块替换)。本文将详细讲解 electron-vite + React 和 electron-vite + Vue 的项目搭建,以及状态管理方案。
electron-vite 工具链
为什么选择 electron-vite
对比传统 Electron Forge + Webpack:
- 构建速度:Vite 使用 ESM,开发模式下无需打包,启动时间从分钟级降到秒级
- HMR:修改代码后毫秒级更新,接近浏览器开发体验
- 配置简洁:一个入口配置管理主进程、预加载、渲染进程三方构建
创建项目
# React 项目
pnpm create @quick-start/electron electron-react-app --template react-ts
# Vue 项目
pnpm create @quick-start/electron electron-vue-app --template vue-ts
cd electron-react-app
pnpm install
pnpm dev项目结构:
electron-react-app/
├── electron/
│ ├── main.ts # 主进程
│ ├── preload.ts # 预加载脚本
│ └── ipc/ # IPC 处理函数
│ └── handlers.ts
├── src/
│ ├── main.tsx # React 入口
│ ├── App.tsx # 根组件
│ ├── pages/ # 页面组件
│ └── stores/ # 状态管理
├── electron.vite.config.ts # Vite 配置(主/预加载/渲染三方)
└── package.jsonelectron.vite.config.ts 核心配置
// electron.vite.config.ts
import { resolve } from 'path';
import { defineConfig, externalizeDepsPlugin } from 'electron-vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
main: {
plugins: [externalizeDepsPlugin()], // 自动 external electron 和 node 内置模块
build: {
rollupOptions: {
input: {
index: resolve(__dirname, 'electron/main.ts'),
},
},
},
},
preload: {
plugins: [externalizeDepsPlugin()],
build: {
rollupOptions: {
input: {
index: resolve(__dirname, 'electron/preload.ts'),
},
},
},
},
renderer: {
plugins: [react()],
resolve: {
alias: {
'@': resolve('src'),
},
},
build: {
rollupOptions: {
input: {
index: resolve(__dirname, 'index.html'),
},
},
},
},
});React + Electron 集成
项目初始化
pnpm create @quick-start/electron my-react-app --template react-ts安装依赖
pnpm add react-router-dom zustand @tanstack/react-queryReact Router 在 Electron 中的使用
// src/App.tsx
import { HashRouter, Routes, Route } from 'react-router-dom';
import Home from './pages/Home';
import Settings from './pages/Settings';
import Editor from './pages/Editor';
export default function App() {
return (
<HashRouter>
<nav className="app-nav">
<NavLink to="/">首页</NavLink>
<NavLink to="/editor">编辑器</NavLink>
<NavLink to="/settings">设置</NavLink>
</nav>
<main>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/editor" element={<Editor />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</main>
</HashRouter>
);
}注意:Electron 应用推荐使用 HashRouter 而不是 BrowserRouter,因为 HashRouter 使用 URL 的 # 部分存储路由状态,不需要服务器端配置路由重定向。
Zustand + IPC 桥接
Zustand 是 React 状态管理库,与 Electron IPC 结合可以实现主进程和渲染进程的状态同步:
// src/stores/appStore.ts — Zustand Store
import { create } from 'zustand';
import { subscribeWithSelector } from 'zustand/middleware';
// 定义 ElectronAPI 类型
declare global {
interface Window {
electronAPI: {
getAppVersion: () => Promise<string>;
getAppPath: (name: string) => Promise<string>;
minimize: () => Promise<void>;
maximize: () => Promise<void>;
close: () => Promise<void>;
onNetworkChange: (cb: (status: string) => void) => () => void;
};
}
}
interface AppState {
version: string;
networkStatus: 'online' | 'offline';
isMaximized: boolean;
setVersion: (v: string) => void;
setNetworkStatus: (status: 'online' | 'offline') => void;
setMaximized: (v: boolean) => void;
}
export const useAppStore = create<AppState>()(
subscribeWithSelector((set) => ({
version: '',
networkStatus: navigator.onLine ? 'online' : 'offline',
isMaximized: false,
setVersion: (v) => set({ version: v }),
setNetworkStatus: (status) => set({ networkStatus: status }),
setMaximized: (v) => set({ isMaximized: v }),
}))
);// src/main.tsx — 在 React 入口中初始化订阅
import { useEffect } from 'react';
import { useAppStore } from './stores/appStore';
function AppInit() {
const setVersion = useAppStore((s) => s.setVersion);
const setNetworkStatus = useAppStore((s) => s.setNetworkStatus);
useEffect(() => {
// 获取应用版本
window.electronAPI.getAppVersion().then(setVersion);
// 订阅网络状态变化(通过 IPC)
const unsubscribe = window.electronAPI.onNetworkChange((status) => {
setNetworkStatus(status as 'online' | 'offline');
});
return unsubscribe;
}, []);
return null;
}
export default function App() {
return (
<>
<AppInit />
<HashRouter>...</HashRouter>
</>
);
}Vue 3 + Electron 集成
项目初始化
pnpm create @quick-start/electron my-vue-app --template vue-tsPinia + IPC 桥接
// electron/preload.ts — Vue 版本 preload
import { contextBridge, ipcRenderer } from 'electron';
const api = {
// 主进程调用
invoke: <T = unknown>(channel: string, ...args: unknown[]) =>
ipcRenderer.invoke(channel, ...args) as Promise<T>,
// 事件订阅
on: (channel: string, callback: (...args: unknown[]) => void) => {
const subscription = (_event: Electron.IpcRendererEvent, ...args: unknown[]) =>
callback(...args);
ipcRenderer.on(channel, subscription);
return () => ipcRenderer.removeListener(channel, subscription);
},
};
contextBridge.exposeInMainWorld('electronAPI', api);// src/stores/app.ts — Pinia Store
import { defineStore } from 'pinia';
import { ref, onMounted } from 'vue';
export const useAppStore = defineStore('app', () => {
const version = ref('');
const networkStatus = ref<'online' | 'offline'>('online');
const isMaximized = ref(false);
async function fetchVersion() {
version.value = await window.electronAPI.invoke('app:get-version');
}
function setupNetworkListener() {
window.electronAPI.on('network:status-change', (status: string) => {
networkStatus.value = status as 'online' | 'offline';
});
}
// 窗口控制
async function minimize() {
await window.electronAPI.invoke('window:minimize');
}
async function maximize() {
await window.electronAPI.invoke('window:maximize');
isMaximized.value = !isMaximized.value;
}
onMounted(() => {
fetchVersion();
setupNetworkListener();
});
return { version, networkStatus, isMaximized, minimize, maximize };
});Vue Router 配置
// src/router/index.ts
import { createRouter, createWebHashHistory } from 'vue-router';
import Home from '../pages/Home.vue';
import Settings from '../pages/Settings.vue';
const routes = [
{ path: '/', name: 'home', component: Home },
{ path: '/settings', name: 'settings', component: Settings },
];
const router = createRouter({
history: createWebHashHistory(), // 使用 HashHistory,无需服务端配置
routes,
});
export default router;主进程 IPC 处理器(统一管理)
// electron/ipc/handlers.ts
import { ipcMain, BrowserWindow, app, dialog, shell } from 'electron';
import path from 'path';
export function registerIpcHandlers() {
// 应用信息
ipcMain.handle('app:get-version', () => app.getVersion());
ipcMain.handle('app:get-path', (_event, name: string) => app.getPath(name as any));
// 窗口控制
ipcMain.handle('window:minimize', (event) => {
BrowserWindow.fromWebContents(event.sender)?.minimize();
});
ipcMain.handle('window:maximize', (event) => {
const win = BrowserWindow.fromWebContents(event.sender);
if (win?.isMaximized()) {
win.unmaximize();
} else {
win?.maximize();
}
});
ipcMain.handle('window:close', (event) => {
BrowserWindow.fromWebContents(event.sender)?.close();
});
ipcMain.handle('window:is-maximized', (event) => {
return BrowserWindow.fromWebContents(event.sender)?.isMaximized() ?? false;
});
// 文件对话框
ipcMain.handle('dialog:open-file', async () => {
return dialog.showOpenDialog({
properties: ['openFile'],
filters: [{ name: '图片', extensions: ['png', 'jpg', 'webp'] }],
});
});
// Shell
ipcMain.handle('shell:open-external', async (_event, url: string) => {
// 验证 URL 白名单
if (url.startsWith('https://')) {
return shell.openExternal(url);
}
throw new Error('仅支持 https 链接');
});
ipcMain.handle('shell:show-item', (_event, filePath: string) => {
shell.showItemInFolder(filePath);
});
}// electron/main.ts
import { createWindow } from './window';
import { registerIpcHandlers } from './ipc/handlers';
app.whenReady().then(() => {
registerIpcHandlers();
createWindow();
});开发与构建
开发模式
# electron-vite 项目
pnpm dev # 启动开发服务器(主进程 + 渲染进程热更新)
pnpm build # 生产构建
pnpm preview # 预览生产构建结果生产构建输出
out/
├── my-react-app-darwin-arm64/ # macOS arm64 应用
│ ├── my-react-app.app
│ └── ...
├── my-react-app-win32-x64/ # Windows x64 应用
│ └── my-react-app.exe
└── my-react-app-linux-x64/ # Linux 应用
└── my-react-app常见问题
路由模式下刷新页面 404
Electron 加载的是本地文件,BrowserRouter 需要服务端重定向。使用 HashRouter 可以避免此问题。
React StrictMode 双重渲染
React 18 的 StrictMode 在开发环境下会双重渲染,可能导致 IPC 订阅重复。使用 subscribeWithSelector 中间件或 once 方式处理:
// preload.ts — 返回一次性取消函数
on: (channel, callback) => {
const handler = (...args) => callback(...args);
ipcRenderer.once(channel, handler); // once 只触发一次
return () => ipcRenderer.removeListener(channel, handler);
},小结
- electron-vite 是当前最推荐的 Electron + 前端框架构建工具链
- React / Vue 均使用
HashRouter(路由状态在#中,无需服务端支持) - Zustand / Pinia 状态管理库通过 IPC 桥接实现主进程与渲染进程的状态同步
- IPC 处理器集中管理在
electron/ipc/handlers.ts,便于维护 - preload 脚本通过
contextBridge暴露类型安全的 API 给前端
下一篇文章我们将介绍 应用打包与分发,学习使用 electron-builder 配置多平台打包、自动更新和代码签名。
评论
Written by
AI-Writer
Related Articles
Electron Forge 开发环境与项目配置
使用 Electron Forge 脚手架快速创建项目、配置 Vite/Webpack 构建、开发热重载调试、解析 package.json 关键字段与标准目录结构
Read More原生对话框与文件操作
使用 dialog.showOpenDialog、showSaveDialog、showMessageBox 原生对话框,Shell 模块打开外部链接和文件资源管理器,跨平台路径处理和用户数据目录访问
Read MoreElectron 安全最佳实践
掌握 Electron 安全配置:CSP 内容安全策略、openExternal 白名单、webSecurity 限制、safeStorage 加密存储、Electron Fuses 安全加固以及安全审计工具的使用
Read More