ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Electron + Vue 项目结构搭建:主进程、渲染进程与IPC通信实践

Electron + Vue 项目结构搭建:主进程、渲染进程与IPC通信实践

我最早是在做一个后台管理系统的时候碰上了 Electron。当时需求很明确:系统本身是 Vue 写好的网页,但客户希望在 Windows 桌面上能直接双击打开、离线可用、还能调用一些浏览器里做不了的系统能力,比如文件拖拽和本地目录读写。如果单独做一套桌面客户端,等于推翻重来;用 Electron 给 Vue 套一层壳,是最顺的路径。最近又帮一个新手团队把这个结构重新搭了一遍,踩了不少坑,顺手把整个搭建流程和背后的考虑写清楚。这篇文章不搞长篇大论,就聚焦在“Electron 构建 Vue 项目基本结构”这个主题上,把这几个问题讲透:为什么要这样搭、脚手架怎么选、主进程和渲染进程怎么分工、Vue 在桌面环境里有哪些必须处理的细节、打包发布前要注意什么。

不管你是刚接触 Electron 的 Vue 开发者,还是打算把现有网页改造成桌面软件的老手,这份内容都能直接对照着操作。我尽量把每一步背后的为什么也说清楚,不只是丢一条命令给你。

1. 为什么是 Electron + Vue,方案选型怎么想

1.1 Electron 到底解决什么问题

很多人一上来就纠结 Electron 和原生客户端的区别。其实 Electron 的本质就是一个“浏览器壳”,它内嵌了完整的 Chromium 内核和 Node.js 运行时,前端代码跑在渲染进程里,系统底层能力由主进程通过 Node.js 的 API 暴露出来。你用 Vue 写的页面,本质上还是在写网页,只是这个网页被 Electron 装进了桌面窗口里。

这个方案最大的价值在于技术栈复用:一个前端团队不需要重新学 C++ 或 Rust 就能交付桌面应用,业务逻辑、UI 组件、状态管理全部沿用 Web 端的既有资产。我见过很多团队就是因为这个原因选 Electron 而不是其他方案。代价是打包体积偏大、内存占用偏高,但在大部分企业管理类和工具类软件场景里,这些缺点完全可接受。

在你动手之前,心里要有数:Electron 适合做“交互重、界面复杂、更新频繁”的应用,比如编辑器、聊天工具、数据看板;不适合做“极简且性能敏感”的工具,比如文本编辑器插件、后台驻留服务。搞清楚这一点,你才知道后面的一些取舍该怎么定。

1.2 渲染层为什么用 Vue 而不是别家

Electron 本身不限制前端框架,原生 JavaScript、React、Vue、Svelte 都能跑。选 Vue 的核心理由是它的渐进式设计:你可以只在某个页面里引入 Vue,也可以做一个完整的单页应用;它的响应式系统和单文件组件结构,跟 Electron 的窗口 + 页面模型匹配得非常好。

具体到工程上,Vue 生态里的路由、状态管理、组件库都是现成的。做中后台管理系统的话,市面上有大量免费的开源管理模板可以改造成 Electron 渲染层,几天就能出一个能看的桌面端原型。再加上 Vue 的开发者工具在 Electron 渲染进程里是可以直接调试的,这一点对排查问题非常有用。

如果你问我 React 或原生能不能用,当然能。但这个主题定的就是 Vue,所以后续内容我全部以 Vue 3 + Vite 技术栈为基线展开,这也是目前创建新项目时最合理的选择。

1.3 脚手架方案对比:快速搭建还是手动集成

搭建 Electron + Vue 项目主要有三条路,我整理了一张对比表格:

方案优势劣势适合场景
electron-vite 官方脚手架主进程、预加载、渲染层三端统一构建,热更新完善需要适应它的目录约定新项目、常规桌面应用
已有 Vue 项目 + 手动引入 Electron不改动现有业务代码逻辑需要自己处理通信、打包、路径等大量细节把现有网页改造成桌面端
Vue CLI + electron-builder 插件老一批教程常用,生态较成熟技术栈偏旧,Vite 生态已经明显更优维护老项目时有参考价值

我个人的建议是:新项目一律用 electron-vite。它内置了主进程、预加载脚本、渲染进程三套独立的构建配置,开发时主进程和 renderer 同时启动,改主进程代码能自动重启 Electron,改 Vue 页面走 Vite 的热更新,整个开发体验非常顺滑。这篇博文后面所有操作也基于这个方案。

2. 从零初始化 Electron + Vue 项目

2.1 环境检查与准备

动手之前,先确认本机的环境。Node.js 版本建议 18 以上,我用的是 20 LTS,整个过程没有遇到兼容性问题。检查命令:

node -v npm -v

如果你还在用 npm 6 或者更老的 Node,建议先用 nvm 切换到新版本,不要在这上面节省时间。Electron 相关的依赖包对 Node 版本有要求,老版本经常会出现模块编译失败的情况。

还有一点要提前说:因为 Electron 本身要下载二进制文件,国内网络环境下安装依赖时经常卡住。解决办法是设置 npm 镜像和 Electron 镜像,在项目根目录下的.npmrc文件里写入:

registry=https://registry.npmmirror.com electron_mirror=https://npmmirror.com/mirrors/electron/

这两行配置能避免大半的下载超时问题。如果不设置,你可能会反复看到安装进程卡在download electron这个地方,非常折磨人。

2.2 用 electron-vite 创建一个可直接运行的工程

electron-vite 官方提供了一条脚手架命令,直接执行:

npm create @quick-start/electron@latest

命令运行后会询问项目名称,我建议不要用带大写字母的名字,包名规范一点后面打包时省不少麻烦。接下来会让你选择项目模板,这里直接选vue模板,TypeScript 选或不选都可以,新手的话我建议直接选上,ESLint 也可以默认装上,能做最基本的代码规范检查。

生成完毕后的目录结构大致长这样:

my-electron-app/ ├── src/ │ ├── main/ # 主进程代码 │ │ └── index.ts │ ├── preload/ # 预加载脚本 │ │ └── index.ts │ └── renderer/ # Vue 渲染进程 │ ├── src/ │ │ ├── App.vue │ │ ├── main.ts │ │ └── components/ │ └── index.html ├── electron.vite.config.ts # 三端构建配置 ├── electron-builder.yml # 打包配置 └── package.json

这个目录结构就是 Electron + Vue 项目的基本骨架,后面所有的业务代码都往对应目录里放,千万不要乱放,后面讲每个目录的职责你会更清楚为什么这么分。

然后安装依赖:

npm install

依赖装完后启动开发模式:

npm run dev

如果一切顺利,你会看到一个默认窗口弹出来,这就是最基础的 Electron + Vue 可运行工程。

2.3 安装依赖踩过的坑

第一次执行npm install时,最常见的两个坑一个是 Electron 二进制下载失败,另一个是 electron-vite 或某些原生模块编译失败。

二进制下载失败,症状就是安装进度一直停在 Electron 的下载阶段,或者提示超时。解决办法就是前文提到的.npmrc配置。第二个坑通常出现在 Node 版本不对时,比如某些模块需要 Node 18+,但你用的是 16,编译时会报gyp ERR之类的错误,这种问题升级 Node 版本基本就解决了。

还有一个小提示:如果你在 Windows 上安装时看到类似 “electron 安装失败” 的错误,可以先手动删除项目里的node_modules,清理 npm 缓存后再安装。命令:

rm -rf node_modules package-lock.json npm cache clean --force npm install

遇到过好多次了,这种“先彻底清一遍”的笨办法比各种花式排查靠谱得多。

3. 主进程、预加载、渲染层如何分工

3.1 三个目录,三种角色

Electron 应用里最核心的概念是“进程”,这个不搞懂,后面一写功能就乱。简单说:

  • 主进程:应用的大脑,负责创建窗口、管理生命周期、操作系统 API、菜单、托盘等。它运行在 Node.js 环境里,可以随意使用文件系统、网络、子进程等能力。
  • 渲染进程:浏览器环境,跑 Vue 页面。它基本等同于一个 Chrome 标签页,无法直接使用 Node.js API(出于安全考虑)。
  • 预加载脚本:主进程和渲染进程之间的“信使”,运行在一个特殊的上下文里,通过contextBridge把安全封装好的 API 暴露给渲染进程。

对应的就是src/main、src/renderer、src/preload三个目录。这个结构不光是 electron-vite 的约定,也是 Electron 官方推荐的最佳实践。

3.2 预加载脚本与 contextBridge 安全桥接

早期 Electron 教程里有种做法是直接在渲染进程里开启nodeIntegration: true,让 Vue 组件里直接require('fs')读文件。这在原型开发时很快,但非常危险——万一页面里有用户的不可信输入,攻击者就能通过你的页面直接操作使用者电脑,属于典型的严重漏洞。现在新项目里建议全部禁用 nodeIntegration,统一走contextBridge。

预加载脚本的作用就是安全地暴露主进程的能力。在src/preload/index.ts里这样写:

import { contextBridge, ipcRenderer } from 'electron' const api = { getAppVersion: () => ipcRenderer.invoke('app:get-version'), openExternal: (url: string) => ipcRenderer.invoke('shell:open-external', url) } if (process.contextIsolated) { contextBridge.exposeInMainWorld('api', api) }

然后在 Vue 组件里通过window.api来调用:

const version = await window.api.getAppVersion()

这个模式既能保证渲染进程拿不到完整的 Node 能力,又能把主进程的能力以白名单方式安全传递下去。把预加载脚本当成“统一出口”来用,后面维护 IPC 通信会变得特别清晰。

3.3 IPC 通信:渲染进程和主进程怎么互相喊话

Electron 的 IPC(Inter-Process Communication)是双向的。渲染进程往主进程发消息,常用ipcRenderer.invoke+ipcMain.handle,适合一问一答式调用;如果只是单向通知,可用send+on。

主进程侧注册接口:

import { ipcMain, shell } from 'electron' ipcMain.handle('app:get-version', (event) => process.env.npm_package_version) ipcMain.handle('shell:open-external', async (event, url: string) => { await shell.openExternal(url) })

这里有个经验:所有 IPC 通道都用域:动作的格式命名。比如app:get-version、user:login、file:save。项目大了之后会有几十个 IPC 通道,统一的命名规则能让你一眼看出这个接口是哪个模块在用什么功能,否则后面找接口就是你最痛苦的事情。

主进程向渲染进程主动推送消息的场景(比如主进程监听某个文件变化后通知页面更新),用webContents.send:

mainWindow.webContents.send('file:changed', filePath)

渲染进程里对应监听:

window.api.onFileChanged((path) => { console.log('文件变更:', path) })

如果不需要持续监听,记得在组件卸载时取消监听,否则会造成内存泄漏。

4. Vue 页面在桌面环境里的关键适配

4.1 资源路径问题:为什么打包后页面白屏

很多人的 Electron + Vue 项目开发时一切正常,一打包就白屏。十有八九是 Vite 的 base 路径没配好。网页项目部署在服务器上,资源路径可以用绝对路径/assets/xxx.js;但 Electron 打包后是通过file://协议加载本地文件的,根路径/对应的不是你的项目目录,这就导致资源加载失败。

解决办法是在electron.vite.config.ts里给渲染进程设置base: './',用相对路径方式引用所有资源:

export default defineConfig({ main: { ... }, preload: { ... }, renderer: { base: './' } })

这样打包出来的 HTML 里引用资源用的就是相对的./assets/xxx.js,在file://协议下能正确加载。只要是 Electron 项目,这个配置几乎是必须的。

4.2 路由模式:为什么强烈建议用 hash 路由

Vue Router 默认的 history 路由适合网站,因为服务器可以配置 fallback 把所有未知路径重定向到入口 HTML。但 Electron 加载本地文件时没有服务器做这个重定向,你点一下页面里的<a>或者手动改路由,一旦刷新页面,很容易变成白屏或者直接提示找不到文件。

所以 Electron + Vue 项目里,路由模式统一用:

import { createWebHashHistory, createRouter } from 'vue-router' const router = createRouter({ history: createWebHashHistory(), routes: [...] })

hash 模式下路由变化靠#后面的内容记录,刷新页面时不会发起新的文件请求,在桌面环境下稳得很。网上很多人在 Electron 里遇到“刷新页面白屏”“直接打开某个路由路径打不开”的问题,根源基本就是这里。

4.3 跨域与接口请求:别再直接在渲染进程发请求

Node.js 渲染进程其实是一个正常的浏览器环境,页面里的fetch/axios依然受同源策略限制。你在网页开发时可以通过后端配置 CORS 解决,在 Electron 开发环境里也能通过webSecurity: false这种配置临时处理,但千万不要在正式环境里这么干——关闭这个就等于给用户留了一个安全后门。

比较推荐的做法分两种:

  • 如果只是请求某个 HTTP 接口,而接口服务允许跨域,那么正常请求即可。
  • 如果接口不允许跨域,或者需要带本地文件路径访问,就通过 IPC 把请求交给主进程完成。主进程里没有跨域限制,请求完再把结果返回给渲染进程。

比如下载一个远程文件:

// 主进程 ipcMain.handle('net:download', async (event, url, savePath) => { const res = await fetch(url) const buffer = Buffer.from(await res.arrayBuffer()) await fs.promises.writeFile(savePath, buffer) return { ok: true } })

这样所有和系统强相关的操作都收口到主进程,Vue 里只用调一个封装好的方法就行。

5. 窗口配置与调试技巧

5.1 定制窗口的基础配置

Electron 的窗口体验和普通浏览器差别很大,你可以在src/main/index.ts里通过BrowserWindow配置很多参数。常见几个:

const mainWindow = new BrowserWindow({ width: 1200, height: 800, minWidth: 900, minHeight: 600, autoHideMenuBar: true, webPreferences: { preload: join(__dirname, '../preload/index.js'), contextIsolation: true, nodeIntegration: false } })

autoHideMenuBar: true是我用得非常顺手的一个配置,它会自动隐藏默认菜单栏,视觉上更像原生应用。如果你做的是全屏看板类应用,可能还需要设置fullscreen: true或者kiosk: true。

窗口打开时的加载逻辑也有讲究,开发环境和生产环境加载方式不同,需要这样处理:

if (process.env.ELECTRON_RENDERER_URL) { mainWindow.loadURL(process.env.ELECTRON_RENDERER_URL) } else { mainWindow.loadFile(join(__dirname, '../renderer/index.html')) }

ELECTRON_RENDERER_URL是 electron-vite 开发模式下自动注入的环境变量,指向本地开发服务器;生产环境则直接加载打包后的 HTML 文件。

5.2 调试方法:renderer 和 main 分开看

Electron 开发时的调试体验其实比很多原生桌面框架好。渲染进程就是普通的 Chromium DevTools,在窗口里按Ctrl+Shift+I就能调出来,你能像调试网页一样检查 DOM、看网络请求、打断点。

主进程的调试要麻烦一点。electron-vite 在开发模式下会以 Node.js 调试端口运行主进程,你可以打开 Chrome 地址栏输入chrome://inspect来调试主进程代码。但说实话,日常开发中主进程用console.log输出日志基本够用,因为主进程逻辑通常不复杂,大多数问题都出在 IPC 和路径上。

还有一个我常用的技巧:在窗口创建完成时自动打开 DevTools,省去手动按快捷键:

mainWindow.webContents.openDevTools()

平时建议留着,需要发布时才删掉。

6. 打包与发布前的常见问题实录

6.1 electron-builder 配置要点

electron-vite 脚手架默认集成了 electron-builder,打包配置写在electron-builder.yml里。一份最基础的 Windows 打包配置:

appId: com.example.myapp productName: MyApp directories: buildResources: build files: - '!**/.vscode/*' - '!src/*' - '!electron.vite.config.{js,ts,mjs,cjs}' - '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}' - '!{.env,.env.*,.npmrc,pnpm-lock.yaml}' win: target: - nsis nsis: oneClick: false allowToChangeInstallationDirectory: true createDesktopShortcut: true

执行:

npm run build npm run build:win

然后dist目录下就会生成安装包和免安装版。这里有个容易踩的坑:打包机最好和发布目标系统一致,如果你在 macOS 上打 Windows 包,需要额外配置 Wine 和 Cross 编译环境;如果不搞 CI,建议直接在 Windows 机器上跑build:win。

6.2 常见问题速查表

下面这张表是我这几年在 Electron + Vue 项目里高频遇到并排查过的坑,直接对着看能省很多时间:

现象原因解决方案
打包后白屏Vite base 路径是绝对路径设置base: './'
刷新页面白屏使用了 history 路由切换到 hash 路由
页面请求接口报跨域渲染进程作为浏览器受同源策略限制请求收口到主进程发
Electron 安装失败二进制下载慢或超时配置镜像源
窗口显示不出来主进程代码报错但控制台看不到用--enable-logging启动或看终端日志
打包后文件体积巨大Electron 内嵌了完整 Chromium换小体积运行时,或接受体积做增量更新
Node 模块报错Node 版本兼容问题升级到 Node 18+

可能你还会遇到打包后第一次启动很慢的问题。多数情况下是杀毒软件扫描安装目录导致的,跟代码没关系。可以考虑引导用户把应用加入白名单,或者做静默启动优化。

6.3 实际经验小建议

最后分享几条我实操过程中的个人习惯,不一定适合所有人,但至少能帮你少走弯路:

第一,主进程的代码尽量精简。很多人喜欢把所有业务逻辑都塞到主进程里,这种做法短期内没有问题,但后面窗口变多、功能变多,主进程会变成一团乱麻。我的习惯是主进程只做窗口管理和系统能力调用,业务逻辑尽量留在 Vue 层,通过 IPC 接口操作。这样主进程代码基本不变,大部分开发可以像写网页一样在渲染进程里完成。

第二,预加载脚本一定要做类型声明。如果你用 TypeScript,在src/preload/index.d.ts里声明window.api的接口类型,这样 Vue 组件里写window.api的时候会有完整的类型提示,效率高很多,还不容易写错方法名。

第三,开发模式和打包模式不要混在一个环境里调试。经常有人在开发模式里设置了--no-sandbox,或者用了比较激进的跨域配置,最后打包出来出现和开发环境表现不一致的问题。开发和生产配置从一开始就分开,并且打包后一定要做一次全功能冒烟测试。

现在再看 Electron + Vue 这个项目结构,其实核心思路就是一句话:分清主进程和渲染进程的职责,用预加载脚本搭一座安全的桥,让 Vue 专注做界面,其余的系统能力全部交给主进程。遵循这个结构,你会发现桌面应用开发并没有比写网页复杂太多,但接手这些代码的人会觉得非常清晰。如果你正准备把一个 Vue 项目包装成桌面应用,从这套结构开始,后面会顺畅很多。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进