什么是 Electron?

Electron 是一个用于构建跨平台桌面应用的框架。它结合了 Chromium 和 Node.js,使开发者可以使用 Web 技术(HTML、CSS、JavaScript)来构建桌面应用。

Electron Components

如上图所示,Electron 由 Chromium、Node.js 和 Native API 组成。

Electron 的架构

Electron 的架构分为主进程和渲染进程。主进程负责管理应用的生命周期和窗口创建,而渲染进程负责显示网页内容。

Electron Architecture

主进程

  • Main Native API: 处理系统级别的操作。
  • Node.js: 提供服务器端功能。

渲染进程

  • Web 页面: 使用 HTML、CSS、JavaScript 构建。
  • Node.js: 提供服务器端功能。
  • Webkit: 渲染网页内容。

进程间通信 (IPC)

主进程和渲染进程之间通过 IPC (Inter-Process Communication) 进行通信。

IPC Communication

  • 渲染进程: 使用 ipcRenderer 进行通信。
  • 主进程: 使用 ipcMain 进行通信。

使用 Electron-Vite 构建项目

Electron-Vite 是一个集成了 Vite 的工具,简化了 Electron 应用的构建过程。

步骤

  1. 安装 Electron-Vite 使用 pnpm 安装 Electron-Vite:
    pnpm create @quick-start/electron
    
  2. 创建项目 然后按照提示操作即可!

✔ Project name: … ✔ Select a framework: › vue ✔ Add TypeScript? … No / Yes ✔ Add Electron updater plugin? … No / Yes ✔ Enable Electron download mirror proxy? … No / Yes Scaffolding project in ./... Done.


3. **开发模式**

启动开发服务器:

```bash
npm run dev
  1. 构建应用

    构建生产版本:

    npm run build
    

进程间通信

1. 主进程与渲染进程的角色

主进程 (Main Process)

  • 只有一个,是应用程序的入口点
  • 可以访问原生 GUI
  • 管理所有渲染进程
  • 可以访问 Node.js API
  • 控制应用程序的生命周期

渲染进程 (Renderer Process)

  • 可以有多个,每个对应一个窗口
  • 运行网页内容
  • 无法直接访问原生 GUI
  • 默认沙箱化环境
  • 通过 IPC 与主进程通信

2. IPC 通信详解

从渲染进程到主进程 (Renderer to Main)

  1. 单向通信 (send)
// 渲染进程发送
ipcRenderer.send('message-to-main', data)

// 主进程接收
ipcMain.on('message-to-main', (event, data) => {
    console.log(data)
})
  1. 请求响应模式 (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. 通信最佳实践

  1. 类型安全
interface IpcChannels {
    'user-login': { username: string; password: string }
    'login-result': { success: boolean; token?: string }
}
  1. 错误处理
try {
    const result = await ipcRenderer.invoke('async-operation')
} catch (error) {
    console.error('IPC 调用失败:', error)
}
  1. 安全考虑
// 限制暴露的 API
contextBridge.exposeInMainWorld('secureAPI', {
    // 只暴露安全的方法
    sendMessage: (message) => {
        // 验证消息格式
        if (isValidMessage(message)) {
            ipcRenderer.send('secure-channel', message)
        }
    }
})
  1. 性能优化
// 使用节流防止过多IPC调用
const throttledSend = _.throttle((data) => {
    ipcRenderer.send('frequent-update', data)
}, 100)

5. 调试技巧

  1. 监听所有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. 运行应用

  1. 安装依赖:
npm install
  1. 开发模式运行:
npm run dev
  1. 构建应用:
npm run build