
1. 项目概述与环境准备1.1 这套技术栈到底解决什么问题做桌面端应用的时候很多团队第一反应是选 Electron但真正动起手来才发现坑不少。我做了几年的前端最近把一个内部工具从纯 Web 端迁到了桌面端技术选型用的就是 Vue3 Vite Electron。之所以选这套组合核心原因是 Vue3 的组合式 API 写业务逻辑比 Vue2 舒服太多Vite 的冷启动速度和热更新体验又是 Webpack 时代完全没法比的加上 Electron 能把 Web 技术栈直接带到桌面端——三个东西配合起来从开发到打包整个链路非常顺。这套方案适合谁如果你们团队本身就是以 Web 技术为主突然接到一个桌面应用需求不想用 Java 或 C 重新招人、重新搭班子那 Vue3 Vite Electron 几乎是最优解。尤其是做内部管理系统、数据可视化大屏、硬件串口调试工具、音视频处理工具这类场景Web 侧生态成熟Electron 又能访问系统底层能力两边优势都能吃到。当然必须提前说清楚这套方案不是万能的。如果做的是高性能图形处理、3D 建模、大型游戏Electron 的 Chromium 内核和 Node.js 桥接层会拖后腿内存占用也比较夸张。选型之前先问自己三个问题用户机器配置怎么样应用是否需要长时间运行对安装包体积是否敏感想清楚再动手后面能少走很多弯路。1.2 Node.js 版本与 npm/pnpm 工具链选型环境准备是整个链路的地基这一块我踩过不少坑先说结论Node.js 版本建议直接上 18 或 20 LTS不要用 16 以下也不要追最新的奇数版本。Electron 对 Node 版本有内置要求Vite 5 以上版本也要求 Node 18如果你本地装的是 14跑npm create vite的时候大概率会直接报engines校验错误。包管理器方面npm、pnpm、yarn 我都实际用过最终留在了 pnpm。原因是 Electron 最终打包的时候会把整个 node_modules 打进去一部分pnpm 的硬链接机制配合node-linkerhoisted设置能让依赖安装速度和磁盘占用都好看很多。不过这里有一个经典坑pnpm 默认的符号链接结构在某些 Electron 原生模块编译环节会出问题需要在项目根目录放一个.npmrc文件写入两行配置node-linkerhoisted shamefully-hoisttrue这样 pnpm 会采用扁平化的依赖结构行为更接近 npmElectron 相关的 node-gyp 编译和 electron-builder 打包基本就不会因为模块找不到闹脾气了。我第一次没加这个配置打包的时候报了一堆Cannot find module的错误后来排查到就是这个原因。如果你用的是 npm 或 yarn可以跳过这段但下面所有操作逻辑是通用的。1.3 用 Vite 创建 Vue3 项目并集成 Electron环境准备好之后开始创建项目。这一步有一个推荐做法和一个反推荐做法。反推荐做法是直接搜一个 Electron Vue 的模板仓库克隆下来用热词里搜electron 模板项目的人很多但我不建议你这么做——模板项目往往内置了一些你可能用不到的依赖和改动出了问题排查起来特别难受而且别人封装过的目录结构未必符合你的项目习惯。推荐做法是分两步走先用 Vite 把纯前端项目建好再把 Electron 手动接进来。整个流程控制在自己手里每一步都明白发生了什么。先创建 Vue3 项目npm create vitelatest my-app -- --template vue cd my-app npm install这里--template vue会生成一个标准 Vue3 Vite 项目JavaScript 版本如果你需要 TypeScript把模板参数改成vue-ts即可。装完依赖后先别急着加 Electron先把 Web 端跑起来确认环境没问题npm run dev看到 Vite 的本地服务地址默认 http://localhost:5173能正常打开说明 Node、npm、Vite 链路没问题。接着安装 Electron 相关依赖npm install electron electron-builder --save-dev npm install vitejs/plugin-vue cross-env --save-develectron是你开发时用来跑桌面壳子的运行时electron-builder是后面打包安装包的利器cross-env用来跨平台设置环境变量。到这里环境准备告一段落。接下来要面对的问题就是——三个技术栈怎么在项目里有序配合。2. 项目结构设计与核心配置2.1 主进程、预加载脚本与渲染进程的角色分工Electron 应用在结构上分三个进程主进程Main Process、预加载脚本Preload Script和渲染进程Renderer Process。很多人理解不彻底我打个比方主进程就像餐厅后厨的管事掌控全局能访问系统文件、开窗户、看后厨库存Node.js API渲染进程是前厅服务员负责和顾客用户打交道展示页面和响应用户操作但它不能直接碰后厨的东西预加载脚本是连接前厅和后厨的传菜口只能传递特定的、安全的菜品不能什么都往上送。所以项目的目录结构我建议这样拆my-app/ ├── electron/ │ ├── main.js # 主进程入口 │ ├── preload.js # 预加载脚本 │ └── icon/ # 应用图标资源 ├── src/ │ ├── components/ # Vue组件 │ ├── views/ # 页面视图 │ ├── router/ # 路由配置 │ ├── store/ # 状态管理 │ └── App.vue # 根组件 ├── index.html ├── vite.config.js └── package.jsonelectron目录放主进程和预加载脚本相关代码和src目录完全隔离。这样做的好处是职责清晰后续打包配置时不会出现 src 里的文件被误打包成主进程代码的情况。2.2 Vite 配置开发服务器与代理Vite 在开发模式下启动的是一个本地 HTTP 服务器默认 5173 端口而 Electron 加载页面有两种方式开发环境加载http://localhost:5173生产环境加载打包后的index.html文件。所以主进程里需要判断当前是开发模式还是生产模式逻辑如下// electron/main.js import { app, BrowserWindow } from electron import path from node:path import { fileURLToPath } from node:url const __dirname path.dirname(fileURLToPath(import.meta.url)) process.env.APP_ENV process.env.APP_ENV || development function createWindow() { const win new BrowserWindow({ width: 1200, height: 800, webPreferences: { preload: path.join(__dirname, preload.js), contextIsolation: true, nodeIntegration: false } }) if (process.env.APP_ENV development) { win.loadURL(http://localhost:5173) } else { win.loadFile(path.join(__dirname, ../dist/index.html)) } } app.whenReady().then(createWindow)这里有几个关键点必须强调。第一contextIsolation: true是安全底线必须开启它能把渲染进程和 Node.js 环境隔离起来防止页面上的恶意脚本拿到系统权限相应地nodeIntegration: false也应该保持关闭。如果为了省事把这两个关掉页面上的任何 JavaScript 都能直接读写本地文件系统等于给黑客开了一扇大门。第二preload.js要用 CommonJS 语法写require不要用 ES Moduleimport因为 Electron 的预加载脚本在沙箱环境里执行模块解析方式比较特殊。下面是一个安全的预加载脚本示例// electron/preload.js const { contextBridge, ipcRenderer } require(electron) contextBridge.exposeInMainWorld(api, { send: (channel, data) ipcRenderer.send(channel, data), invoke: (channel, data) ipcRenderer.invoke(channel, data), on: (channel, callback) { ipcRenderer.on(channel, (event, ...args) callback(...args)) } })渲染进程里通过window.api就能与主进程通信既不会直接暴露 ipcRenderer也保证了只能通过白名单通道通信。第三Vite 的vite.config.js里需要配一下base: ./否则打包后index.html里的资源路径会使用绝对路径在 Electron 的file://协议下无论如何都加载不到资源最后页面白屏。这个坑我在第一次打包时踩过后面会再讲。2.3 开发模式同时拉起 Vite 和 Electron配置好了主进程和预加载怎么让两个进程同时跑呢直接在命令行里开两个终端窗口分别跑npm run dev和npm run electron也可以但很低效而且 Electron 启动的时候 Vite 不一定已经就绪页面可能找不到服务器。一个更优雅的方式是借助concurrently和wait-on两个工具npm install concurrently wait-on --save-dev然后在package.json里写好三个脚本{ scripts: { dev:web: vite, dev:electron: cross-env APP_ENVdevelopment electron ., dev: concurrently -k \npm run dev:web\ \npm run dev:electron\ } }但我实际开发中觉得这样还不够稳妥因为 Electron 主进程启动时会在页面加载瞬间访问http://localhost:5173如果 Vite 还没启动完就会白屏报错。我会改成这样dev: concurrently -k \npm run dev:web\ \wait-on http://localhost:5173 npm run dev:electron\意思是用wait-on先轮询 Vite 端口确认服务器已经能响应了再拉起 Electron。实测下来基本不会出现白屏问题。3. 核心功能开发从页面到桌面能力3.1 主进程与渲染进程通信的正确姿势桌面应用和纯 Web 应用最大的区别在于页面里经常会用到系统能力读取本地文件、访问串口、监听快捷键、控制窗口大小等等。这些能力都在主进程侧渲染进程只能通过通信渠道来请求。Electron 的 IPCInter-Process Communication机制是实现这个通信的官方方案核心 API 是ipcMain和ipcRenderer两个模块。我的建议是所有通信都走invoke/handle模式尽量不用send/on。原因很简单invoke/handle是请求-响应模式天然支持返回 Promise调用方可以拿到结果代码写起来像同步逻辑一样流畅而send/on是纯事件模式发完就没了主进程处理完再发一个事件回来中间状态全靠回调管理时序问题很容易出 bug。举一个真实案例。我的一个内部工具需要在页面上点击按钮后读取本地某个目录下的配置文件并解析成 JSON 显示出来。流程是// 主进程 import { ipcMain } from electron import fs from node:fs/promises ipcMain.handle(read-config, async (event, filePath) { try { const content await fs.readFile(filePath, utf-8) return { success: true, data: JSON.parse(content) } } catch (error) { return { success: false, message: error.message } } })渲染进程调用const result await window.api.invoke(read-config, C:/Users/xxx/config.json) if (result.success) { console.log(配置内容, result.data) } else { console.error(读取失败, result.message) }这样一个清晰的双向调用链就搭起来了。注意所有错误要在主进程侧先捕获并返回给渲染进程不要让异常直接抛到渲染进程的 Promise 下面不然一会儿是Error occurred in handler for xxx一会儿是渲染进程拿不到正确信息排查起来极其痛苦。3.2 菜单栏定制原生菜单与自定义菜单的取舍很多从 Web 端转过来的开发者会忽略Menu这块。Electron 默认会有一套菜单栏包含 File、Edit、View 等标准项目但默认菜单的文案是英文的而且很多功能比如 reload、toggleDevTools在面向终端用户的产品里是不应该暴露的。所以发布应用之前最好定制一下菜单栏内容。在预加载脚本之后我在主进程里加了这样一段菜单配置import { Menu, shell } from electron function setupMenu() { const template [ { label: 文件, submenu: [ { label: 打开配置文件, accelerator: CmdOrCtrlO, click: () openFile() }, { type: separator }, { label: 退出, accelerator: CmdOrCtrlQ, role: quit } ] }, { label: 编辑, submenu: [ { label: 复制, role: copy }, { label: 粘贴, role: paste }, { type: separator }, { label: 截屏, accelerator: CmdOrCtrlShiftS, click: () captureScreen() } ] }, { label: 帮助, submenu: [ { label: 项目主页, click: () shell.openExternal(https://github.com/your/repo) } ] } ] Menu.setApplicationMenu(Menu.buildFromTemplate(template)) }这里有一个细节值得注意role属性表示这个菜单项走的是 Electron 内置行为比如 copy、paste、quit、toggleDevTools 等在 mac 上还能自动匹配系统应用的菜单习惯。业务相关的菜单项比如打开配置文件需要自己绑定click事件。macOS 平台上还有一个容易忽略的问题应用菜单第一项应该固定显示应用名称比如我的工具否则不符合 mac 用户的习惯。这个可以通过在 template 数组最前面加一个 label 为应用名的对象来解决。如果你觉得原生菜单栏样式呆板想完全自定义页面内的菜单栏把菜单做成 Vue 组件隐藏系统菜单栏可以在createWindow里设置autoHideMenuBar: true或Menu.setApplicationMenu(null)。但我要提醒你这种情况在 Windows 上按下 Alt 键仍然会弹出系统菜单栏需要监听menu-bar-visibility-change事件再处理一下。两种方案各有取舍我的建议是内部工具直接保留原生菜单栏面向 C 端用户的产品再考虑完全自定义。3.3 串口通信与 Node 原生模块的坑做桌面应用经常会碰到访问硬件设备的场景比如通过 RS232 串口和单片机通信。Electron 可以加载 Node.js 生态的serialport库但这里有一个非常经典的坑serialport是一个原生模块编译时依赖 Node 的头文件和 Python 环境而 Electron 运行时的 Node 版本和你本机 Node 版本往往不一致直接npm install serialport装的是针对本机 Node 编译的二进制放进 Electron 里根本跑不起来会报NODE_MODULE_VERSION不匹配的错误。解决办法是让electron-rebuild重新编译原生模块匹配 Electron 的 ABI 版本npm install electron/rebuild --save-dev npx electron-rebuild -f -w serialport每次升级 Electron 版本后都要重新执行一次 rebuild这个事很容易忘我建议把命令写进postinstall脚本里{ scripts: { postinstall: electron-rebuild -f -w serialport } }这样只要执行了npm install就会自动触发重建。实际使用中serialport配合ReadlineParser可以很方便地接收串口数据import { SerialPort } from serialport import { ReadlineParser } from serialport/parser-readline const port new SerialPort({ path: COM3, // Windows 用 COM 口Linux/mac 用 /dev/ttyUSB0 baudRate: 115200 }) const parser port.pipe(new ReadlineParser({ delimiter: \r\n })) parser.on(data, (data) { mainWindow.webContents.send(serial-data, data) })串口通信这种低频数据量场景完全够用。唯一要注意的是串口连接状态的变化比如端口被拔掉、设备掉线要及时监听并反馈到页面上不然用户看着界面以为还在正常接收数据实际上设备早就断开了。3.4 在 Vue3 里愉快地使用 JSX开发复杂页面的时候纯模板写法有时会变得很啰嗦尤其是遇到大量条件渲染或者需要函数式渲染的场景。Vue3 的官方生态是支持 JSX 的不过默认的 Vite 模板不会帮你配好需要自己装插件npm install vitejs/plugin-vue-jsx --save-dev然后在vite.config.js里进行配置import vueJsx from vitejs/plugin-vue-jsx export default { plugins: [vueJsx()] }配置好之后.tsx或.jsx文件里就可以直接写组件逻辑了import { defineComponent, ref } from vue export default defineComponent({ name: DeviceStatus, setup() { const connected ref(false) const toggleConnect () { connected.value !connected.value } return () ( div classdevice-panel span class{connected.value ? status-on : status-off} {connected.value ? 已连接 : 未连接} /span button onClick{toggleConnect}切换状态/button /div ) } })JSX 在这种组件里比模板清晰太多。但注意不要整个项目全用 JSX官方模板语法在绝大多数场景下可读性更好也更容易配合编译器做性能优化。我个人的习惯是常规业务组件用.vue单文件组件渲染逻辑特别复杂的展示型组件才用 JSX。3.5 热更新失效与端口占用时的处理Vite 的开发体验很丝滑但配合 Electron 时偶尔会遇到两个烦人问题。第一是改 Vue 代码后页面不更新——这个问题几乎都是因为 Electron 渲染进程加载的 URL 和 Vite 实际端口不一致导致的。排查思路先看 Vite 终端日志里监听的是哪个端口再看主进程loadURL写的是哪个端口。改代码前把这两个地方统一基本一次搞定。第二个问题是端口被占用。比如上一次开发进程没有完全退出5173 端口被残留进程占用了Vite 会自动换到 5174但 Electron 主进程写死的还是 5173于是 Electron 窗口加载失败。处理方案有两个一个是在主进程启动时从命令行参数读取 Vite 端口动态传给loadURL另一个简单粗暴——在package.json的 dev 脚本里启动前先把旧进程杀掉dev: concurrently -k \npm run dev:web\ \wait-on http://localhost:5173 npm run dev:electron\concurrently的-k参数会保证其中一个进程退出时把另一个也杀掉配合使用时基本不会残留。真遇到顽固残留Windows 上用netstat -ano | findstr 5173查 PID再taskkill /PID xxx /F手动清掉。4. 打包发布全流程与常见报错4.1 electron-builder 配置详解开发完代码只是第一步真正让项目可以交付用户的是打包环节。我用的打包工具是electron-builder它能把 Electron 应用打成 Windows 的 NSIS 安装包.exe、macOS 的 dmg 或 zip以及 Linux 的 AppImage、deb、rpm 等格式。先看一份我项目里的electron-builder配置写在package.json或单独的electron-builder.yml中{ build: { appId: com.example.myapp, productName: MyDesktopTool, directories: { output: release }, files: [ dist/**/*, electron/**/*, package.json ], win: { target: nsis, icon: build/icon.ico }, nsis: { oneClick: false, allowToChangeInstallationDirectory: true, createDesktopShortcut: true }, mac: { target: dmg, category: public.app-category.developer-tools }, linux: { target: [AppImage, deb], category: Utility } } }这里重点解释两个容易被忽略的地方。第一files字段决定了哪些文件会打进安装包。必选项是dist也就是 Vite 构建后的前端产物、electron主进程和预加载脚本、package.json。如果漏了dist打包出来的应用打开是空白窗口因为loadFile找不到页面文件如果漏了electron主进程入口都加载不了。还有一点生产环境依赖dependencies会被自动打包开发依赖devDependencies不会。所以 Electron 应用里如果主进程要用到某些第三方库一定要装到dependencies里装到devDependencies里就会落包。第二Windows 下应用的图标必须是.ico格式而且最好含 256x256 分辨率的图macOS 用.icnsLinux 用 PNG 就可以。如果你手头只有一张普通比例的 PNG直接在 electron-builder 里指定会被提示格式错误。推荐的做法是先用工具把 PNG 转成对应平台要求的格式不要把一张 512x512 的 PNG 直接拿去当 win 图标用。4.2 打包前必须处理的工程化细节我的习惯是在打包之前先跑一遍完善的构建流程把可能出错的信息全部暴露出来。第一步是执行npm run build把前端代码用 Vite 构建到dist目录。这一步要注意dist目录如果存在历史残留文件建议先删掉再构建避免旧文件混入新包。Vite 默认构建和开发环境比较干净但需要注意环境变量的设置。热词里有vite build --mode test这说明很多人会在不同环境下打包比如测试环境、生产环境对应的环境变量不同。Vite 是这么处理环境变量的项目根目录建三个文件.env # 所有环境共享的配置 .env.development # 开发环境 .env.production # 生产环境在.env.production里配置VITE_APP_API_BASEhttps://api.example.com VITE_ENVproduction执行vite build --mode production时Vite 会自动加载.env.production文件里的变量。如果用--mode test那就对应.env.test文件里的配置。但 Electron 主进程的环境变量不能直接读取VITE_开头的变量需要在主进程里通过process.env.VITE_APP_API_BASE读取的话得在打包前把 Vite 侧的变量注入到 Node 侧或者统一用dotenv来管理。具体场景各异一般前端业务代码直接读import.meta.env.VITE_APP_API_BASE即可。第二步是检查package.json里的main字段。Electron 主进程入口路径必须指向electron/main.js否则应用不知道怎么启动。electron-builder 打包时也会根据这个字段确定入口文件。我见过有人把main设成了src/main.js结果打包后的应用一直报Electron failed to install correctly的错误。第三步是确认构建输出目录和打包配置一致。Vite 默认输出distelectron-builder 设置的files里也包含了dist/**/*两者能对上。如果改了 Vite 的outDir那files里的路径也要跟着改否则打包产物缺少前端文件应用启动就是白屏。4.3 Windows 打包细节与 NSIS 安装包的坑Windows 平台打包相对省心但有几个细节值得专门讲。首先electron-builder 在打包 Windows 应用时需要下载 Electron 的 Windows 预编译二进制和 NSIS 工具链。如果网络环境不稳定下载经常会失败报错类似cannot get https://github.com/electron/electron/releases/download/...。这时候有两个解决办法一是设置镜像源二是在项目根目录放一个electron-builder.env文件配置ELECTRON_MIRRORhttps://npmmirror.com/mirrors/electron/ ELECTRON_BUILDER_BINARIES_MIRRORhttps://npmmirror.com/mirrors/electron-builder-binaries/这两个镜像能显著提升国内下载成功率。设置后最好清一下缓存再重新打包npx electron-builder --win其次NSIS 安装包配置里的oneClick: false很关键它决定用户安装时是必须走完安装向导可以改安装目录还是一键安装不可改安装路径。如果面向的是企业用户安装目录有时候需要按公司规范来建议设置成false。另外桌面快捷方式默认勾选创建这个一般不用动。还有一个常见的白屏问题。如果你开发时正常、打包后应用窗口内容空白打开开发者工具发现很多静态资源 404几乎都是因为没有设置base: ./。Vite 默认 base 是/构建出来的index.html里引用的资源路径是/assets/xxx.js在 Electron 的file://协议下这个绝对路径会被解析到磁盘根目录自然找不到。把vite.config.js里的 base 改掉export default { base: ./, plugins: [vue()] }改完后重新npm run build再打包就正常了。4.4 Linux 打包遇到 fpm 报错的完整解决路径在 Linux 上打 deb 或 rpm 包时electron-builder 底层会依赖 fpmeffing package management工具来生成安装包。很多人在这一步卡住报错信息五花八门最常见的几种cannot find rpmbuild fpm: command not found You must install fpm with: gem install fpm我在 Ubuntu 20.04 上第一次打包 deb 时也碰到过fpm相关报错。这个问题出现的原因有两个层面。第一层是系统缺工具比如打 rpm 包需要 rpm 构建工具链打 deb 包需要fakeroot和dpkg有些基础镜像里没装。针对这个先补齐系统依赖sudo apt-get update sudo apt-get install -y ruby ruby-dev rubygems build-essential sudo apt-get install -y rpm fakeroot dpkg sudo gem install fpm第二层是 gem 源的问题。如果gem install fpm走的默认源在国外速度慢或直接失败可以换成国内源gem sources --add https://gems.ruby-china.com/ --remove https://rubygems.org/ gem install fpm装好之后electron-builder 的 Linux 打包流程就会顺畅很多。这里我多说一句Linux 打包通常比 Windows 更容易遇到环境问题如果在生产服务器或 CI 环境打 Linux 包我建议直接用 docker 镜像比如electronuserland/builder:wine在容器里构建环境一次配好之后每次打包都复现同样的结果不会今天能过明天不能过。还有一个容易踩的坑Linux 打包 AppImage 时如果应用名称里有中文生成的 AppImage 文件在某些桌面环境下无法正常显示图标。稳妥做法是productName用英文图标资源单独配中文显示名。4.5 打包后布局异常的排查思路热词里有一个很典型的搜索vue 打包后 布局异常这个我在开发中也遇到过。平时的解决方案主要有几个方向。第一先确认是不是base路径的问题。文章前面说过资源路径配错会导致 CSS、JS 加载不出来表现得就像布局全乱、图片全挂。第二排查 CSS 单位问题。比如某些布局在 Web 端正常打包后在 Electron 窗口里却乱了——最常见的原因是在index.html里没有设置合适的 viewport或者某些大屏适配方案依赖窗口尺寸而 Electron 窗口初始尺寸和你浏览器调试时的尺寸不一致。建议在项目里把根容器设置为100%宽高并监听window.resize事件做动态适配。第三字体加载问题。Web 端可以用 CDN 字体打包成 Electron 应用后离线环境没有网络如果 CSS 里引用了远程字体文字就会显示成系统默认字体导致某些宽度计算和设计稿不一致。解决办法是下载好字体文件放进src/assets/fonts目录用font-face本地引入。排查这类问题我的固定流程是先用 Electron 打开应用后点开 DevTools开发模式下CtrlShiftI切到 Network 面板看有没有资源加载失败再切到 Console 看有没有 JS 报错最后检查窗口尺寸对比设计图。三步走完80% 的布局异常问题都能定位到根因。5. 常见问题排查与避坑备忘5.1 高频问题与对应解决方案速查表这一节把前面提到过的所有坑汇总成一张速查表方便你遇到问题时直接查。问题现象可能原因解决方案Electron 启动后白屏Vite 尚未就绪或 base 路径不对wait-on轮询端口后再启动 Electron设置base: ./打包后静态资源 404vite.config.js 缺少 base 配置在vite.config.js中增加base: ./NODE_MODULE_VERSION不匹配serialport 等原生模块未针对 Electron 重编执行npx electron-rebuild -f -w serialport并写入postinstallelectron-builder 下载超时Electron 二进制或工具链下载慢配置ELECTRON_MIRROR和ELECTRON_BUILDER_BINARIES_MIRROR镜像Linux 打包报 fpm 相关错误系统缺少 ruby、rpm、fakeroot 等安装对应系统依赖并gem install fpm打包后窗口菜单显示英文未配置菜单栏用Menu.buildFromTemplate自定义菜单设置中文 label页面执行window.api为 undefined预加载脚本未加载或 contextIsolation 配置问题检查webPreferences.preload路径确认preload.js使用 CommonJSElectron 窗口打开后没有热更新端口不一致或进程未清理确认loadURL端口与 Vite 输出一致用concurrently -k控制进程打包体积过大依赖被重复打包或包含不必要的文件复查files字段只打包dist、electron、package.json生产依赖只放必需库这张表是我实际项目中踩过坑的汇总每一条背后都有一段调试时间。建议把它保存到 README 里同事遇到同类问题能直接查。5.2 提升 Electron 应用安全性的几个习惯安全性这个话题容易被人忽略因为实际开发中大家更关注功能能不能跑通。但 Electron 应用的安全漏洞一旦被利用后果比普通 Web 应用严重得多——它直接暴露在用户的操作系统上。我给自己定了几条铁律。第一条永远开启contextIsolation: true永远关闭nodeIntegration: false。这是 Electron 官方安全基线目的前面说过。如果你实在需要从渲染进程直接访问 Node API也请先走 preload 的白名单通信不要暴力开全局 Node 集成。第二条所有渲染进程发来的数据在主进程侧做一层校验。IPC 通道虽然是内部通道但页面一旦被 XSS攻击者就能通过window.api.invoke向主进程发送任意数据。如果主进程不校验恶意数据可能被当作命令执行。最简单的做法是对 channel 做白名单校验对 payload 做类型和范围检查ipcMain.handle(read-config, async (event, filePath) { if (typeof filePath ! string || !filePath.startsWith(C:/safe/dir/)) { return { success: false, message: 非法路径 } } // ... 执行读取 })第三条不要轻易打开新窗口的nodeIntegration。有些第三方登录或外部链接页面如果被加载到带 Node 权限的窗口里风险极高。需要打开外部链接时优先用shell.openExternal交给系统浏览器处理。5.3 从开发环境转到打包环境的最后检查清单每次准备发版前我会按下面这个顺序过一遍基本能堵住大部分低级错误。第一本地用npm run build构建前端产物确认无报错后用npm run preview本地预览一下构建结果确认页面正常。第二检查dist目录里是否包含index.html和assets目录确认index.html里资源引用是./assets/...相对路径。第三确认package.json的main字段指向electron/main.jselectron目录下所有文件使用 CommonJS 语法除非你额外配置了构建工具。第四检查electron/main.js中win.loadFile的路径和实际dist目录结构能对齐。比如dist/index.html存在路径就应该指向path.join(__dirname, ../dist/index.html)。第五在本地机器上跑一次npx electron-builder --win或对应的平台命令看整个打包流程是否完整走通。首次打包通常比较慢需要下载依赖耐心等完确认产物生成。第六把生成的安装包装到一台干净机器上测试一次。这一步最重要很多问题在开发机上永远不会暴露因为开发机有各种全局依赖和缓存测试机才是用户真正面对的干净环境。6. 结语与额外建议做 Vue3 Vite Electron 桌面应用这条路我走过很多弯路但整体而言这套技术栈的上手门槛比传统桌面开发低得多投入产出比很高。前端团队不需要额外学习 Java Swing 或 C# WPF就能独立交付一个可安装、可分发、可升级的桌面应用。最后再分享两个小技巧。第一个开发 Electron 应用时给主进程开--inspect调试很多诡异的启动问题能通过 debugger 逐步定位。第二个如果应用涉及文件下载或上传建议在下载过程中用session.defaultSession的will-download事件管理任务进度这个东西文档里写得少但实际用起来非常顺手。根据我个人实操的经验在把应用从 Web 搬到桌面这个过程中最大的成本往往不是写代码而是排查那些环境差异、安全边界和打包细节。这篇内容能把这些问题提前暴露出来让你少花几个通宵。后面如果你们团队也踩到了其他新坑欢迎在评论区交流我会持续补充避坑清单。