{T}

项目架构设计

本文介绍 Electron 应用的项目架构设计最佳实践,帮助你构建可维护、可扩展的应用。

架构概览

Electron 应用采用多进程架构,主要包含三个核心部分:

plaintext
┌─────────────────────────────────────────────────────────────────┐
│                        Electron 应用                             │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ┌─────────────────┐    IPC    ┌─────────────────────────────┐ │
│  │   主进程 (Main)  │◄────────►│      渲染进程 (Renderer)     │ │
│  │                 │           │                             │ │
│  │  - 窗口管理      │           │  - UI 渲染 (Vue/React)      │ │
│  │  - 系统托盘      │           │  - 用户交互                 │ │
│  │  - 原生菜单      │           │  - 页面路由                 │ │
│  │  - 文件系统      │           │                             │ │
│  │  - IPC 处理器    │           │                             │ │
│  └────────┬────────┘           └──────────────┬──────────────┘ │
│           │                                     │               │
│           │         ┌─────────────────┐         │               │
│           └────────►│  预加载脚本      │◄────────┘               │
│                     │  (Preload)       │                        │
│                     │                  │                        │
│                     │  - contextBridge │                        │
│                     │  - API 暴露      │                        │
│                     │  - 安全隔离      │                        │
│                     └──────────────────┘                        │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

进程职责划分

进程运行环境职责
主进程Node.js窗口管理、系统托盘、原生菜单、文件系统操作、IPC 处理
渲染进程浏览器UI 渲染、用户交互、页面路由
预加载脚本Node.js (受限)桥接主进程与渲染进程、安全暴露 API

目录结构

基础结构

plaintext
my-electron-app/
├── package.json
├── electron-builder.json       # 打包配置
├── tsconfig.json               # TypeScript 配置
├── vite.config.ts              # Vite 配置

├── src/
│   ├── main/                   # 主进程代码
│   │   ├── index.ts            # 主进程入口
│   │   ├── ipc/                # IPC 处理器
│   │   │   ├── index.ts
│   │   │   ├── dialog.ts
│   │   │   └── file.ts
│   │   ├── services/           # 主进程服务
│   │   │   ├── store.ts
│   │   │   ├── tray.ts
│   │   │   └── window.ts
│   │   └── utils/              # 工具函数
│   │
│   ├── preload/                # 预加载脚本
│   │   └── index.ts
│   │
│   └── renderer/               # 渲染进程代码
│       ├── index.html
│       ├── src/
│       │   ├── main.ts
│       │   ├── App.vue
│       │   ├── components/
│       │   ├── views/
│       │   ├── stores/
│       │   └── utils/
│       └── vite.config.ts

├── resources/                  # 静态资源
│   ├── icons/
│   └── assets/

└── scripts/                    # 构建脚本
    ├── build.js
    └── dev.js

功能模块结构

plaintext
src/
├── main/
│   ├── ipc/
│   │   ├── index.ts            # IPC 注册入口
│   │   ├── handlers/           # 按功能划分
│   │   │   ├── dialog.ts
│   │   │   ├── file.ts
│   │   │   ├── store.ts
│   │   │   └── window.ts
│   │   └── types.ts            # IPC 类型定义
│   │
│   ├── services/
│   │   ├── window.ts           # 窗口管理
│   │   ├── tray.ts             # 托盘管理
│   │   ├── menu.ts             # 菜单管理
│   │   ├── shortcut.ts         # 快捷键
│   │   ├── update.ts           # 自动更新
│   │   └── store.ts            # 数据存储
│   │
│   └── utils/
│       ├── path.ts             # 路径工具
│       ├── logger.ts           # 日志工具
│       └── security.ts         # 安全工具

├── preload/
│   ├── index.ts                # 入口
│   ├── apis/                   # 暴露的 API
│   │   ├── dialog.ts
│   │   ├── file.ts
│   │   └── window.ts
│   └── types.ts                # 类型定义

└── renderer/
    └── src/
        ├── services/           # 封装 Electron API
        │   ├── electron.ts
        │   └── storage.ts
        └── stores/             # 状态管理
            ├── app.ts
            └── settings.ts

分层架构

主进程架构

typescript
// src/main/index.ts
import { app, BrowserWindow } from 'electron'
import { registerIPCHandlers } from './ipc'
import { WindowService } from './services/window'
import { TrayService } from './services/tray'
import { StoreService } from './services/store'
 
class Application {
  private windowService: WindowService
  private trayService: TrayService
  private storeService: StoreService
 
  constructor() {
    this.windowService = new WindowService()
    this.trayService = new TrayService()
    this.storeService = new StoreService()
  }
 
  async init() {
    await app.whenReady()
    
    // 初始化服务
    await this.storeService.init()
    
    // 注册 IPC 处理器
    registerIPCHandlers(this.storeService)
    
    // 创建窗口
    this.windowService.createMainWindow()
    
    // 创建托盘
    this.trayService.create()
    
    // 设置监听
    this.setupListeners()
  }
 
  private setupListeners() {
    app.on('activate', () => {
      this.windowService.showMainWindow()
    })
 
    app.on('window-all-closed', () => {
      if (process.platform !== 'darwin') {
        app.quit()
      }
    })
  }
}
 
new Application().init()

IPC 层

typescript
// src/main/ipc/index.ts
import { ipcMain } from 'electron'
import type { StoreService } from '../services/store'
 
export function registerIPCHandlers(store: StoreService) {
  // 注册所有处理器
  ipcMain.handle('store:get', (_, key) => store.get(key))
  ipcMain.handle('store:set', (_, key, value) => store.set(key, value))
  
  // ... 其他处理器
}
typescript
// src/main/ipc/handlers/dialog.ts
import { ipcMain, dialog } from 'electron'
 
export function registerDialogHandlers() {
  ipcMain.handle('dialog:openFile', async (_, options) => {
    const result = await dialog.showOpenDialog(options)
    return result.filePaths
  })
 
  ipcMain.handle('dialog:saveFile', async (_, options) => {
    const result = await dialog.showSaveDialog(options)
    return result.filePath
  })
}

服务层

typescript
// src/main/services/window.ts
import { BrowserWindow, app } from 'electron'
import path from 'path'
 
export class WindowService {
  private mainWindow: BrowserWindow | null = null
 
  createMainWindow() {
    this.mainWindow = new BrowserWindow({
      width: 1200,
      height: 800,
      webPreferences: {
        preload: path.join(__dirname, '../preload/index.js'),
        contextIsolation: true,
        nodeIntegration: false
      }
    })
 
    this.mainWindow.loadFile('index.html')
    return this.mainWindow
  }
 
  showMainWindow() {
    if (this.mainWindow) {
      this.mainWindow.show()
      this.mainWindow.focus()
    } else {
      this.createMainWindow()
    }
  }
 
  hideMainWindow() {
    this.mainWindow?.hide()
  }
 
  getMainWindow() {
    return this.mainWindow
  }
}

模块通信

事件总线

typescript
// src/main/utils/eventBus.ts
import EventEmitter from 'events'
 
export const eventBus = new EventEmitter()
 
// 使用
eventBus.emit('file:changed', filePath)
eventBus.on('file:changed', (path) => {
  // 处理文件变化
})

依赖注入

typescript
// src/main/container.ts
import { StoreService } from './services/store'
import { WindowService } from './services/window'
import { TrayService } from './services/tray'
 
export class ServiceContainer {
  private services = new Map()
 
  register<T>(name: string, instance: T) {
    this.services.set(name, instance)
  }
 
  get<T>(name: string): T {
    return this.services.get(name)
  }
}
 
// 初始化
const container = new ServiceContainer()
const storeService = new StoreService()
container.register('store', storeService)
container.register('window', new WindowService(storeService))

配置管理

环境配置

typescript
// src/main/config/index.ts
import path from 'path'
import { app } from 'electron'
 
const isDev = process.env.NODE_ENV === 'development'
 
export const config = {
  isDev,
  
  paths: {
    userData: app.getPath('userData'),
    logs: path.join(app.getPath('userData'), 'logs'),
    temp: app.getPath('temp')
  },
  
  app: {
    name: 'My Electron App',
    version: app.getVersion()
  },
  
  window: {
    width: 1200,
    height: 800,
    minWidth: 800,
    minHeight: 600
  },
  
  api: {
    baseUrl: isDev ? 'http://localhost:3000' : 'https://api.example.com'
  }
}

用户配置

typescript
// src/main/services/store.ts
import Store from 'electron-store'
 
interface UserConfig {
  theme: 'light' | 'dark'
  language: string
  autoStart: boolean
  shortcuts: Record<string, string>
}
 
export class StoreService {
  private store: Store<UserConfig>
 
  constructor() {
    this.store = new Store<UserConfig>({
      defaults: {
        theme: 'light',
        language: 'zh-CN',
        autoStart: false,
        shortcuts: {
          'toggle-window': 'CommandOrControl+Shift+Space'
        }
      }
    })
  }
 
  get<K extends keyof UserConfig>(key: K): UserConfig[K] {
    return this.store.get(key)
  }
 
  set<K extends keyof UserConfig>(key: K, value: UserConfig[K]) {
    this.store.set(key, value)
  }
 
  getAll() {
    return this.store.store
  }
}

构建配置

electron-vite 配置

typescript
// electron.vite.config.ts
import { defineConfig } from 'electron-vite'
import vue from '@vitejs/plugin-vue'
 
export default defineConfig({
  main: {
    build: {
      rollupOptions: {
        input: {
          index: './src/main/index.ts'
        }
      }
    }
  },
  
  preload: {
    build: {
      rollupOptions: {
        input: {
          index: './src/preload/index.ts'
        }
      }
    }
  },
  
  renderer: {
    root: './src/renderer',
    plugins: [vue()],
    build: {
      rollupOptions: {
        input: {
          index: './src/renderer/index.html'
        }
      }
    }
  }
})

错误处理机制

全局错误捕获

typescript
// src/main/utils/errorHandler.ts
import { dialog } from 'electron'
import { logger } from './logger'
 
export function setupErrorHandler() {
  // 捕获未处理的 Promise 拒绝
  process.on('unhandledRejection', (reason, promise) => {
    logger.error('Unhandled Rejection:', reason)
    dialog.showErrorBox('错误', `发生未处理的异常: ${reason}`)
  })
 
  // 捕获未捕获的异常
  process.on('uncaughtException', (error) => {
    logger.error('Uncaught Exception:', error)
    dialog.showErrorBox('错误', `发生未捕获的异常: ${error.message}`)
  })
}
 
// 在主进程入口调用
import { setupErrorHandler } from './utils/errorHandler'
setupErrorHandler()

渲染进程错误处理

typescript
// src/renderer/src/utils/errorHandler.ts
import { ElMessage } from 'element-plus'
 
export function setupRendererErrorHandler() {
  window.addEventListener('error', (event) => {
    console.error('Renderer Error:', event.error)
    ElMessage.error(`发生错误: ${event.error.message}`)
    event.preventDefault()
  })
 
  window.addEventListener('unhandledrejection', (event) => {
    console.error('Unhandled Rejection:', event.reason)
    ElMessage.error(`异步操作失败: ${event.reason}`)
    event.preventDefault()
  })
}

IPC 错误处理

typescript
// src/main/ipc/handler.ts
import { ipcMain } from 'electron'
 
// 统一的错误处理包装器
export function wrapIPCHandler<T extends (...args: any[]) => Promise<any>>(
  handler: T
): T {
  return (async (...args: any[]) => {
    try {
      return await handler(...args)
    } catch (error: any) {
      logger.error('IPC Handler Error:', error)
      return {
        success: false,
        error: error.message || 'Unknown error'
      }
    }
  }) as T
}
 
// 使用示例
ipcMain.handle('file:read', wrapIPCHandler(async (_, filePath: string) => {
  const content = await fs.readFile(filePath, 'utf-8')
  return { success: true, data: content }
}))

日志系统

日志服务实现

typescript
// src/main/utils/logger.ts
import fs from 'fs'
import path from 'path'
import { app } from 'electron'
 
enum LogLevel {
  DEBUG = 0,
  INFO = 1,
  WARN = 2,
  ERROR = 3
}
 
class Logger {
  private logDir: string
  private logLevel: LogLevel
  private currentLogFile: string
 
  constructor() {
    this.logDir = path.join(app.getPath('userData'), 'logs')
    this.logLevel = process.env.NODE_ENV === 'development' 
      ? LogLevel.DEBUG 
      : LogLevel.INFO
    this.ensureLogDir()
    this.currentLogFile = this.getLogFileName()
  }
 
  private ensureLogDir() {
    if (!fs.existsSync(this.logDir)) {
      fs.mkdirSync(this.logDir, { recursive: true })
    }
  }
 
  private getLogFileName() {
    const date = new Date().toISOString().split('T')[0]
    return path.join(this.logDir, `app-${date}.log`)
  }
 
  private formatMessage(level: string, ...args: any[]) {
    const timestamp = new Date().toISOString()
    return `[${timestamp}] [${level}] ${args.map(this.stringify).join(' ')}`
  }
 
  private stringify(arg: any): string {
    if (typeof arg === 'object') {
      try {
        return JSON.stringify(arg, null, 2)
      } catch {
        return String(arg)
      }
    }
    return String(arg)
  }
 
  private writeLog(message: string) {
    fs.appendFileSync(this.currentLogFile, message + '\n')
  }
 
  debug(...args: any[]) {
    if (this.logLevel <= LogLevel.DEBUG) {
      const message = this.formatMessage('DEBUG', ...args)
      console.log('\x1b[36m%s\x1b[0m', message)
      this.writeLog(message)
    }
  }
 
  info(...args: any[]) {
    if (this.logLevel <= LogLevel.INFO) {
      const message = this.formatMessage('INFO', ...args)
      console.log('\x1b[32m%s\x1b[0m', message)
      this.writeLog(message)
    }
  }
 
  warn(...args: any[]) {
    if (this.logLevel <= LogLevel.WARN) {
      const message = this.formatMessage('WARN', ...args)
      console.log('\x1b[33m%s\x1b[0m', message)
      this.writeLog(message)
    }
  }
 
  error(...args: any[]) {
    if (this.logLevel <= LogLevel.ERROR) {
      const message = this.formatMessage('ERROR', ...args)
      console.log('\x1b[31m%s\x1b[0m', message)
      this.writeLog(message)
    }
  }
 
  // 清理旧日志(保留最近 7 天)
  cleanOldLogs() {
    const files = fs.readdirSync(this.logDir)
    const sevenDaysAgo = Date.now() - 7 * 24 * 60 * 60 * 1000
 
    files.forEach(file => {
      const filePath = path.join(this.logDir, file)
      const stat = fs.statSync(filePath)
      if (stat.mtimeMs < sevenDaysAgo) {
        fs.unlinkSync(filePath)
      }
    })
  }
}
 
export const logger = new Logger()

日志使用示例

typescript
import { logger } from './utils/logger'
 
logger.info('应用启动')
logger.debug('加载配置:', config)
logger.warn('配置项缺失,使用默认值')
logger.error('文件读取失败:', error)

安全最佳实践

安全配置清单

typescript
// src/main/services/window.ts
import { BrowserWindow } from 'electron'
 
export function createSecureWindow() {
  const win = new BrowserWindow({
    webPreferences: {
      // 启用上下文隔离(必须)
      contextIsolation: true,
      
      // 禁用 Node.js 集成(必须)
      nodeIntegration: false,
      
      // 启用沙箱
      sandbox: true,
      
      // 禁用远程模块
      enableRemoteModule: false,
      
      // 预加载脚本
      preload: path.join(__dirname, '../preload/index.js'),
      
      // 仅允许同源
      webSecurity: true,
      
      // 禁用危险的 webview
      webviewTag: false
    }
  })
 
  // 设置内容安全策略
  win.webContents.session.webRequest.onHeadersReceived((details, callback) => {
    callback({
      responseHeaders: {
        ...details.responseHeaders,
        'Content-Security-Policy': [
          "default-src 'self'; " +
          "script-src 'self'; " +
          "style-src 'self' 'unsafe-inline'; " +
          "img-src 'self' data: https:; " +
          "connect-src 'self' https://api.example.com;"
        ]
      }
    })
  })
 
  return win
}

安全的 IPC 通信

typescript
// src/preload/index.ts
import { contextBridge, ipcRenderer } from 'electron'
 
// 定义允许的 API
const allowedChannels = {
  invoke: ['dialog:open', 'file:read', 'file:write'],
  send: ['notification:show'],
  on: ['app:update', 'file:changed']
}
 
const electronAPI = {
  invoke: (channel: string, ...args: any[]) => {
    if (allowedChannels.invoke.includes(channel)) {
      return ipcRenderer.invoke(channel, ...args)
    }
    throw new Error(`Channel ${channel} is not allowed`)
  },
 
  send: (channel: string, ...args: any[]) => {
    if (allowedChannels.send.includes(channel)) {
      ipcRenderer.send(channel, ...args)
    }
  },
 
  on: (channel: string, callback: (...args: any[]) => void) => {
    if (allowedChannels.on.includes(channel)) {
      const subscription = (_event: any, ...args: any[]) => callback(...args)
      ipcRenderer.on(channel, subscription)
      return () => ipcRenderer.removeListener(channel, subscription)
    }
    throw new Error(`Channel ${channel} is not allowed`)
  }
}
 
contextBridge.exposeInMainWorld('electronAPI', electronAPI)

最佳实践

  1. 职责分离:主进程、渲染进程、preload 脚本各司其职
  2. 模块化:按功能划分模块,便于维护
  3. 类型安全:使用 TypeScript 定义类型
  4. 配置分离:区分开发/生产环境配置
  5. 错误处理:统一的错误处理机制
  6. 日志记录:完善的日志系统

参考链接