从架构设计到部署发布,全面解析桌面应用开发最佳实践
您好!作为一名资深全栈工程师和桌面应用开发专家,我将为您呈现一份**完整、系统且具有生产实践指导意义的 Electron + Vue 3 桌面应用开发教程**。这份教程不仅覆盖您提及的所有核心点,更会融入我在实际项目中沉淀下来的最佳实践、架构思考、性能优化深度解析、版本管理以及部署策略,助您构建出高质量、高性能的跨平台桌面应用。
本教程将引导您从零开始,理解 Electron 的设计哲学,学习如何结合现代前端框架 Vue 3 高效开发,并掌握交付可维护、可扩展桌面应用的关键技能。
Electron 并非简单地将网页打包成桌面应用,其背后是精妙的多进程架构,这是理解一切的基础。
fs, path, http, child_process 等)和 Electron 的内置模块(app, BrowserWindow, ipcMain, Menu, dialog 等)。BrowserWindow 实例内部都运行一个独立的 Chromium 浏览器实例,负责渲染网页内容(HTML, CSS, JavaScript),呈现用户界面。主进程和渲染进程之间无法直接共享内存或调用函数,必须通过 IPC 机制进行通信。
ipcMain (主进程) / ipcRenderer (渲染进程): Electron 提供的核心 IPC 模块。
ipcRenderer.send(channel, args...) → ipcMain.on(channel, (event, args...) => {}):渲染进程向主进程发送单向消息。ipcRenderer.invoke(channel, args...) → ipcMain.handle(channel, async (event, args...) => {}):渲染进程调用主进程函数,并等待返回结果。**这是官方推荐且最安全的双向通信方式。**mainWindow.webContents.send(channel, args...) → ipcRenderer.on(channel, (event, args...) => {}):主进程向特定渲染进程发送消息。contextBridge.exposeInMainWorld(),它可以安全地将主进程的功能(通过 ipcRenderer 封装)暴露给渲染进程的 window 对象,同时保持渲染进程的沙盒化。window 对象。这对于安全性至关重要,防止恶意脚本注入。
我们将采用现代、高效的构建工具 Vite,结合 electron-builder 进行项目初始化。
# 1. 创建 Vue 3 项目 (使用 Vite)
npm create vue@latest my-electron-app -- --typescript # 可选 TypeScript
cd my-electron-app
npm install
# 2. 安装 Electron 和构建工具
npm install --save-dev electron electron-builder concurrently wait-on
# 3. 推荐安装一些辅助开发工具
# electron-devtools-installer: 在 Electron 应用中安装 Chrome 开发者工具扩展(如Vue Devtools)
npm install --save-dev electron-devtools-installer
一个清晰的目录结构对于大型项目至关重要。
my-electron-app/
├── public/ # 静态资源,直接拷贝到生产环境
├── src/ # Vue 3 前端代码
│ ├── assets/
│ ├── components/
│ ├── router/
│ ├── stores/ # Vuex 或 Pinia 状态管理
│ ├── App.vue
│ ├── main.ts # Vue 应用入口
│ └── shims-vue.d.ts # TypeScript 类型声明
├── electron/ # Electron 主进程及相关配置
│ ├── main.ts # 主进程入口
│ ├── preload.ts # 预加载脚本
│ └── utils/ # 主进程工具函数 (如文件操作、数据库连接)
│ └── db.ts # 示例:SQLite 数据库操作
│ └── update.ts # 自动更新相关逻辑
├── build/ # 存放应用图标等构建资源
│ ├── icon.ico
│ └── icon.icns
├── package.json # 项目配置
├── tsconfig.json # TypeScript 配置
├── vite.config.ts # Vite 构建配置
└── .env # 环境变量
package.json
{
"name": "my-electron-app",
"version": "1.0.0",
"main": "electron/main.js", // 注意:这里通常指向编译后的JS文件
"scripts": {
"dev": "vite", // 启动 Vue 开发服务器
"build:vue": "vue-tsc --noEmit && vite build", // 构建 Vue 生产包
"build:electron": "tsc --project electron", // 编译 Electron 主进程和预加载脚本
"electron:start": "npm run build:vue && npm run build:electron && electron .", // 运行 Electron (生产模式)
"electron:dev": "concurrently \"npm run dev\" \"wait-on http://localhost:5173 && npm run build:electron && electron .\"", // 开发模式
"electron:build": "npm run build:vue && npm run build:electron && electron-builder", // 打包应用
"postinstall": "electron-builder install-app-deps", // 安装Electron相关依赖
"test": "vitest", // 单元测试,可选
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
},
"devDependencies": {
"electron": "^28.0.0",
"electron-builder": "^24.0.0",
"concurrently": "^8.0.0",
"wait-on": "^7.0.0",
"electron-devtools-installer": "^3.2.0",
"@vitejs/plugin-vue": "^4.0.0",
"vite": "^5.0.0",
"vue": "^3.0.0",
"typescript": "^5.0.0",
"@types/node": "^20.0.0",
# ...其他如 ESLint, Prettier, Vitest 等
},
"build": {
"appId": "com.yourcompany.yourappname",
"productName": "My Awesome App",
"copyright": "Copyright © 2023 ${author}",
"directories": {
"output": "dist-electron" // 打包输出目录
},
"files": [
"dist/**/*", // Vue 构建后的前端文件
"electron/**/*.js", // 编译后的主进程/预加载JS文件
"electron/**/*.json" // 如果有配置文件
],
"asar": true, // 推荐:打包成asar格式,保护源码并加速文件读取
"win": {
"target": [
{ "target": "nsis", "arch": ["x64"] } // Windows 64位安装包
],
"icon": "build/icon.ico"
},
"mac": {
"target": [
"dmg"
],
"icon": "build/icon.icns"
},
"linux": {
"target": [
"AppImage",
"deb"
],
"icon": "build/icon.png"
},
"publish": [ // 自动更新配置,此处以 GitHub Releases 为例
{
"provider": "github",
"owner": "your-github-username",
"repo": "your-github-repo"
}
]
}
}
vite.config.ts
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { resolve } from 'path';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
base: './', // 确保生产构建的相对路径正确,Electron加载本地文件需要
build: {
outDir: 'dist', // Vue 构建输出目录
rollupOptions: {
input: {
main: resolve(__dirname, 'index.html')
}
}
},
resolve: {
alias: {
'@': resolve(__dirname, './src'), // 配置路径别名
},
},
server: {
port: 5173, // Vite 开发服务器端口
}
});
electron/main.ts (主进程)
import { app, BrowserWindow, ipcMain, screen, Menu, dialog } from 'electron';
import path from 'path';
import installExtension, { VUEJS3_DEVTOOLS } from 'electron-devtools-installer';
import { handleIPC } from './utils/ipcHandlers'; // 集中管理IPC处理
import { initAutoUpdater } from './utils/update'; // 自动更新逻辑
let mainWindow: BrowserWindow | null;
const isDev = process.env.NODE_ENV === 'development';
const VITE_DEV_SERVER_URL = 'http://localhost:5173'; // Vite 开发服务器地址
function createMainWindow() {
const primaryDisplay = screen.getPrimaryDisplay();
const { width, height } = primaryDisplay.workAreaSize; // 获取屏幕工作区域大小
mainWindow = new BrowserWindow({
width: Math.min(1200, width * 0.8), // 初始窗口大小
height: Math.min(800, height * 0.8),
minWidth: 800,
minHeight: 600,
show: false, // 先隐藏,待内容加载完毕再显示
frame: true, // 是否显示系统默认边框,可设置为false自定义
webPreferences: {
preload: path.join(__dirname, 'preload.js'), // 预加载脚本
nodeIntegration: false, // 禁用 Node.js 集成,必须!
contextIsolation: true, // 启用上下文隔离,必须!
webSecurity: !isDev, // 开发时可关闭,生产务必开启
// allowRunningInsecureContent: isDev // 不推荐,仅极端开发情况
}
});
if (isDev) {
mainWindow.loadURL(VITE_DEV_SERVER_URL);
// 打开开发者工具并安装Vue Devtools
mainWindow.webContents.once('dom-ready', async () => {
try {
await installExtension(VUEJS3_DEVTOOLS);
mainWindow?.webContents.openDevTools();
} catch (e) {
console.error('Failed to install Vue Devtools:', e);
}
});
} else {
mainWindow.loadFile(path.join(__dirname, '../dist/index.html'));
}
// 内容加载完毕后显示窗口,优化启动体验
mainWindow.webContents.on('did-finish-load', () => {
mainWindow?.show();
});
// 窗口关闭事件
mainWindow.on('closed', () => {
mainWindow = null;
});
// 创建应用菜单 (可选)
const template: Electron.MenuItemConstructorOptions[] = [
{
label: '文件',
submenu: [
{ label: '新建', accelerator: 'CmdOrCtrl+N', click: () => console.log('New File') },
{ type: 'separator' },
{ label: '退出', role: 'quit' }
]
},
// ...其他菜单项
];
const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);
}
// Electron 应用准备就绪
app.whenReady().then(() => {
createMainWindow();
handleIPC(ipcMain, mainWindow); // 注册所有IPC处理函数
if (process.env.NODE_ENV === 'production') {
initAutoUpdater(mainWindow); // 生产环境检查更新
}
app.on('activate', () => {
// macOS 上当点击 dock 图标时,如果没有其他窗口打开则重新创建一个
if (BrowserWindow.getAllWindows().length === 0) {
createMainWindow();
}
});
});
// 所有窗口关闭时退出应用 (macOS 除外)
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
// 在 Electron 进程中捕获未处理的异常
process.on('uncaughtException', (error) => {
console.error('主进程未捕获的异常:', error);
dialog.showErrorBox('应用错误', '发生了一个未预期的错误,应用将退出。' + error.message);
app.quit();
});
electron/preload.ts (预加载脚本)安全地暴露主进程功能给渲染进程。
import { contextBridge, ipcRenderer } from 'electron';
// 定义需要暴露给渲染进程的 API 接口
export interface ElectronAPI {
// 双向通信 (渲染 -> 主,带返回值)
selectFile: () => Promise<string | undefined>;
saveFile: (content: string) => Promise<boolean>;
// 单向通信 (渲染 -> 主,无返回值)
showNotification: (message: string) => void;
// 单向通信 (主 -> 渲染,监听器)
onUpdateProgress: (callback: (progress: number) => void) => () => void; // 返回一个取消监听的函数
// 自动更新相关
onUpdateStatus: (callback: (status: string) => void) => () => void;
restartApp: () => void;
}
const electronAPI: ElectronAPI = {
selectFile: () => ipcRenderer.invoke('dialog:openFile'),
saveFile: (content: string) => ipcRenderer.invoke('dialog:saveFile', content),
showNotification: (message: string) => ipcRenderer.send('app:showNotification', message),
onUpdateProgress: (callback: (progress: number) => void) => {
const handler = (event: Electron.IpcRendererEvent, progress: number) => callback(progress);
ipcRenderer.on('main:updateProgress', handler);
return () => ipcRenderer.off('main:updateProgress', handler); // 返回取消函数
},
onUpdateStatus: (callback: (status: string) => void) => {
const handler = (event: Electron.IpcRendererEvent, status: string) => callback(status);
ipcRenderer.on('update-status', handler);
return () => ipcRenderer.off('update-status', handler);
},
restartApp: () => ipcRenderer.send('app:restart')
};
// 将 API 暴露到全局 window 对象,名称为 electronAPI
contextBridge.exposeInMainWorld('electronAPI', electronAPI);
electron/utils/ipcHandlers.ts (IPC 集中管理)将所有的 IPC 监听逻辑集中在一个文件,方便管理和维护。
import { ipcMain, dialog, Notification, app, BrowserWindow } from 'electron';
import path from 'path';
import fs from 'fs';
export function handleIPC(ipcMainInstance: typeof ipcMain, mainWindow: BrowserWindow | null) {
// 监听打开文件对话框
ipcMainInstance.handle('dialog:openFile', async (event) => {
const { canceled, filePaths } = await dialog.showOpenDialog(mainWindow!, {
properties: ['openFile'],
filters: [{ name: 'Text Files', extensions: ['txt', 'md'] }]
});
if (!canceled && filePaths.length > 0) {
const content = fs.readFileSync(filePaths[0], 'utf8');
return content;
}
return undefined;
});
// 监听保存文件对话框
ipcMainInstance.handle('dialog:saveFile', async (event, content: string) => {
const { canceled, filePath } = await dialog.showSaveDialog(mainWindow!, {
filters: [{ name: 'Text Files', extensions: ['txt', 'md'] }]
});
if (!canceled && filePath) {
fs.writeFileSync(filePath, content, 'utf8');
return true;
}
return false;
});
// 监听显示通知
ipcMainInstance.on('app:showNotification', (event, message: string) => {
if (Notification.isSupported()) {
new Notification({
title: 'Electron App Notification',
body: message
}).show();
} else {
console.log('系统不支持通知:', message);
}
});
// 监听重启应用
ipcMainInstance.on('app:restart', () => {
app.relaunch({ args: process.argv.slice(1).concat(['--relaunch']) });
app.exit(0);
});
// 示例:主进程主动发送消息给渲染进程 (模拟进度更新)
let progressInterval: NodeJS.Timeout | null = null;
ipcMainInstance.on('main:startProgressUpdate', (event, start: boolean) => {
if (start) {
let progress = 0;
progressInterval = setInterval(() => {
progress += 10;
if (mainWindow) {
mainWindow.webContents.send('main:updateProgress', progress);
}
if (progress >= 100) {
clearInterval(progressInterval!);
progressInterval = null;
if (mainWindow) {
mainWindow.webContents.send('main:updateProgress', 100); // 确保发送100%
}
}
}, 500);
} else if (progressInterval) {
clearInterval(progressInterval);
progressInterval = null;
}
});
}
在 Vue 3 应用中,通过预加载脚本暴露的 window.electronAPI 来与主进程交互。
<template>
<h1>Electron + Vue 3 应用</h1>
<button @click="handleSelectFile">选择文件</button>
<p v-if="selectedFileContent">文件内容:{{ selectedFileContent.substring(0, 100) }}...</p>
<button @click="handleSaveFile">保存文件</button>
<button @click="handleShowNotification">显示通知</button>
<h3>主进程进度更新:</h3>
<p>当前进度: {{ progress }}%</p>
<button @click="toggleProgressUpdate">{{ isUpdatingProgress ? '停止更新' : '开始更新' }}</button>
<h3>应用更新状态:</h3>
<p>{{ updateStatus }}</p>
<button v-if="updateStatus.includes('下载完毕')" @click="handleRestartApp">立即重启安装</button>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
import type { ElectronAPI } from '../electron/preload'; // 导入类型声明
// 确保在 Electron 环境下才能访问 electronAPI
declare global {
interface Window {
electronAPI?: ElectronAPI;
}
}
const selectedFileContent = ref<string | undefined>(undefined);
const progress = ref(0);
const isUpdatingProgress = ref(false);
const updateStatus = ref('未检查更新');
// 选择文件
const handleSelectFile = async () => {
if (window.electronAPI) {
const content = await window.electronAPI.selectFile();
if (content) {
selectedFileContent.value = content;
window.electronAPI.showNotification('文件已成功读取!');
} else {
selectedFileContent.value = '未选择文件或读取失败。';
}
} else {
console.warn('Electron API 不可用');
}
};
// 保存文件
const handleSaveFile = async () => {
if (window.electronAPI) {
const success = await window.electronAPI.saveFile('这是一段要保存到文件的内容。');
if (success) {
window.electronAPI.showNotification('文件已成功保存!');
} else {
window.electronAPI.showNotification('文件保存失败。');
}
}
};
// 显示通知
const handleShowNotification = () => {
if (window.electronAPI) {
window.electronAPI.showNotification('这是一条来自Vue的通知!');
}
};
// 主进程进度更新控制
const toggleProgressUpdate = () => {
if (window.electronAPI) {
isUpdatingProgress.value = !isUpdatingProgress.value;
// 通过 IPC 向主进程发送控制信号
if (isUpdatingProgress.value) {
window.electronAPI.showNotification('开始模拟进度更新...');
window.electronAPI.onUpdateProgress((p: number) => {
progress.value = p;
if (p === 100) {
isUpdatingProgress.value = false;
window.electronAPI?.showNotification('进度更新完成!');
}
});
// 告诉主进程开始发送更新消息
window.electronAPI.onUpdateProgress(() => {}); // 占位,实际调用主进程方法来启动
ipcRenderer.send('main:startProgressUpdate', true); // 主进程需要监听这个事件
} else {
window.electronAPI.showNotification('停止模拟进度更新。');
ipcRenderer.send('main:startProgressUpdate', false);
}
}
};
// 应用更新相关
const handleRestartApp = () => {
if (window.electronAPI) {
window.electronAPI.restartApp();
}
};
onMounted(() => {
// 监听自动更新状态
if (window.electronAPI) {
window.electronAPI.onUpdateStatus((status: string) => {
updateStatus.value = status;
});
}
});
onUnmounted(() => {
// 清理监听器 (如果 onUpdateStatus 返回了取消函数)
// 例如:const unsubscribe = window.electronAPI.onUpdateStatus(...)
// unsubscribe();
});
</script>
localStorage (主进程或渲染进程), 或通过 IPC 存储到主进程的文件/数据库。Electron 应用的性能优化是生产级应用的关键。它涉及多个层面,从代码编写到构建配置。
show: false + did-finish-load: 在 BrowserWindow 创建时设置 show: false,然后在 webContents.on('did-finish-load') 事件中调用 mainWindow.show()。这能避免用户看到空白窗口。nodeIntegration: false 和 contextIsolation: true 是强制开启的安全和性能优化选项。它们将渲染进程沙盒化,防止内存泄漏和全局变量污染。ipcRenderer.sendSync() 会阻塞主进程直到收到回复,严重影响性能和用户体验,应尽量避免。使用 ipcRenderer.invoke() + ipcMain.handle() 进行异步双向通信。ipcRenderer.send/on (单向) 或 ipcRenderer.invoke/handle (双向)。Readable/Writable Stream 或共享内存(如 SharedArrayBuffer,但复杂且需小心使用),或者直接将数据写入文件后传递文件路径。BrowserWindow 创建隐藏的“工作窗口”来处理,而不是在用户可见的渲染进程中运行。.asar 归档: Electron-builder 默认将应用资源打包成 .asar 格式,这能提高文件读取速度、减小应用体积(尤其是Windows平台)并保护源代码。package.json 中的 dependencies 只包含生产环境真正需要的包,将开发工具放在 devDependencies。VITE_ENV=production),启用摇树优化 (Tree Shaking)、代码混淆和压缩。根据数据类型和需求,选择合适的存储方案:
fs 模块进行读写。通过 IPC 为渲染进程提供读写接口。better-sqlite3 或 sequelize (ORM) 等 npm 包进行操作。通过 IPC 暴露查询接口给渲染进程。localStorage / sessionStorage (渲染进程):
经验建议: 对于大多数应用,主进程 + SQLite 是管理本地结构化数据的最佳实践。配置、日志等少量数据可直接写入文件。渲染进程只通过 IPC 访问这些数据。
提供顺畅的应用升级体验是桌面应用长期维护的关键。
electron-updater)
electron-updater 是 autoUpdater 模块的高级封装,与 electron-builder 完美结合。
import { autoUpdater } from 'electron-updater';
import { dialog, BrowserWindow, app } from 'electron';
import log from 'electron-log'; // 推荐使用 electron-log 进行日志管理
autoUpdater.logger = log; // 设置日志输出
autoUpdater.logger.transports.file.level = 'info';
autoUpdater.autoDownload = true; // 默认自动下载新版本
export function initAutoUpdater(mainWindow: BrowserWindow | null) {
if (!mainWindow) return;
// 设置更新服务器地址 (如果不是GitHub Releases,需要配置)
// autoUpdater.setFeedURL({
// provider: 'generic', // 例如通用HTTP服务器
// url: 'http://your-update-server.com/updates/' + process.platform + '/' + app.getVersion()
// });
// 检查更新
const checkForUpdates = () => {
console.log('检查更新...');
mainWindow?.webContents.send('update-status', '正在检查更新...');
autoUpdater.checkForUpdates(); // 不需要 .then,监听事件即可
};
// 监听事件
autoUpdater.on('checking-for-update', () => {
mainWindow?.webContents.send('update-status', '正在检查更新...');
});
autoUpdater.on('update-available', (info) => {
log.info('发现新版本:', info.version);
mainWindow?.webContents.send('update-status', `发现新版本: ${info.version},正在下载...`);
});
autoUpdater.on('update-not-available', () => {
mainWindow?.webContents.send('update-status', '当前已是最新版本。');
});
autoUpdater.on('download-progress', (progressObj) => {
const message = `下载进度: ${progressObj.percent ? progressObj.percent.toFixed(2) : 0}% (${progressObj.transferred}/${progressObj.total})`;
log.info(message);
mainWindow?.webContents.send('update-status', message);
});
autoUpdater.on('update-downloaded', (info) => {
log.info('更新下载完毕,准备安装...', info);
mainWindow?.webContents.send('update-status', `新版本 ${info.version} 已下载,重启应用以安装。`);
dialog.showMessageBox(mainWindow!, {
type: 'info',
title: '应用更新',
message: `新版本 ${info.version} 已下载,是否立即安装并重启?`,
buttons: ['立即安装', '稍后']
}).then(result => {
if (result.response === 0) { // 用户选择立即安装
autoUpdater.quitAndInstall(); // 退出并安装
}
});
});
autoUpdater.on('error', (err) => {
log.error('更新出错:', err);
mainWindow?.webContents.send('update-status', `更新出错: ${err.message}`);
dialog.showErrorBox('更新失败', `检查更新时发生错误: ${err.message}`);
});
// 启动时延迟检查更新,避免阻塞应用启动
setTimeout(checkForUpdates, 5000);
}
版本发布与服务器:
electron-builder 打包生成的安装包和 latest.yml/latest-mac.yml 上传到 GitHub Releases。electron-updater 会自动检测。latest.yml/latest-mac.yml 文件可访问。electron-updater 会解析这些文件获取更新信息。将开发完成的 Electron 应用交付给用户。
electron-builder 打包
electron-builder 是功能强大的打包工具,支持跨平台打包(Windows, macOS, Linux)并生成各种安装格式。
# 打包所有平台
npm run electron:build
# 只打包 Windows
npm run electron:build --win
# 只打包 macOS
npm run electron:build --mac
# 只打包 Linux
npm run electron:build --linux
package.json 的 "build" 配置中指定图标路径。提供 .ico (Windows) 和 .icns (macOS) 格式。nsis (安装程序), portable (便携版), zip 等。dmg (磁盘映像), zip 等。AppImage (通用可执行文件), deb (Debian/Ubuntu), rpm (Red Hat/Fedora) 等。自动化构建、测试、打包和发布流程,是生产环境的最佳实践。
高效的调试和严谨的测试是保证应用质量的关键。
mainWindow.webContents.openDevTools())。.vscode/launch.json 中配置 Node.js 调试会话。electron-log: 一个优秀的日志库,支持多种传输,方便调试和生产环境日志记录。这份教程涵盖了从 Electron 基础、项目搭建、核心功能实现、进程间通信、深度性能优化、版本升级到部署发布的全链路知识。我希望它能为您在 Electron + Vue 3 的桌面应用开发之路上提供坚实的指导和宝贵的实践经验。
记住,桌面应用开发是一个系统工程,需要对前端、后端、操作系统特性甚至部署运维都有所了解。持续学习、勤于实践、保持对新技术的探索,您必将成为一名优秀的 Electron 开发者!祝您编码顺利,项目成功!