技术文档中心
首页
React
Vue
TypeScript
Kotlin
React Native
Electron
Android
首页
React
Vue
TypeScript
Kotlin
React Native
Electron
Android
  • 基础入门

    • Electron 学习指南
    • 窗口管理
    • 进程通信
    • 对话框
  • 进阶内容

    • 菜单和托盘
    • 打包发布
    • Electron 自动更新
  • 框架集成

    • React + Electron
    • Vue + Electron

React + Electron

项目搭建

使用 Vite 创建项目

# 创建 React 项目
yarn create vite my-app --template react
cd my-app
yarn install

# 安装 Electron
yarn add -D electron electron-builder concurrently wait-on cross-env

项目结构

my-app/
├── electron/
│   ├── main.js           # 主进程
│   └── preload.js        # 预加载脚本
├── src/
│   ├── App.jsx
│   ├── main.jsx
│   └── components/
├── index.html
├── package.json
└── vite.config.js

配置 package.json

{
  "name": "my-app",
  "version": "1.0.0",
  "main": "electron/main.js",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "electron": "wait-on tcp:5173 && cross-env NODE_ENV=development electron .",
    "electron:dev": "concurrently \"yarn dev\" \"yarn electron\"",
    "electron:build": "vite build && electron-builder"
  },
  "build": {
    "appId": "com.example.myapp",
    "productName": "我的应用",
    "directories": {
      "output": "dist-electron"
    },
    "files": [
      "dist/**/*",
      "electron/**/*"
    ],
    "mac": {
      "target": ["dmg"]
    },
    "win": {
      "target": ["nsis"]
    },
    "linux": {
      "target": ["AppImage"]
    }
  }
}

配置 vite.config.js

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  base: './',
  server: {
    port: 5173
  },
  build: {
    outDir: 'dist'
  }
})

主进程配置

electron/main.js

const { app, BrowserWindow, ipcMain } = require('electron')
const path = require('path')

const isDev = process.env.NODE_ENV === 'development'

function createWindow() {
  const win = new BrowserWindow({
    width: 1200,
    height: 800,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      nodeIntegration: false,
      contextIsolation: true
    }
  })

  if (isDev) {
    win.loadURL('http://localhost:5173')
    win.webContents.openDevTools()
  } else {
    win.loadFile(path.join(__dirname, '../dist/index.html'))
  }
}

app.whenReady().then(() => {
  createWindow()

  app.on('activate', () => {
    if (BrowserWindow.getAllWindows().length === 0) {
      createWindow()
    }
  })
})

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit()
  }
})

// IPC 通信示例
ipcMain.handle('get-app-version', () => {
  return app.getVersion()
})

ipcMain.handle('get-app-path', (event, name) => {
  return app.getPath(name)
})

electron/preload.js

const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld('electronAPI', {
  getAppVersion: () => ipcRenderer.invoke('get-app-version'),
  getAppPath: (name) => ipcRenderer.invoke('get-app-path', name),
  
  // 文件操作
  openFile: () => ipcRenderer.invoke('open-file'),
  saveFile: (content) => ipcRenderer.invoke('save-file', content),
  
  // 系统通知
  showNotification: (title, body) => ipcRenderer.send('show-notification', title, body),
  
  // 监听事件
  onUpdateAvailable: (callback) => {
    ipcRenderer.on('update-available', (event, info) => callback(info))
  }
})

React 组件集成

创建 Electron Context

// src/contexts/ElectronContext.jsx
import { createContext, useContext, useEffect, useState } from 'react'

const ElectronContext = createContext(null)

export function ElectronProvider({ children }) {
  const [isElectron, setIsElectron] = useState(false)
  const [appVersion, setAppVersion] = useState('')

  useEffect(() => {
    // 检测是否在 Electron 环境
    if (window.electronAPI) {
      setIsElectron(true)
      
      // 获取应用版本
      window.electronAPI.getAppVersion().then(version => {
        setAppVersion(version)
      })
    }
  }, [])

  const value = {
    isElectron,
    appVersion,
    api: window.electronAPI
  }

  return (
    <ElectronContext.Provider value={value}>
      {children}
    </ElectronContext.Provider>
  )
}

export function useElectron() {
  const context = useContext(ElectronContext)
  if (!context) {
    throw new Error('useElectron must be used within ElectronProvider')
  }
  return context
}

使用 Context

// src/main.jsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import { ElectronProvider } from './contexts/ElectronContext'
import './index.css'

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <ElectronProvider>
      <App />
    </ElectronProvider>
  </React.StrictMode>
)

组件中使用

// src/App.jsx
import { useElectron } from './contexts/ElectronContext'
import FileManager from './components/FileManager'
import './App.css'

function App() {
  const { isElectron, appVersion } = useElectron()

  return (
    <div className="app">
      <header>
        <h1>React + Electron 应用</h1>
        {isElectron && <span>v{appVersion}</span>}
      </header>
      
      <main>
        {isElectron ? (
          <FileManager />
        ) : (
          <p>请在 Electron 环境中运行</p>
        )}
      </main>
    </div>
  )
}

export default App

实战示例:文件管理器

主进程文件操作

// electron/main.js
const { dialog } = require('electron')
const fs = require('fs').promises

ipcMain.handle('open-file', async () => {
  const result = await dialog.showOpenDialog({
    properties: ['openFile'],
    filters: [
      { name: 'Text Files', extensions: ['txt', 'md', 'json'] },
      { name: 'All Files', extensions: ['*'] }
    ]
  })

  if (result.canceled) {
    return { success: false, canceled: true }
  }

  try {
    const filePath = result.filePaths[0]
    const content = await fs.readFile(filePath, 'utf-8')
    return {
      success: true,
      filePath,
      fileName: path.basename(filePath),
      content
    }
  } catch (error) {
    return { success: false, error: error.message }
  }
})

ipcMain.handle('save-file', async (event, { filePath, content }) => {
  try {
    let saveFilePath = filePath

    if (!saveFilePath) {
      const result = await dialog.showSaveDialog({
        filters: [
          { name: 'Text Files', extensions: ['txt', 'md'] }
        ]
      })

      if (result.canceled) {
        return { success: false, canceled: true }
      }

      saveFilePath = result.filePath
    }

    await fs.writeFile(saveFilePath, content, 'utf-8')
    return {
      success: true,
      filePath: saveFilePath,
      fileName: path.basename(saveFilePath)
    }
  } catch (error) {
    return { success: false, error: error.message }
  }
})

React 文件管理组件

// src/components/FileManager.jsx
import { useState } from 'react'
import { useElectron } from '../contexts/ElectronContext'
import './FileManager.css'

function FileManager() {
  const { api } = useElectron()
  const [currentFile, setCurrentFile] = useState(null)
  const [content, setContent] = useState('')
  const [isModified, setIsModified] = useState(false)

  const handleOpenFile = async () => {
    const result = await api.openFile()
    
    if (result.success) {
      setCurrentFile({
        path: result.filePath,
        name: result.fileName
      })
      setContent(result.content)
      setIsModified(false)
    } else if (!result.canceled) {
      alert('打开文件失败: ' + result.error)
    }
  }

  const handleSaveFile = async () => {
    const result = await api.saveFile({
      filePath: currentFile?.path,
      content
    })

    if (result.success) {
      setCurrentFile({
        path: result.filePath,
        name: result.fileName
      })
      setIsModified(false)
      api.showNotification('保存成功', '文件已保存')
    } else if (!result.canceled) {
      alert('保存文件失败: ' + result.error)
    }
  }

  const handleContentChange = (e) => {
    setContent(e.target.value)
    setIsModified(true)
  }

  return (
    <div className="file-manager">
      <div className="toolbar">
        <button onClick={handleOpenFile}>打开文件</button>
        <button onClick={handleSaveFile} disabled={!isModified}>
          保存 {isModified && '*'}
        </button>
        <span className="file-name">
          {currentFile ? currentFile.name : '未打开文件'}
        </span>
      </div>

      <textarea
        className="editor"
        value={content}
        onChange={handleContentChange}
        placeholder="打开文件或开始输入..."
      />

      <div className="status-bar">
        <span>{content.length} 字符</span>
        <span>{content.split('\n').length} 行</span>
      </div>
    </div>
  )
}

export default FileManager

使用 TypeScript

安装依赖

yarn add -D typescript @types/react @types/react-dom @types/node

tsconfig.json

{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true
  },
  "include": ["src"],
  "references": [{ "path": "./tsconfig.node.json" }]
}

类型定义

// src/types/electron.d.ts
export interface ElectronAPI {
  getAppVersion: () => Promise<string>
  getAppPath: (name: string) => Promise<string>
  openFile: () => Promise<FileResult>
  saveFile: (data: SaveFileData) => Promise<FileResult>
  showNotification: (title: string, body: string) => void
  onUpdateAvailable: (callback: (info: UpdateInfo) => void) => void
}

export interface FileResult {
  success: boolean
  canceled?: boolean
  error?: string
  filePath?: string
  fileName?: string
  content?: string
}

export interface SaveFileData {
  filePath?: string
  content: string
}

declare global {
  interface Window {
    electronAPI: ElectronAPI
  }
}

TypeScript 组件

// src/components/FileManager.tsx
import { useState } from 'react'
import { useElectron } from '../contexts/ElectronContext'

interface FileInfo {
  path: string
  name: string
}

function FileManager() {
  const { api } = useElectron()
  const [currentFile, setCurrentFile] = useState<FileInfo | null>(null)
  const [content, setContent] = useState<string>('')
  const [isModified, setIsModified] = useState<boolean>(false)

  const handleOpenFile = async (): Promise<void> => {
    const result = await api.openFile()
    
    if (result.success && result.filePath && result.fileName) {
      setCurrentFile({
        path: result.filePath,
        name: result.fileName
      })
      setContent(result.content || '')
      setIsModified(false)
    }
  }

  // ... 其他方法

  return (
    // ... JSX
  )
}

export default FileManager

状态管理集成

使用 Zustand

yarn add zustand
// src/stores/fileStore.ts
import { create } from 'zustand'

interface FileState {
  currentFile: { path: string; name: string } | null
  content: string
  isModified: boolean
  setCurrentFile: (file: { path: string; name: string } | null) => void
  setContent: (content: string) => void
  setIsModified: (modified: boolean) => void
}

export const useFileStore = create<FileState>((set) => ({
  currentFile: null,
  content: '',
  isModified: false,
  setCurrentFile: (file) => set({ currentFile: file }),
  setContent: (content) => set({ content, isModified: true }),
  setIsModified: (modified) => set({ isModified: modified })
}))
// 在组件中使用
import { useFileStore } from '../stores/fileStore'

function FileManager() {
  const { currentFile, content, isModified, setContent } = useFileStore()
  
  // ...
}

性能优化

1. 代码分割

import { lazy, Suspense } from 'react'

const FileManager = lazy(() => import('./components/FileManager'))

function App() {
  return (
    <Suspense fallback={<div>加载中...</div>}>
      <FileManager />
    </Suspense>
  )
}

2. 虚拟列表

yarn add react-window
import { FixedSizeList } from 'react-window'

function FileList({ files }) {
  const Row = ({ index, style }) => (
    <div style={style}>
      {files[index].name}
    </div>
  )

  return (
    <FixedSizeList
      height={600}
      itemCount={files.length}
      itemSize={35}
      width="100%"
    >
      {Row}
    </FixedSizeList>
  )
}

3. 防抖输入

import { useMemo } from 'react'
import { debounce } from 'lodash-es'

function Editor() {
  const handleChange = useMemo(
    () => debounce((value) => {
      // 保存到本地存储
      localStorage.setItem('draft', value)
    }, 1000),
    []
  )

  return (
    <textarea onChange={(e) => handleChange(e.target.value)} />
  )
}

打包发布

# 构建
yarn electron:build

# 指定平台
yarn electron:build --mac
yarn electron:build --win
yarn electron:build --linux

调试技巧

VSCode 配置

// .vscode/launch.json
{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Electron: Main",
      "type": "node",
      "request": "launch",
      "cwd": "${workspaceFolder}",
      "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron",
      "windows": {
        "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd"
      },
      "args": ["."],
      "outputCapture": "std",
      "env": {
        "NODE_ENV": "development"
      }
    }
  ]
}

常见问题

1. 开发环境跨域

// vite.config.js
export default defineConfig({
  server: {
    cors: true
  }
})

2. 生产环境路径问题

// electron/main.js
const isDev = process.env.NODE_ENV === 'development'

if (isDev) {
  win.loadURL('http://localhost:5173')
} else {
  win.loadFile(path.join(__dirname, '../dist/index.html'))
}

3. 热更新

yarn add -D electron-reloader
// electron/main.js
try {
  require('electron-reloader')(module)
} catch {}
最近更新: 2026/2/24 16:53
Contributors: hailong
Next
Vue + Electron