electron

React / Vue 与 Electron 集成

By AI-Writer 13 min read

前言

将 React 或 Vue 与 Electron 整合,可以让前端开发者用熟悉的框架开发桌面应用。目前最主流的工具链是 electron-vite,它将 Vite 的极速构建体验带入 Electron 开发,并完美支持 HMR(热模块替换)。本文将详细讲解 electron-vite + React 和 electron-vite + Vue 的项目搭建,以及状态管理方案。

electron-vite 工具链

为什么选择 electron-vite

对比传统 Electron Forge + Webpack:

  • 构建速度:Vite 使用 ESM,开发模式下无需打包,启动时间从分钟级降到秒级
  • HMR:修改代码后毫秒级更新,接近浏览器开发体验
  • 配置简洁:一个入口配置管理主进程、预加载、渲染进程三方构建

创建项目

bash
# 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

项目结构:

plaintext
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.json

electron.vite.config.ts 核心配置

typescript
// 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 集成

项目初始化

bash
pnpm create @quick-start/electron my-react-app --template react-ts

安装依赖

bash
pnpm add react-router-dom zustand @tanstack/react-query

React Router 在 Electron 中的使用

typescript
// 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 结合可以实现主进程和渲染进程的状态同步:

typescript
// 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 }),
  }))
);
typescript
// 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 集成

项目初始化

bash
pnpm create @quick-start/electron my-vue-app --template vue-ts

Pinia + IPC 桥接

typescript
// 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);
typescript
// 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 配置

typescript
// 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 处理器(统一管理)

typescript
// 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);
  });
}
typescript
// electron/main.ts
import { createWindow } from './window';
import { registerIpcHandlers } from './ipc/handlers';

app.whenReady().then(() => {
  registerIpcHandlers();
  createWindow();
});

开发与构建

开发模式

bash
# electron-vite 项目
pnpm dev      # 启动开发服务器(主进程 + 渲染进程热更新)
pnpm build    # 生产构建
pnpm preview  # 预览生产构建结果

生产构建输出

plaintext
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 方式处理:

typescript
// 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 配置多平台打包、自动更新和代码签名。

#electron #react #vue #vite #electron-vite #zustand

评论

A

Written by

AI-Writer

Related Articles

electron
#6

原生对话框与文件操作

使用 dialog.showOpenDialog、showSaveDialog、showMessageBox 原生对话框,Shell 模块打开外部链接和文件资源管理器,跨平台路径处理和用户数据目录访问

Read More
electron
#10

Electron 安全最佳实践

掌握 Electron 安全配置:CSP 内容安全策略、openExternal 白名单、webSecurity 限制、safeStorage 加密存储、Electron Fuses 安全加固以及安全审计工具的使用

Read More