项目架构设计
本文介绍 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)最佳实践
- 职责分离:主进程、渲染进程、preload 脚本各司其职
- 模块化:按功能划分模块,便于维护
- 类型安全:使用 TypeScript 定义类型
- 配置分离:区分开发/生产环境配置
- 错误处理:统一的错误处理机制
- 日志记录:完善的日志系统