尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

Vue3+TypeScript+Element Plus+Vite+Electron 桌面应用开发实战

发布时间:2026/9/26 17:43:08

资讯中心
01
ARTICLE

Vue3+TypeScript+Element Plus+Vite+Electron 桌面应用开发实战

Vue3+TypeScript+Element Plus+Vite+Electron 桌面应用开发实战
1. 开局梳理为什么非要用这套组合搭桌面端先说结论Vue3 TypeScript Element Plus Vite Electron这五个词放在一起基本就是目前前端开发桌面端应用最舒服的组合之一。Electron 负责把 Web 技术搬到桌面Vue3 负责界面交互TypeScript 负责类型约束和代码可维护性Element Plus 提供现成的后台管理组件Vite 负责开发体验和构建速度。这套组合对写过 Vue 后台系统的人来说几乎是零门槛迁移你今天写增删改查明天就能把同样的逻辑变成一个桌面软件。但要注意网上大量教程都是照着旧版踩过的路写的Electron 版本更新快Vite 插件的生态也在变很多配置已经不是当年的写法了。我下面写的内容全部基于当前比较稳定的版本组合vue^3.4、typescript^5.3、vite^5、electron^30、electron-builder^24、vite-plugin-electron^0.28。如果你用的版本和我不同配置上可能会略有差异但核心思路都是一样的——先把主进程、渲染进程、预加载脚本这三层关系搞清楚配置只是水到渠成的事。这篇文章能帮你解决什么问题从零初始化工程、打通 Vite 与 Electron 的关系、配置 Element Plus 按需引入、写出安全规范的主进程与渲染进程通信、最后打包成安装包。适合哪些人看有 Vue3 项目经验但没碰过 Electron 的前端以及已经能在 Electron 里跑 Hello World 但想梳理清楚体系的新手。如果你第一次听说 Electron我建议你先花十分钟搞清楚它是什么——本质上就是用你的 HTML/CSS/JS 能力包一个能够调用系统 API 的壳这个壳里还套了一个浏览器内核。2. 初始化项目从零搭起工程骨架2.1 用 Vite 创建基础工程先不用管 Electron我们先把 Vue3 TS 的壳子建出来。这里必须用 Vite 官方脚手架自定义配置可以后面再加。npm create vitelatest electron-vue3-ts -- --template vue-ts等依赖安装完先启动一遍确认基础工程没问题npm install npm run dev这一步看似多余其实非常关键。如果直接上来就改 Electron 相关配置万一遇到问题你根本分不清是 Vue 工程的问题还是 Electron 的问题。先确认 Vue 工程能跑后面排查就简单一半。2.2 安装 Electron 与核心依赖然后安装 Electron 本体和通信相关的类型声明npm install electron -D npm install types/node -D这里我强烈建议不要用npm install electron --save-dev同时做一堆事而是分步来。Electron 下载慢是个老问题如果你在公司内网或者网络受限很容易卡在下载二进制文件那一步。提示如果 Electron 下载卡住可以把二进制包的镜像源指到国内镜像。在项目根目录新建.npmrc文件写入electron_mirrorhttps://npmmirror.com/mirrors/electron/再重新安装即可。接着安装 Vite 的 Electron 插件这是让 Vite 认识 Electron 的关键npm install vite-plugin-electron -D这个插件的作用是把你写的 Electron 主进程代码和 preload 脚本也当成 Vite 的构建单元开发时自动帮你编译并启动 Electron打包时帮你输出到指定目录。2.3 设计项目的目录结构很多初学者把主进程代码、渲染进程代码和 preload 脚本全部混在src里结果打包时各种路径错乱。一个清晰的目录结构应该是这样electron-vue3-ts/ ├── src/ # 渲染进程Vue 应用 │ ├── main.ts │ ├── App.vue │ ├── components/ │ └── views/ ├── electron/ │ ├── main.ts # 主进程入口 │ └── preload.ts # 预加载脚本 ├── vite.config.ts ├── tsconfig.json ├── index.html └── package.json主进程和渲染进程分开放这是整套架构的根基。我见过很多人把BrowserWindow的创建代码塞进 Vite 项目的主入口main.ts里那是因为分不清Vue 应用的入口和Electron 主进程的入口。记住一个原则src/main.ts是浏览器页面的 JavaScript 入口electron/main.ts是整个桌面应用的系统级入口两者没有任何关系。2.4 Vite 配置文件的结构化改造打开vite.config.ts我们要在这里同时定义三个构建目标主进程、preload、渲染进程。import { defineConfig } from vite import vue from vitejs/plugin-vue import electron from vite-plugin-electron export default defineConfig({ plugins: [ vue(), electron({ // 主进程入口 entry: electron/main.ts, vite: { build: { outDir: dist-electron/main, sourcemap: false } } }), // 自定义 preload 构建 { name: preload-script, apply: build, // ...preload 相关编译逻辑 } ] })这里需要说明一下vite-plugin-electron插件的配置在不同版本间的差异比较大。我用的 0.28 版本支持在electron()内部传vite配置项来单独控制主进程的编译行为。你安装后最好先看一眼node_modules/vite-plugin-electron/dist/index.d.ts的类型定义再动手写配置。3. 逐个击破主进程、渲染进程与预加载脚本3.1 主进程的本质与分工在 Electron 里主进程是唯一能跟操作系统打交道的进程。它负责创建窗口、管理应用生命周期、调用系统能力如文件对话框、通知、系统托盘。主进程代码直接在 Node.js 环境运行所以你可以用require(fs)、process.platform这些 Node API。一个最简可用的主进程入口长这样import { app, BrowserWindow } from electron import path from node:path function createMainWindow(): BrowserWindow { const mainWindow new BrowserWindow({ width: 1100, height: 750, show: false, webPreferences: { preload: path.join(__dirname, preload.js), contextIsolation: true, nodeIntegration: false } }) mainWindow.once(ready-to-show, () { mainWindow.show() }) // 开发环境加载 Vite dev server 地址 if (process.env.VITE_DEV_SERVER_URL) { mainWindow.loadURL(process.env.VITE_DEV_SERVER_URL) } else { mainWindow.loadFile(path.join(__dirname, ../dist/index.html)) } return mainWindow } app.whenReady().then(() { createMainWindow() app.on(activate, () { if (BrowserWindow.getAllWindows().length 0) { createMainWindow() } }) }) app.on(window-all-closed, () { if (process.platform ! darwin) { app.quit() } })这里有两个细节值得展开。第一show: false配合ready-to-show事件再显示窗口是为了避免白屏闪烁——很多 Electron 应用一启动就白屏半秒就是因为一创建窗口就立刻渲染页面而页面 JS 还没 ready。第二contextIsolation: true和nodeIntegration: false是安全铁律含义是渲染进程里不能直接使用 Node.js API必须在 preload 脚本里通过contextBridge暴露给你需要的函数。3.2 开发环境如何让 Electron 与 Vite 协同工作在开发模式你不需要手动编译主进程再启动 Electron。vite-plugin-electron在我用的版本里已经处理好了当你执行npm run dev时它会把主进程和 preload 编译到内存或磁盘上然后自动启动 Electron 并指向 Vite dev server。这背后的机制值得理解一下Electron 需要通过loadURL加载一个 http 地址而 Vite dev server 默认跑在http://localhost:5173。插件会把VITE_DEV_SERVER_URL注入到主进程的环境变量中这样主进程就能判断当前是开发模式去连 dev server。所以 package.json 的 scripts 可以这样配置{ scripts: { dev: vite, build: vue-tsc vite build, electron:build: npm run build electron-builder } }不需要单独写一个electron .的开发和启动命令插件会搞定。3.3 预加载脚本安全通信的唯一通道很多人忽略 preload 脚本直接在主进程里webPreferences: { nodeIntegration: true }然后在 Vue 组件里import { ipcRenderer } from electron。这种写法在功能上没问题但安全上非常糟糕。如果渲染进程因为加载了远程资源或受到 XSS 攻击攻击者就能直接通过这个ipcRenderer调用系统命令。正确的做法是preload 脚本作为桥梁。它运行在一个既不是主进程、也不是普通渲染进程的特殊环境里可以访问contextBridgeAPI把指定的 IPC 方法暴露给渲染进程。一个安全的 preload 脚本长这样import { contextBridge, ipcRenderer } from electron contextBridge.exposeInMainWorld(electronAPI, { openFile: () ipcRenderer.invoke(dialog:openFile), getAppVersion: () ipcRenderer.invoke(app:getVersion), onWindowClose: (callback: () void) { const listener () callback() ipcRenderer.on(window:close, listener) return () { ipcRenderer.removeListener(window:close, listener) } } })contextBridge.exposeInMainWorld把electronAPI挂到window对象上渲染进程里就能通过window.electronAPI调用。这样做的好处是渲染进程永远拿不到ipcRenderer本身只能调用你暴露的这几个函数攻击面被压到最小。注意preload 脚本编制后必须使用 CommonJS 格式导出因为 Electron 加载 preload 时默认按 Node 模块方式解析。如果你在 preload 里用import语法需要确认编译产物是.cjs或者有对应的打包配置否则会报 Unexpected token export。4. 界面层Vue3 与 Element Plus 的接入细节4.1 按需引入 Element PlusElement Plus 是 Vue3 生态里最成熟的中后台组件库了。它支持全量引入和按需引入两种模式我推荐你直接用按需引入原因不只是体积还有组件样式的打包可控性。需要安装额外的两个插件npm install element-plus npm install unplugin-auto-import unplugin-vue-components -D然后在 Vite 配置里追加import AutoImport from unplugin-auto-import/vite import Components from unplugin-vue-components/vite import { ElementPlusResolver } from unplugin-vue-components/resolvers export default defineConfig({ plugins: [ vue(), AutoImport({ imports: [vue, vue-router], resolvers: [ElementPlusResolver()] }), Components({ resolvers: [ElementPlusResolver()] }) ] })这个配置完成后你在组件里使用el-button、ElMessage不需要手写import语句插件会自动帮你引入对应的组件和样式。这个方案是 Element Plus 官方推荐的长期维护没有问题。不过有一个地方我建议手动引入——全局弹窗类组件比如ElMessage。组件库按需引入时这些手动调用的 API 偶尔会因为上下文里的ElMessage没有被正确注入而失灵。最简单的办法是在main.ts里显式引入一次import { ElMessage } from element-plus app.config.globalProperties.$message ElMessage这样你能在任意组件里用this.$message或proxy.$message避免踩到消息提示不显示的坑。4.2 路由与状态管理的选型桌面应用和 Web 后台在路由状态上有个显著区别——桌面应用需要考虑多窗口场景。虽然很多 Electron 应用就是单窗口但你做架构时要有这个意识每个窗口的渲染进程是独立的Vuex/Pinia 的状态只存在于当前窗口的渲染进程里不会跨窗口共享。我用 Pinia 时习惯把需要共享的数据结构设计成可序列化对象方便以后配合 IPC 做跨窗口同步。比如用户配置数据主进程读配置文件渲染进程通过window.electronAPI拿到数据存进 Pinia store。这样主进程始终是数据权威渲染进程只是展示层。路由方面如果你用的是 Hash 模式打包进 Electron 会非常顺利。如果要用 History 模式需要额外担心file://协议下的路径问题我建议桌面端一律用createWebHashHistory()省心且无副作用。4.3 全局样式与暗黑模式适配Element Plus 自带暗黑模式支持桌面端用户对暗黑主题的期待比 Web 用户高得多。你可以直接用 HTML 的class切换// 在某个设置组件里 const toggleDark () { const html document.documentElement html.classList.toggle(dark) }Element Plus 的暗黑模式约定就是在根节点上添加darkclass并且要引入暗黑主题变量import element-plus/theme-chalk/dark/css-vars.css;我建议在你的全局样式里也定义一套 CSS 变量以--app-bg、--app-text这类前缀命名方便非 Element Plus 组件也跟随暗黑切换。桌面应用长期开着用户工作时间长眼睛环境又昏暗暗黑模式不是锦上添花是刚需。5. IPC 通信主进程与渲染进程的桥梁5.1 从流程图到代码理解双向通信很多初学者把 IPC 理解成主进程能调渲染进程的方法、渲染进程能调主进程的方法这样一个模糊概念。实际上一旦动手写就会晕。我把不同通信场景整理了一下通信方向使用场景推荐 API渲染进程 → 主进程单向通知主进程执行某操作不需要返回值ipcRenderer.send()ipcMain.on()渲染进程 → 主进程双向请求主进程做事并等待结果ipcRenderer.invoke()ipcMain.handle()主进程 → 渲染进程单向给当前窗口发通知如更新进度webContents.send()ipcRenderer.on()主进程 → 渲染进程广播给所有窗口发通知webContents.send()遍历所有窗口在 Vue3 组件里直接写ipcRenderer不是不行但为了类型安全和可维护性我建议在src/types/electron.d.ts里声明 window 上挂载的 API 类型export interface IElectronAPI { openFile: () Promisestring[] getAppVersion: () Promisestring onWindowClose: (callback: () void) () void } declare global { interface Window { electronAPI: IElectronAPI } }这样在 Vue 组件里const list await window.electronAPI.openFile()提交代码时 TypeScript 能自动检查 API 名是否拼错、参数是否缺失比在渲染进程里裸写ipcRenderer可靠得多。5.2 常见代码实例打开系统文件对话框我们以点击按钮选择本地文件并显示路径为例完整走一遍三层代码。主进程里import { app, BrowserWindow, dialog, ipcMain } from electron ipcMain.handle(dialog:openFile, async (event) { const win BrowserWindow.fromWebContents(event.sender) const result await dialog.showOpenDialog(win!, { title: 请选择文件, properties: [openFile, multiSelections], filters: [ { name: 文本文件, extensions: [txt, md, log] }, { name: 所有文件, extensions: [*] } ] }) if (result.canceled) { return [] } return result.filePaths })preload 脚本里暴露contextBridge.exposeInMainWorld(electronAPI, { openFile: () ipcRenderer.invoke(dialog:openFile) })Vue 组件里使用template div el-button typeprimary clickhandleSelectFile选择文件/el-button el-tag v-forfile in fileList :keyfile closable{{ file }}/el-tag /div /template script setup langts import { ref } from vue const fileList refstring[]([]) async function handleSelectFile() { const files await window.electronAPI.openFile() fileList.value files } /script这个链路看着简单但每一步都经过安全边界主进程不信任渲染进程传来的任何路径字符串而是由系统对话框自己返回路径。如果你要做一个渲染进程传路径、主进程读取文件的场景务必在主进程里做路径校验和权限检查不要直接fs.readFileSync(userInputPath)。5.3 性能与安全方面的心得桌面端的 IPC 通信需要注意一下频率。如果你在渲染进程里监听主进程发送的实时数据如进度条更新那么渲染进程里订阅后一定在onUnmounted/onBeforeUnmount时取消订阅否则组件销毁后回调还在执行轻则数据更新无效重则内存泄漏。还有一种常见坑是主进程向一个已经被销毁的webContents发消息控制台会报Object has been destroyed。稳妥做法是在每次发送前先判断const win BrowserWindow.fromWebContents(event.sender) if (win !win.isDestroyed()) { win.webContents.send(update-progress, progress) }6. 打包交付electron-builder 配置与体积优化6.1 为什么选择 electron-builder而不是 Electron ForgeElectron 官方虽然推荐 Forge但在国内开发者的实际反馈中 electron-builder 明显更成熟、配置更直观对多平台打包的支持也更完善。electron-builder 可以通过一个electron-builder.yml或electron-builder.json文件描述打包行为支持自动下载对应平台的构建工具。安装npm install electron-builder -D在package.json里增加build字段或单独写配置文件。我用单独配置文件的方式避免package.json太长# electron-builder.yml appId: com.example.yourapp productName: YourApp directories: output: release/ buildResources: build/ files: - dist/** - dist-electron/** asar: true win: target: - target: nsis arch: - x64 nsis: oneClick: false allowToChangeInstallationDirectory: true createDesktopShortcut: true createStartMenuShortcut: true mac: target: - target: dmg arch: - arm64 - x64这里多讲一句asar。electron-builder 默认会把应用代码打成 asar 压缩包既提升了加载速度也避免了源码完全裸露。但如果你用了 native module编译型的 Node 模块就需要在配置里声明asarUnpack否则打包后模块路径找不到。node-sqlite3这类常见 native 模块就是典型例子。6.2 打包提速的实战技巧Vite 打包慢感觉慢在哪主要是首次冷启动时依赖预构建和路径解析。针对 Electron 项目我推荐从这几个方面提速第一vite 配置里给别名加缓存优化。import path from node:path resolve: { alias: { : path.resolve(__dirname, src) } }第二把 Electron 主进程相关依赖从渲染进程里彻底隔离。不要electron包出现在渲染进程的import语句里否则 Vite 会尝试把 Node 内置模块打进 bundle。第三执行vue-tsc时注意如果项目大类型检查拖慢整体打包。vue-tsc和vite build都是 CPU 密集型任务我在 CI 上会把这两个任务分步执行而不是串在一条命令里。{ scripts: { type-check: vue-tsc --noEmit, build-only: vite build, build: npm run type-check npm run build-only } }本地开发可以用npm run build-only跳过类型检查拿产物验证提交前跑完整build。既不用每个改动都等类型检查又能保证合入前是干净的。6.3 降低打包产物体积的非主流思路Electron 应用体积大是普遍的抱怨。一个空白 Electron 应用打包出来也有 80MB 左右其中大头就在 Electron 的 Chromium 和 Node.js 运行时这个没法压。我们能做的是压缩自己的代码体积。全量引入 Element Plus 会让渲染进程的 JS 产物直接飙到 1MB 以上按需引入后能小不少。另外Vite 默认的代码分割行为在 Electron 的file://协议下偶尔会出问题如果遇到打包后白屏但控制台没有 JS 报错的情况多半是动态 import 的 chunk 路径不对。此时最简单的解法是关掉 chunk 分割build: { rollupOptions: { output: { manualChunks: undefined } } }代价是产物稍微大一些但路径问题彻底消失。桌面应用不存在 Web 场景下首屏只加载必要分包的强烈诉求所以这个取舍值得做。6.4 注意打包时的 gc 与内存问题热搜词里有人提到--expose-gc参数和打包软件占用内存的问题。这确实是 Electron 打包时一个实际存在的坑——在内存较小的机器上执行 electron-builder 时如果同时又跑着vite build很容易 OOM。并且在运行时如果你在 Electron 主进程里设置了定时器或大量缓存对象GC 回收不及时内存占用会越来越高。可以这样定期获取内存指标方便定位问题import { app } from electron const mem app.getAppMetrics()[0] console.log(mem.memory.workingSetSize) // 单位 KB如果是渲染进程导致的泄漏更多要检查 Vue 组件的订阅和定时器清理这个问题我们前面 IPC 一节已经提到过。7. 高频问题排查清单与避坑记录7.1 Vite 里报 process is not defined这是所有 Electron Vite 新手必踩的坑。报错信息大概是Uncaught ReferenceError: process is not defined原因很纯粹你打包后的渲染进程 JS 还在引用 Node 的全局变量process但渲染进程跑在浏览器环境里根本没有process。解决思路有两条。第一代码里确认没有写process.env.NODE_ENV之类。Vite 里判断环境用import.meta.env不要用process.env。第二如果确认是某些第三方库引用了process可以在 Vite 配置里显式兼容define: { process.env: {} }但这只是一种堵漏式处理最干净的还是从源头消灭渲染进程里的 Node 依赖。HTML 里如果用了纯静态process.env.REACT_APP_XXX写法也会被带进来检查一下index.html就行。7.2 开发时局域网打开空白如果你在 dev 模式下把 Vite dev server 跑在局域网里Electron 窗口加载后可能白屏。这是因为 Vite dev server 默认绑定localhost局域网内的设备无法访问。在vite.config.ts里加server: { host: true }就会监听0.0.0.0。但注意Electron 主进程加载的VITE_DEV_SERVER_URL是http://localhost:5173在渲染进程内部使用没问题。如果你真的要在手机或另一台电脑上打开这个 dev serverElectron 里不常见URL 要换成局域网 IP。7.3 打包后应用白屏的经典三连如果是打包后应用白屏按这个顺序排查第一看主进程的loadFile路径是否正确。我上面代码里用的是path.join(__dirname, ../dist/index.html)前提是主进程产物输出在dist-electron/main。如果你的主进程产物输出到了别的目录相对路径就得跟着改。第二检查 Vite 的base配置。默认base: /在浏览器没问题但在 Electron 的file://协议下会导致 JS/CSS 资源路径指向file:///C:/.../assets/xxx.js这类绝对根路径直接加载失败。要么手动设置base: ./要么保持默认配置但确认你的loadFile路径和index.html引用资源的相对路径能对上。我的建议是统一设置相对路径base: ./这是最保险的组合。第三检查 console 里是否有Not allowed to load local resource报错。这说明有资源尝试用 web 协议加载本地文件。有可能是你在渲染进程里直接import img from /assets/xxx.png并引用到了正确做法是把这类静态资源交给 Vite 打包不要让人手动写相对路径。还有一种是开发模式正常、打包后白屏的隐性原因CSP 安全策略。如果你在 HTML 或 meta 里设置了严格的Content-Security-Policy可能阻止了内联脚本或远程资源。桌面端如果不是特别敏感的场景不建议把 CSP 设得太严否则排查起来非常痛苦。7.4 vue-tsc 版本兼容问题热搜词里特别提到了vue-tsc: ^1.8.27和typescript: ^5.3.3的组合。这个组合目前比较稳但如果你新建项目模板默认生成的版本号可能不同。如果vue-tsc报类型错误与你项目无关比如某个第三方库的类型声明不兼容先别急着改业务代码尝试把vue-tsc升级到最新版很多时候是版本间对 TS 语法支持有差异导致的。个人建议把vue-tsc和typescript都保持 major 版本一致比如 TypeScript 5.x 就都用 5.x不要混用 4.x 和 5.x 的类型检查器否则极易出现模板类型校验失败。7.5 我积累的几个实用小技巧调试主进程时在electron/main.ts里加一行process.env.ELECTRON_RENDERER_URL || 开发模式下打印一下能快速排查Electron 连不上 Vite的问题。主进程代码改动在开发模式下不会自动重启你需要手动关掉 Electron 再重新执行npm run dev。如果觉得麻烦可以用electron-renderer相关的自动化重启方案但多数项目没必要。Element Plus 的表单校验在 Electron 里偶尔因为input事件时序导致校验闪烁解决方案是用:validate-on-rule-changefalse并单独控制validate时机。所有窗口 UI 的contextmenu事件在 Electron 里默认是禁用的。如果你想实现右键菜单要么用 Electron 原生的Menu要么在渲染进程里自己监听contextmenu事件并preventDefault。否则用户右键完全没反应体验很怪异。生产环境下如果某个渲染进程页面加载 3 秒还没有执行完 JS可以在主进程里做超时判断并重新loadURL。桌面应用的网络环境远没有 Web 稳定重试机制很实用。8. 最后的几句实在话折腾这套组合一段时间后我最大的感受是Electron 开发的核心难点其实不在于某个 API 怎么写而在于进程思维。Vue3、Vite、Element Plus 这些都是前端工程师的舒适区真正要花时间的是理解主进程和渲染进程的边界以及那句永远被低估的配置——contextIsolation: true。如果一开始就用 nodeIntegration 写起来确实省事但等应用变大、需要面对安全问题的时候你一定会回来重构。我给新手的建议是第一周先不要追求炫酷功能就认认真真把窗口创建、菜单改造、文件读写、系统托盘、IPC 通信这几个基础能力啃下来。把这个骨架搭扎实后续接业务功能基本是一马平川。我后面也会把自己项目中比较有意思的部分比如多窗口管理、自定义系统菜单、打包时自动下载 Electron 镜像的 CI 流程单独拿出来写写。到时候再聊。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。