什么是 Electron?
Electron 是一个用于构建跨平台桌面应用的框架。它结合了 Chromium 和 Node.js,使开发者可以使用 Web 技术(HTML、CSS、JavaScript)来构建桌面应用。
如上图所示,Electron 由 Chromium、Node.js 和 Native API 组成。
Electron 的架构
Electron 的架构分为主进程和渲染进程。主进程负责管理应用的生命周期和窗口创建,而渲染进程负责显示网页内容。
主进程
- Main Native API: 处理系统级别的操作。
- Node.js: 提供服务器端功能。
渲染进程
- Web 页面: 使用 HTML、CSS、JavaScript 构建。
- Node.js: 提供服务器端功能。
- Webkit: 渲染网页内容。
进程间通信 (IPC)
主进程和渲染进程之间通过 IPC (Inter-Process Communication) 进行通信。
- 渲染进程: 使用
ipcRenderer
进行通信。 - 主进程: 使用
ipcMain
进行通信。
使用 Electron-Vite 构建项目
Electron-Vite 是一个集成了 Vite 的工具,简化了 Electron 应用的构建过程。
步骤
- 安装 Electron-Vite
使用 pnpm 安装 Electron-Vite:
pnpm create @quick-start/electron
- 创建项目
然后按照提示操作即可!
✔ Project name: …
3. **开发模式**
启动开发服务器:
```bash
npm run dev
-
构建应用
构建生产版本:
npm run build
进程间通信
1. 主进程与渲染进程的角色
主进程 (Main Process)
- 只有一个,是应用程序的入口点
- 可以访问原生 GUI
- 管理所有渲染进程
- 可以访问 Node.js API
- 控制应用程序的生命周期
渲染进程 (Renderer Process)
- 可以有多个,每个对应一个窗口
- 运行网页内容
- 无法直接访问原生 GUI
- 默认沙箱化环境
- 通过 IPC 与主进程通信
2. IPC 通信详解
从渲染进程到主进程 (Renderer to Main)
- 单向通信 (send)
// 渲染进程发送
ipcRenderer.send('message-to-main', data)
// 主进程接收
ipcMain.on('message-to-main', (event, data) => {
console.log(data)
})
- 请求响应模式 (invoke/handle)
// 渲染进程
const response = await ipcRenderer.invoke('async-message', data)
// 主进程
ipcMain.handle('async-message', async (event, data) => {
// 处理异步操作
return result
})
从主进程到渲染进程 (Main to Renderer)
// 主进程发送
mainWindow.webContents.send('message-to-renderer', data)
// 渲染进程接收
ipcRenderer.on('message-to-renderer', (event, data) => {
console.log(data)
})
3. 页面间通信
方式一:通过主进程中转
// 页面A发送消息
ipcRenderer.send('message-to-page-b', data)
// 主进程中转
ipcMain.on('message-to-page-b', (event, data) => {
// 转发给页面B
windowB.webContents.send('receive-from-page-a', data)
})
// 页面B接收消息
ipcRenderer.on('receive-from-page-a', (event, data) => {
console.log(data)
})
方式二:使用 contextBridge
// 预加载脚本中设置
contextBridge.exposeInMainWorld('electronAPI', {
sendMessage: (channel, data) => ipcRenderer.send(channel, data),
onMessage: (channel, func) => ipcRenderer.on(channel, func)
})
4. 通信最佳实践
- 类型安全
interface IpcChannels {
'user-login': { username: string; password: string }
'login-result': { success: boolean; token?: string }
}
- 错误处理
try {
const result = await ipcRenderer.invoke('async-operation')
} catch (error) {
console.error('IPC 调用失败:', error)
}
- 安全考虑
// 限制暴露的 API
contextBridge.exposeInMainWorld('secureAPI', {
// 只暴露安全的方法
sendMessage: (message) => {
// 验证消息格式
if (isValidMessage(message)) {
ipcRenderer.send('secure-channel', message)
}
}
})
- 性能优化
// 使用节流防止过多IPC调用
const throttledSend = _.throttle((data) => {
ipcRenderer.send('frequent-update', data)
}, 100)
5. 调试技巧
- 监听所有IPC通信
// 开发环境下监控所有IPC通信
if (process.env.NODE_ENV === 'development') {
ipcMain.on('*', (event, channel, ...args) => {
console.log(`IPC Channel: ${channel}`, args)
})
}
Demo案例:文件拖放查看器
这个案例展示了一个简单的文件拖放应用,包含了主进程和渲染进程的通信、文件系统操作等基本功能。
项目结构
file-viewer/
├── src/
│ ├── main/
│ │ └── index.ts
│ ├── renderer/
│ │ ├── index.html
│ │ ├── App.vue
│ │ └── style.css
│ └── preload/
│ └── index.ts
├── electron.vite.config.ts
└── package.json
1. 主进程代码
import { app, BrowserWindow, ipcMain } from 'electron'
import { join } from 'path'
import * as fs from 'fs/promises'
function createWindow() {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
nodeIntegration: false,
contextIsolation: true
}
})
if (process.env.VITE_DEV_SERVER_URL) {
win.loadURL(process.env.VITE_DEV_SERVER_URL)
} else {
win.loadFile(join(__dirname, '../renderer/index.html'))
}
// 处理文件读取请求
ipcMain.handle('read-file', async (_, filePath) => {
try {
const content = await fs.readFile(filePath, 'utf-8')
return { success: true, content }
} catch (error) {
return { success: false, error: error.message }
}
})
}
app.whenReady().then(createWindow)
2. 预加载脚本
import { contextBridge, ipcRenderer } from 'electron'
contextBridge.exposeInMainWorld('electronAPI', {
readFile: (filePath: string) => ipcRenderer.invoke('read-file', filePath),
handleDrop: (callback: (files: string[]) => void) => {
document.addEventListener('drop', (e) => {
e.preventDefault()
const files = Array.from(e.dataTransfer.files).map(f => f.path)
callback(files)
})
document.addEventListener('dragover', (e) => {
e.preventDefault()
})
}
})
3. 渲染进程代码
<template>
<div class="container" ref="dropZone">
<div class="drop-zone" :class="{ active: isDragging }">
<div v-if="!currentFile">
拖放文件到这里
</div>
<div v-else class="file-content">
<h3>{{ currentFile.name }}</h3>
<pre>{{ currentFile.content }}</pre>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { basename } from 'path'
const isDragging = ref(false)
const currentFile = ref<{ name: string; content: string } | null>(null)
// 初始化拖放处理
window.electronAPI.handleDrop(async (files) => {
if (files.length > 0) {
const filePath = files[0]
const result = await window.electronAPI.readFile(filePath)
if (result.success) {
currentFile.value = {
name: basename(filePath),
content: result.content
}
} else {
alert(`读取文件失败: ${result.error}`)
}
}
})
</script>
<style scoped>
.container {
height: 100vh;
padding: 20px;
}
.drop-zone {
border: 2px dashed #ccc;
border-radius: 8px;
padding: 20px;
text-align: center;
height: calc(100vh - 40px);
overflow: auto;
}
.drop-zone.active {
border-color: #42b983;
background: rgba(66, 185, 131, 0.1);
}
.file-content {
text-align: left;
}
pre {
background: #f8f8f8;
padding: 15px;
border-radius: 4px;
overflow: auto;
}
</style>
4. 类型声明
export interface ElectronAPI {
readFile: (filePath: string) => Promise<{
success: boolean;
content?: string;
error?: string;
}>;
handleDrop: (callback: (files: string[]) => void) => void;
}
declare global {
interface Window {
electronAPI: ElectronAPI;
}
}
5. 运行应用
- 安装依赖:
npm install
- 开发模式运行:
npm run dev
- 构建应用:
npm run build