☰
Vue3+Vite+Electron桌面应用开发实战:从初始化到打包上线
2026/10/2 1:34:26 网站建设 项目流程

做桌面客户端开发这几年,我一直觉得前端技术栈选型是个很微妙的事——既要考虑团队现有的技术积累,又要兼顾最终交付的体量、跨平台能力和维护成本。Vue3加Vite加Electron这套组合,是我实际踩过不少坑之后,认为比较适合中小团队快速交付桌面应用的方案。这篇文章就围绕这套技术栈,从工程初始化到打包上线的完整闭环,把我在真实项目里遇到的问题和排查过程都记录下来,希望能帮你少走些弯路。

先说清楚这篇文章是写给谁看的:已经会Vue3基础语法、想直接上手桌面应用开发的前端工程师;或者后端同事想独立交付一个带界面的跨平台工具,又不想引入重型桌面框架;也包括那些已经把Electron项目搭起来、但打包阶段一直出问题的人。如果你只是随便看看,这文章也能帮你建立一套关于Electron工程化的整体认知。

1. 为什么选Vue3+Vite+Electron:这套组合解决了什么问题

1.1 技术选型的三个底层逻辑

先说Vite。Electron项目过去默认搭配Webpack,因为脚手架和社区模板都是这么教的,但Vite在开发体验上的优势是碾压级的。Vite利用浏览器原生的ES Module,冷启动几乎秒开,修改代码后热更新也是毫秒级反馈。Electron开发最让人难受的就是——主进程、渲染进程、预加载脚本三套代码要并行看状态,如果用Webpack,改一行渲染层代码可能要等两到三秒重新编译。Vite把这个问题基本消解了,开发时渲染进程的更新速度就像在写普通网页一样,这对于调试界面效率的提升非常明显。

再说Vue3。Vue3的Composition API配合<script setup>语法,写复杂交互逻辑时比Options API清晰得多。桌面应用和网页最大的区别在于:页面上有大量异步事件、多窗口通信、系统级API调用,这些逻辑天然适合用函数式组合的方式管理。我们有同事用Vue2写过Electron应用,状态一多就到处是mixin和eventBus,后期维护成本很高。Vue3的reactive和ref模型在处理IPC回调、串口数据流这类频繁变化的状态时,心智负担低很多。

最后说Electron本身。选择Electron而不用Tauri或Qt,很大程度上是生态问题。Electron的npm包生态极其丰富,原生Node模块基本都能直接用,跨平台打包方案也成熟。Tauri虽然包体积小、内存占用低,但它的Rust侧开发门槛和中文字体、WebView兼容性问题在小团队里往往成为隐形负担。如果你的目标用户主要在Windows上,而且交付周期紧,Electron依旧是最稳妥的选择。

1.2 需要避开的同类方案陷阱

很多人会拿Electron和Tauri反复纠结,我的建议是:别在选型上花太多时间。如果你要做的是数据密集型工具、内部管理系统、需要大量访问Node生态包的应用,Electron是唯一不用考虑边界问题的方案。Tauri需要自己处理Rust编译链,Windows上还得装WebView2运行时,真要遇到浏览器兼容问题,排查成本比Electron高一个量级。

还有一类方案是直接把现有Web项目用PWA包装一下,或者做成浏览器快捷方式。对于纯展示类工具或许可行,但一旦牵扯到文件系统读写、系统托盘、开机自启、串口通信这些桌面级能力,PWA完全无能为力。Electron的意义就是把Web的开发效率和Node的系统能力结合起来,这也是当初选它的底层逻辑。

2. 工程初始化与开发环境搭建:这部分藏着大量暗坑

2.1 基础脚手架创建方式

现在创建一个Vue3加Vite项目很简单,一条命令就够了:

npm create vite@latest my-electron-app -- --template vue

但创建完成之后,很多人习惯性地直接npm install,然后立刻开始写页面。如果你要在同一个项目里同时开发Electron,建议先把依赖装上之后,立即补充一套Electron相关的配置。网上很多模板项目会把Electron主进程放在electron/目录下,渲染进程用Vite管理,主进程用单独的tsconfig,这样职责相对清晰。

我的习惯是创建项目时顺手把electron和electron-builder装好,避免后面开发到一半才发现版本冲突,回头再处理依赖问题:

npm install -D electron electron-builder concurrently wait-on

这里有个小提醒:Electron的npm包在国内下载经常慢到怀疑人生,环境变量配置的electron镜像源这个事建议直接写进项目文档里,团队每个成员都要知道。我不是说让大家去研究什么网络优化,而是实际开发中这个坑真的会耽误半天时间。

2.2 主进程、预加载与渲染进程的接入细节

Electron应用三端代码结构大概是这样的:

my-electron-app/ ├─ src/ // 渲染进程,Vue代码 │ ├─ main.js │ ├─ App.vue │ └─ ... ├─ electron/ │ ├─ main.js // 主进程 │ └─ preload.js // 预加载脚本 ├─ index.html ├─ electron-builder.json // 打包配置 └─ vite.config.js

开发时主进程用electron/main.js直接加载开发服务器地址,生产环境则加载打包后的dist/index.html。这个判断在主进程里是必须的,否则容易出现"开发模式一切正常,打包后白屏"的情况。

预加载脚本的作用很多初学者理解不到位。渲染进程默认是不能直接访问Node API的,出于安全考虑,Electron官方从12版本开始把nodeIntegration默认为false,渲染进程里也拿不到process对象。正确的做法是通过contextBridge把需要的API暴露到window上:

// electron/preload.js const { contextBridge, ipcRenderer } = require('electron') contextBridge.exposeInMainWorld('desktop', { readFile: (path) => ipcRenderer.invoke('file:read', path), writeFile: (path, data) => ipcRenderer.invoke('file:write', path, data), onSerialData: (callback) => { ipcRenderer.on('serial:data', (_event, data) => callback(data)) } })

然后渲染进程里就可以通过window.desktop.readFile(...)来调用,这样既安全又符合Vue项目的模块化习惯。

2.3 开发者冷启动与热更新的坑

开发态下我们需要同时启动Vite和Electron,常规做法是用concurrently并行执行:

{ "scripts": { "dev": "concurrently \"npm run dev:vite\" \"npm run dev:electron\"", "dev:vite": "vite", "dev:electron": "wait-on tcp:5173 && electron electron/main.js" } }

wait-on确保Vite开发服务器起来之后再启动Electron,避免白屏和连接失败。这里有个很常见的问题:Vite默认端口是5173,如果你本机端口被占用,Vite会自动递增端口,但主进程里写死的还是5173,这样Electron就会加载失败。解决方案是给Vite指定固定端口:

// vite.config.js export default { server: { port: 5173, strictPort: true } }

strictPort设置为true,端口被占用时直接报错而不是静默切换,这个思路其实也适用于其他配置——宁可让问题尽早暴露,也不要让它悄悄变成一个难查的bug。

主进程的改动需要重启Electron才能生效,这个问题可以用electronmon或者nodemon监听主进程文件变化自动重启来解决。不过按我的经验,这个阶段的优化优先级其实不高——真正高频改动的还是渲染层界面,主进程代码一旦稳定下来,很长时间不用动。

有一个非常大的坑,就是打包后界面白屏。这个问题的常见原因有两个:一是Vite的base配置,二是路由模式。打包后的应用是file://协议加载,Vite默认生成的静态资源路径以/开头,在本地文件系统里会解析成盘符根目录,导致找不到JS和CSS。解决办法是在vite.config.js里设置:

export default { base: './' }

路由方面,Vue Router必须使用createHashRouter或者createWebHashHistory,不要用createWebHistory。原因很简单,file://协议下没有服务端路由,history模式会直接404。这个问题我在第一次打包时折腾了一个下午,也见过不少人在社区里问,所以值得单独拎出来强调。

3. 核心开发环节:窗口管理、菜单与通信机制

3.1 IPC通信的低配安全模型

很多Electron新手最大的问题就是乱用ipcRenderer和ipcMain。把nodeIntegration打开、在渲染进程里直接用require('fs'),这样做开发速度确实快,但应用的安全性基本为零——一旦渲染层被注入恶意脚本,攻击者就可以直接读写用户文件系统。虽然桌面应用不像Web应用那样暴露在公网,但作为工程标准,我们仍然要按"渲染层永不信任"的原则来做。

我的做法是:统一封装调用接口,渲染进程只通过window.desktop访问白名单方法。每个业务动作对应一条IPC消息,比如读取文件、保存配置、执行串口指令等。主进程收到消息后做鉴权和路径校验,再执行实际操作。这样做还有一个好处:后续如果想把Electron换成Tauri,渲染层的调用代码基本不用改,只替换预加载脚本就行。

IPC调用时可以统一采用invoke/handle模式,它能自动处理Promise的resolve和reject,比send/on模式的回调地狱清爽得多。比如错误处理:

// 主进程 ipcMain.handle('config:save', async (event, config) => { if (!validateConfig(config)) { throw new Error('配置格式不合法') } await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf-8') return { success: true } })

渲染进程里直接try/catch捕获异常即可,调用失败的现场信息通过Error对象传回来,方便定位问题。

3.2 窗口生命周期与菜单的跨平台差异

窗口管理看起来简单,实际操作中踩的坑不少。比如主窗口关闭时,应用默认行为是所有窗口关闭后进程退出,但如果你在main.js里监听了window-all-closed事件并默认调用app.quit(),那么macOS上用户关掉窗口再点击Dock图标时应用不会重新启动,因为进程已经退了。macOS的习惯是关窗口不退出应用,菜单栏还在。这个平台差异如果没处理,会被macOS用户当成bug反馈。

菜单方面,electron的Menu模块可以为应用设置自定义菜单。一个非常常见的坑是:Windows和Linux下菜单默认在应用窗口顶部,macOS下菜单在系统全局菜单栏。如果你要做的是工具类应用,菜单不是必需品,完全可以全部隐藏掉,只保留右键菜单。做法是:

Menu.setApplicationMenu(null)

但这样在macOS上会丢失默认的编辑菜单,导致输入框里不能进行复制粘贴快捷键操作。解决方案是macOS保留默认菜单,Windows和Linux隐藏:

if (process.platform === 'darwin') { Menu.setApplicationMenu(Menu.buildFromTemplate([...defaultMacMenu])) } else { Menu.setApplicationMenu(null) }

还有一个容易被忽略的细节——快捷键注册。在渲染进程里给window对象添加keydown事件监听,看似很常规,但Electron里如果焦点在iframe或WebContents内部,keydown事件可能不触发。稳妥的方案是用主进程的globalShortcut注册全局快捷键,或者用before-input-event拦截键盘事件。特别是做播放器、课件工具这类应用时,这个细节直接决定快捷键在用户机器上是否可靠。

3.3 串口/硬件通信的集成经验

很多桌面工具类应用绕不开串口通信,比如调试烧录工具、读卡器、传感器数据采集等。Node.js生态里用serialport包是标准选择,但它在Electron里有一个很难缠的问题——原生模块。

serialport是C++插件,需要针对Electron的ABI重新编译。安装后如果直接在Electron主进程里require('serialport'),常常会报NODE_MODULE_VERSION不匹配的错误。解决办法是执行:

npx electron-rebuild

或者更省事的方式,在electron-builder里配置npmRebuild: true,打包时会自动重建原生模块。如果还不行,检查一下native_modules相关配置。另外注意,serialport这类原生模块是必须放在主进程使用的,渲染进程里即使开了nodeIntegration也不能直接用,需要走IPC让主进程转发数据。

WebSocket也类似。有人开发时用WebSocket连接外部服务一切正常,打包成App后连不上,排查了域名、端口、防火墙都没问题,最后发现是打包后的App没有设置Certificate相关权限,或者CSP(内容安全策略)把连接拦了。如果你在index.html里配置了CSP,记得把WebSocket地址加进connect-src:

<meta http-equiv="Content-Security-Policy" content="default-src 'self'; connect-src ws://localhost:8080 https://api.example.com">

不配置CSP的话,开发环境没问题,打包后Electron默认安全策略可能会拦截非本地请求。这类问题排查起来非常隐蔽,因为我见过太多人把问题归到"打包后网络失效"然后无从下手。

4. 打包全流程实录:electron-builder从配置到产物

4.1 打包工具选型与基础配置

Electron的打包方案主流是electron-builder和Electron Forge。两者各有拥趸,但electron-builder的配置灵活性和社区资料量更占优势,也是我实际用下来最顺手的。它支持生成Windows的nsis安装包、macOS的dmg、Linux的deb和AppImage,一套配置搞定三端。

一个基础的electron-builder.json配置大致长这样:

{ "appId": "com.example.desktop", "productName": "我的桌面工具", "directories": { "output": "release" }, "files": [ "dist/**/*", "electron/**/*", "package.json" ], "win": { "target": "nsis", "icon": "build/icon.ico" }, "mac": { "target": "dmg", "icon": "build/icon.icns" }, "linux": { "target": ["AppImage", "deb"], "icon": "build/icon.png", "category": "Utility" } }

需要注意files字段——它决定了哪些文件会被拷贝进安装包。这里容易出问题的是,有人把node_modules整个打包进去,导致安装包体积巨大且包含大量无用依赖。electron-builder会自动处理生产依赖,但如果你在dependencies里安装了只在主进程用到的包,要确保它在生产环境能正常工作;而只在渲染进程用的包,尽量放在devDependencies,这样打包时不会被重复收集。

图标文件也很容易出问题。Windows上必须用.ico格式,macOS要用.icns,Linux用.png。网上经常有人只在build目录放一个256x256的png,然后Windows打包报错找不到图标。electron-builder对Windows图标的格式要求很严格,如果你只有一个png,需要先转成ico。可以使用png-to-ico或者在线工具转换,但注意文件不要太大,以及透明度通道处理正确。

4.2 Linux打包与fpm报错的完整解决思路

在Linux环境打包时,很多人在生成deb包时遇到fpm相关的报错,报错信息往往类似fpm exited with code: 1,后面跟着一串Ruby堆栈。这个问题在electron-builder文档里并没有写得很详细,社区里也常常被绕过——直接改用AppImage。但如果你确实需要deb包,尤其是要给Ubuntu用户交付,被这个问题卡住确实非常头疼。

我实际排查下来,fpm报错的根本原因通常是打包机缺少Ruby环境和一些系统依赖,或者fpm版本与electron-builder不兼容。最简单的解决思路是安装完整的依赖:

sudo apt-get update sudo apt-get install -y ruby ruby-dev rubygems build-essential sudo gem install fpm

装完之后再执行electron-builder --linux deb,往往就能顺利通过。另外提醒一句,electron-builder官方要求的Linux运行环境里需要很多库,比如libgtk-3-dev、libnotify-dev、libnss3-dev、libxss1等,如果是在Docker里做CI打包,基础镜像建议直接使用electron-builder团队维护的electronuserland/builder镜像,而不是自己在普通Ubuntu镜像里一个个补齐依赖,那个折腾成本相当高。

如果你不想装fpm,也有一个折中的办法:直接打包成AppImage。AppImage是免安装的绿色版本,用户下载下来赋予执行权限就能直接运行,调试阶段自己用或给测试用都够。正式交付再用deb或rpm,根据用户群体的Linux发行版来定。

4.3 Vite构建与静态资源路径的坑

前文提到打包白屏的问题,这里再展开讲几个衍生场景。

第一个是:base: './'配置之后,如果项目里用到动态引入图片,比如require('@/assets/xxx.png'),在某些构建场景下可能不生效。Vite对动态require的处理跟Webpack不一样,它更推荐用new URL('@/assets/xxx.png', import.meta.url)或直接import。静态资源路径的问题在Electron里尤其刺眼,因为网页端路径错了顶多图片不显示,Electron里路径错了可能导致整个模块加载失败。

第二个是路由懒加载。Vue Router按需加载组件时,Webpack会生成chunk文件,而Vite打包后同样会生成多个js文件。这些文件在file://协议下如果路径写错,会导致点击某个路由时控制台报Failed to fetch dynamically imported module。解决办法同样是将base设为./,确保chunk引用路径是相对的。如果拆得特别碎导致报错频繁,可以考虑减少动态导入的粒度,或者把关键页面合并。

第三个是import.meta.env在生产环境的取值问题。很多人会用import.meta.env.MODE区分开发、测试、生产环境,这本身没问题。但有个细节——vite build --mode test时,Vite要求存在对应的.env.test文件,否则环境变量加载不到,构建结果可能跟预期不一致。为了区分不同环境,要确保:

.env.development .env.test .env.production

每个文件里都定义好该环境独有的变量,并且注意Vite默认暴露给客户端的变量必须以VITE_开头,否则在渲染进程代码里访问会得到undefined。这个坑在联调不同环境时很经典,代码没问题、配置没问题,就是环境变量文件没写对。

5. 高频问题与排查技巧实录

5.1 布局异常与样式失效的类型化处理

说到vue 打包后 布局异常,这个热词背后通常有几类原因。最常见的是Flex或Grid布局依赖的某些CSS特性在新版Chromium里正常,但Electron自带的Chromium版本可能比用户网页版略旧;更常见的是,cssnano或PostCSS压缩时把某些写法变形,或者pxtorem这类插件在转换时误伤了自定义属性或部分框架样式。

举个例子,我在一个Vue3项目里做移动端适配时用了postcss-pxtorem,结果打包后ECharts图表里的字体全部被放大。排查发现是px转rem的规则把ECharts内部的style样式也转换了,而rem的基准值在Electron窗口宽度变化时重新计算,导致图表尺寸异常。解决方案是给postcss-pxtorem配置selectorBlackList,把.echarts相关类名排除掉,或者把ECharts的字体单位强制写成大写PX,避免被转换。

样式问题在Electron里还有一个高频来源——系统默认样式覆盖。例如Windows下按钮在获得焦点时会带蓝色边框,macOS下滑动条默认是悬浮的。解决方式是引入统一的CSS reset,或者在app.whenReady()后调用nativeTheme.themeSource控制主题模式,确保暗色/亮色模式符合设计稿。

5.2 串口、WebSocket等平台能力联调问题

前文聊过serialport需要electron-rebuild,这里再补充一个场景:主进程获取到的串口设备名在Windows下是COM3这种格式,在Linux下是/dev/ttyUSB0,macOS下是/dev/tty.usbserial-xxx。跨平台开发时,设备选择列表必须是动态获取的,别硬编码正则去匹配设备名。你需要做的就是一个下拉框,列出所有可用串口,让用户自己选。

WebSocket在开发环境能连、打包后连不上这个问题,也值得再强调一次排查顺序:先看打包后的App控制台输出什么报错,是CSP拦截还是证书错误;然后在主进程里加日志确认渲染进程发出的连接请求是否到了主进程;最后检查是否被系统防火墙拦截,以及后端服务是否允许了非浏览器源的Origin。串口、USB这类硬件设备问题,还要考虑权限环境——Linux下要把用户加入dialout组,否则没有权限访问串口设备。

5.3 构建阶段的内存溢出与资源失控

大型项目在打包时偶尔会遇到JavaScript heap out of memory的错误。这个问题的根源是Vite构建时Node进程的默认堆内存上限偏低,尤其是组件多、依赖复杂时。一个热门的临时解决方案是在命令行设置环境变量:

NODE_OPTIONS=--max-old-space-size=4096 vite build

Windows系统下的PowerShell写法略有不同:

$env:NODE_OPTIONS="--max-old-space-size=4096"; vite build

但说实话,这一招治标不治本。如果你经常遇到内存溢出,说明项目构建本身有问题。优先检查是不是无意中把整个node_modules目录当成静态资源复制了,或者有没有在main.ts里同步加载大量模块导致构建图过大。另一种情况是electron-builder打包时对node_modules做依赖收集,如果生产依赖特别多,也会增加构建机的内存压力。

一个比较实用的优化是:关闭sourcemap。在vite.config.js里设置:

build: { sourcemap: false }

生产环境绝大多数时候不需要sourcemap,关掉之后构建速度和内存占用都会有明显改善。我之前接手过一个项目,开发者为了排查线上问题开着sourcemap,结果每次打包都要四五分钟,还经常爆内存。关闭后三分钟以内完成,排查问题也基本不受影响。

6. 模板项目与自动化交付的一点建议

前面基本都是围绕一个应用从开发到打包的完整流程,最后聊一下模板项目和自动化的落地经验。Electron官方维护了一个electron-vite工具,也有大量社区模板,但我的建议是:不要直接无脑依赖模板,自己维护一套工程骨架的价值很大。模板里可能有太多用不到的取舍,真正跑起来会忘记它是怎么实现的,出问题就很难排查。

我通常会把下面这些内容固化在项目模板里:

  • 基础的目录结构,electron/、src/、build/、scripts/分开
  • 统一的vite.config.js,写死base: './'、固定端口、别名配置
  • 主进程的sandbox、contextIsolation、nodeIntegration安全配置
  • 一套dev脚本,同时启动Vite和Electron,并负责重启主进程
  • 一个build脚本,先跑vite build再跑electron-builder

自动化构建方面,如果团队有CI/CD基础,建议把electron-builder集成到流水线里。GitHub Actions或自建Jenkins都可以,核心是分别构建Windows和macOS需要不同的运行环境,尤其是macOS的dmg包签名和公证必须在一台macOS机器上跑,Windows平台的nsis安装包也建议在Windows机器上构建,避免跨平台打包出现各种隐性问题。很多开发者抢在Windows上交叉打包dmg,结果发现签名和格式都会出问题,所以别心疼那点CI机器成本。

最后再分享一个我自己的习惯——每次发版前,把整个打包流程从零跑一遍,在一个干净环境里测试安装、启动、升级、卸载。桌面应用和Web应用不一样,用户一旦装上,你无法热修复。把这次打包的产物上传到内网,让测试人员用全新虚拟机装一遍,比你在自己机器上点开一百遍都有用得多。我的体会是,这个环节做得越严谨,售后反馈里的“装不上”“白屏”“闪退”类问题就越少。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询