Electron+Vite+Vue3:从零到一构建现代化桌面应用
1. 为什么选择Electron+Vite+Vue3组合?
在开始动手之前,我们先聊聊为什么这个技术组合如此受欢迎。我最初接触Electron开发时,用的是Webpack+Vue2的组合,每次保存代码都要等上好几秒才能看到效果,调试起来特别痛苦。后来尝试了Vite+Vue3,开发体验直接提升了好几个档次。
速度优势是Vite最明显的特性。传统的打包工具需要先打包整个应用才能启动开发服务器,而Vite直接利用浏览器原生ES模块支持,启动时间几乎可以忽略不计。在我的MacBook Pro上,一个中等规模的Electron项目用Webpack启动要15秒左右,而Vite只需要不到1秒。
Vue3的组合式API让代码组织更加灵活。特别是在Electron开发中,我们经常需要处理各种原生事件和状态管理,组合式API能让我们把相关逻辑集中在一起,而不是分散在多个选项块中。比如窗口大小变化的响应逻辑,可以封装成一个独立的useWindowSize组合函数。
Electron本身则解决了跨平台桌面应用的核心需求。我做过一个数据可视化工具,需要读取本地文件系统并展示复杂图表。用纯Web技术无法直接访问文件系统,而用传统原生开发又太复杂。Electron完美地结合了两者的优势,让我能用熟悉的Web技术实现桌面应用功能。
2. 环境准备与项目初始化
2.1 基础环境配置
首先确保你的开发环境满足以下要求:
- Node.js 18.x或更高版本(推荐使用nvm管理Node版本)
- npm 8.x或更高版本(或使用yarn、pnpm)
- 一个顺手的代码编辑器(VSCode是我的首选)
建议先配置好国内镜像源,可以大幅提升依赖安装速度:
npm config set registry https://registry.npmmirror.com/2.2 使用electron-vite创建项目
electron-vite是一个专门为Electron优化的构建工具,它简化了配置过程。我们用它来初始化项目:
npm create @quick-start/electron my-electron-app创建过程中会有几个选项需要选择:
- 选择框架时,选Vue
- TypeScript按需选择(本文示例用JavaScript)
- 建议启用Electron更新插件
- 国内用户一定要开启镜像代理
初始化完成后,目录结构是这样的:
my-electron-app ├── src │ ├── main # 主进程代码 │ ├── preload # 预加载脚本 │ └── renderer # 渲染进程(Vue项目) ├── electron.vite.config.mjs # 构建配置 └── package.json2.3 启动开发模式
进入项目目录并安装依赖:
cd my-electron-app npm install然后修改package.json,在dev脚本后添加--watch参数以启用主进程热更新:
"scripts": { "dev": "electron-vite dev --watch" }现在运行开发服务器:
npm run dev你会看到一个Electron窗口弹出,里面是Vue的欢迎页面。试着修改src/renderer/App.vue文件,保存后能看到即时更新的效果。
3. 核心概念与进程通信
3.1 Electron的进程模型
Electron应用由三种进程组成:
- 主进程:每个应用只有一个,负责创建窗口和管理应用生命周期
- 渲染进程:每个窗口对应一个,负责展示Web内容
- 预加载脚本:在渲染进程加载前执行,桥接主进程和渲染进程
理解这个模型很重要。我刚开始时经常混淆各个进程的能力边界,比如试图在渲染进程直接调用Node.js API,结果遇到各种问题。
3.2 进程间通信实践
让我们实现一个实用功能:在Vue组件中点击按钮,让主进程弹出一个原生对话框。
首先在预加载脚本(src/preload/index.js)中暴露API:
const { contextBridge, ipcRenderer } = require('electron') contextBridge.exposeInMainWorld('electronAPI', { openDialog: () => ipcRenderer.invoke('dialog:open') })然后在主进程(src/main/index.js)中添加处理程序:
const { ipcMain, dialog } = require('electron') ipcMain.handle('dialog:open', async () => { const { filePaths } = await dialog.showOpenDialog({ properties: ['openFile'] }) return filePaths[0] || null })最后在Vue组件中使用:
<template> <button @click="handleOpen">选择文件</button> </template> <script setup> const handleOpen = async () => { const filePath = await window.electronAPI.openDialog() if (filePath) { console.log('选择的文件:', filePath) } } </script>这种模式是Electron开发中最常用的通信方式。记住几个要点:
- 预加载脚本使用contextBridge安全地暴露API
- 主进程用ipcMain.handle处理调用
- 渲染进程通过window对象访问暴露的API
4. 完善Vue开发环境
4.1 添加Vue全家桶
虽然electron-vite已经配置了Vue3,但我们还需要一些常用工具:
安装路由和状态管理:
npm install vue-router pinia配置路由(src/renderer/router/index.js):
import { createRouter, createWebHashHistory } from 'vue-router' const routes = [ { path: '/', component: () => import('../views/Home.vue') }, { path: '/about', component: () => import('../views/About.vue') } ] export const router = createRouter({ history: createWebHashHistory(), routes })配置Pinia(src/renderer/stores/index.js):
import { defineStore } from 'pinia' export const useAppStore = defineStore('app', { state: () => ({ count: 0 }), actions: { increment() { this.count++ } } })在main.js中安装它们:
import { createApp } from 'vue' import App from './App.vue' import { router } from './router' import { createPinia } from 'pinia' const app = createApp(App) app.use(router) app.use(createPinia()) app.mount('#app')4.2 样式与工具类配置
我习惯使用SCSS和TailwindCSS。先安装必要的依赖:
npm install -D sass tailwindcss postcss autoprefixer npx tailwindcss init配置tailwind.config.js:
module.exports = { content: [ './src/renderer/**/*.{vue,js,ts,jsx,tsx}' ], theme: { extend: {}, }, plugins: [], }然后在src/renderer/assets目录下创建main.scss:
@tailwind base; @tailwind components; @tailwind utilities; /* 自定义样式 */最后在main.js中引入:
import './assets/main.scss'5. 窗口管理与原生功能
5.1 自定义窗口行为
Electron允许我们完全控制窗口的外观和行为。修改主进程代码创建一个自定义窗口:
const createWindow = () => { const win = new BrowserWindow({ width: 1200, height: 800, minWidth: 800, minHeight: 600, frame: false, // 无边框窗口 titleBarStyle: 'hidden', titleBarOverlay: { color: '#2f3241', symbolColor: '#fff' }, webPreferences: { preload: path.join(__dirname, '../preload/index.js'), webSecurity: false // 开发时允许跨域 } }) if (process.env.VITE_DEV_SERVER_URL) { win.loadURL(process.env.VITE_DEV_SERVER_URL) win.webContents.openDevTools() } else { win.loadFile(path.join(__dirname, '../renderer/index.html')) } }5.2 实现自定义标题栏
由于我们使用了无边框窗口,需要自己实现标题栏。在Vue组件中:
<template> <div class="titlebar"> <div class="title">我的应用</div> <div class="controls"> <button @click="minimize">-</button> <button @click="maximize">□</button> <button @click="close">×</button> </div> </div> </template> <script setup> const minimize = () => window.electronAPI.minimize() const maximize = () => window.electronAPI.maximize() const close = () => window.electronAPI.close() </script> <style scoped> .titlebar { -webkit-app-region: drag; display: flex; justify-content: space-between; background: #2f3241; color: white; padding: 0 10px; } .controls { -webkit-app-region: no-drag; } .controls button { background: none; border: none; color: white; padding: 0 10px; cursor: pointer; } </style>对应的预加载脚本API:
contextBridge.exposeInMainWorld('electronAPI', { minimize: () => ipcRenderer.send('window:minimize'), maximize: () => ipcRenderer.send('window:maximize'), close: () => ipcRenderer.send('window:close') })主进程处理:
ipcMain.on('window:minimize', () => win.minimize()) ipcMain.on('window:maximize', () => { if (win.isMaximized()) { win.unmaximize() } else { win.maximize() } }) ipcMain.on('window:close', () => win.close())6. 打包与发布
6.1 配置打包选项
electron-vite使用electron-builder进行打包。修改electron-builder.yml:
appId: com.example.myapp productName: 我的应用 copyright: Copyright © 2023 directories: output: dist buildResources: build files: - from: . filter: - "**/*" to: . win: target: - target: nsis arch: - x64 icon: build/icon.ico nsis: oneClick: false perMachine: false allowToChangeInstallationDirectory: true6.2 处理打包常见问题
国内网络环境下打包可能会遇到下载工具失败的问题。解决方法:
手动下载所需二进制文件:
- winCodeSign: https://cdn.npm.taobao.org/dist/electron-builder-binaries/winCodeSign-2.6.0/winCodeSign-2.6.0.7z
- nsis: https://cdn.npm.taobao.org/dist/electron-builder-binaries/nsis-3.0.4.1/nsis-3.0.4.1.7z
- nsis-resources: https://cdn.npm.taobao.org/dist/electron-builder-binaries/nsis-resources-3.4.1/nsis-resources-3.4.1.7z
解压到指定目录:
- Windows:
C:\Users\你的用户名\AppData\Local\electron-builder\Cache
- Windows:
6.3 生成安装包
运行打包命令:
npm run build打包完成后,dist目录下会有安装包和可执行文件。如果要生成免安装的绿色版,可以配置:
win: target: - target: portable arch: - x647. 进阶技巧与优化
7.1 性能优化建议
Electron应用容易变得臃肿,这里有几个优化技巧:
- 按需加载Node模块:很多Node模块体积很大,只在主进程需要时再require
- 启用上下文隔离:防止渲染进程直接访问Node API,提高安全性
- 使用Vite的代码分割:Vue路由的懒加载可以显著减少初始加载时间
- 优化渲染进程:像Web应用一样,避免不必要的DOM操作和大型状态
7.2 调试技巧
开发时可以利用这些调试工具:
- 主进程:VSCode的JavaScript调试终端
- 渲染进程:Chrome开发者工具(默认会自动打开)
- 进程通信:在控制台打印ipc通信日志
一个有用的调试配置(.vscode/launch.json):
{ "version": "0.2.0", "configurations": [ { "name": "Debug Main Process", "type": "node", "request": "launch", "cwd": "${workspaceFolder}", "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron-vite", "runtimeArgs": ["dev"], "outputCapture": "std", "skipFiles": ["<node_internals>/**"] } ] }7.3 原生功能扩展
Electron的强大之处在于可以轻松集成原生功能。比如实现一个全局快捷键:
主进程中:
const { globalShortcut } = require('electron') app.whenReady().then(() => { globalShortcut.register('CommandOrControl+Shift+I', () => { win.webContents.openDevTools() }) }) app.on('will-quit', () => { globalShortcut.unregisterAll() })或者使用系统托盘:
const { Tray, Menu } = require('electron') const path = require('path') let tray = null app.whenReady().then(() => { tray = new Tray(path.join(__dirname, '../resources/tray.png')) const contextMenu = Menu.buildFromTemplate([ { label: '显示', click: () => win.show() }, { label: '退出', click: () => app.quit() } ]) tray.setToolTip('我的应用') tray.setContextMenu(contextMenu) })8. 项目结构与代码组织
8.1 推荐的项目结构
经过多个Electron项目实践,我总结出这样的目录结构:
src/ ├── main/ │ ├── lib/ # 主进程工具库 │ ├── services/ # 后台服务 │ ├── windows/ # 多窗口管理 │ └── index.js # 主入口 ├── preload/ │ ├── lib/ # 预加载脚本工具 │ └── index.js # 主入口 └── renderer/ ├── src/ │ ├── assets/ │ ├── components/ │ ├── composables/ # Vue组合式函数 │ ├── router/ │ ├── stores/ │ ├── views/ │ ├── App.vue │ └── main.js └── index.html8.2 状态共享模式
主进程和渲染进程之间共享状态是个常见需求。我通常采用这种模式:
- 主进程维护真实状态
- 通过ipc暴露状态读写接口
- 渲染进程通过Pinia封装这些接口
例如实现一个主题切换功能:
主进程:
let currentTheme = 'light' ipcMain.handle('theme:get', () => currentTheme) ipcMain.handle('theme:set', (_, theme) => { currentTheme = theme win.webContents.send('theme:changed', theme) })渲染进程的Pinia store:
export const useThemeStore = defineStore('theme', () => { const theme = ref('light') const getTheme = async () => { theme.value = await window.electronAPI.invoke('theme:get') } const setTheme = async (newTheme) => { await window.electronAPI.invoke('theme:set', newTheme) theme.value = newTheme } // 监听主题变化 window.electronAPI.on('theme:changed', (_, newTheme) => { theme.value = newTheme }) return { theme, getTheme, setTheme } })这种模式保持了单一数据源,同时提供了响应式的使用体验。
