Electron 应用打包实战:从 Vite + React 项目到 Windows 安装包

举报
yd_295943643 发表于 2026/10/26 19:13:33 2026/10/26
【摘要】 做桌面端应用时,Electron 几乎是绕不开的技术栈:Chromium 内核负责 UI,Node.js 负责系统能力,两者结合让前端技术栈直接覆盖桌面场景。但真正把一个 Electron 项目从"能开发"推到"能交付",中间横着不少坑。本文以 Vite + React + TypeScript 的

Electron 应用打包实战:从 Vite + React 项目到 Windows 安装包

做桌面端应用时,Electron 几乎是绕不开的技术栈:Chromium 内核负责 UI,Node.js 负责系统能力,两者结合让前端技术栈直接覆盖桌面场景。但真正把一个 Electron 项目从"能开发"推到"能交付",中间横着不少坑。本文以 Vite + React + TypeScript 的项目结构为例,完整走一遍从工程初始化到产出 Windows 安装包的全流程,并重点解释几个最容易踩的雷。

一、为什么选 Vite 而不是 webpack

传统 Electron 模板用的是 webpack,配 Electron 打包插件时构建动辄一两分钟起步,热更新更是要等。Vite 的 dev server 走原生 ESM,冷启动基本是秒级,对 Electron 主进程 + 渲染进程的双进程结构也支持得很好。

关键是理解 Vite 在 Electron 里的两套构建目标:渲染进程用标准 web 配置即可,而主进程是 Node 环境,不能用浏览器端的构建目标,否则会踩到 `require is not defined` 这类问题。

// electron.vite.config.ts

import { defineConfig, externalizeDepsPlugin } from 'electron-vite'

import react from '@vitejs/plugin-react'

export default defineConfig({

main: {

plugins: [externalizeDepsPlugin()],

build: {

rollupOptions: {

input: { index: 'src/main/index.ts' }

}

}

},

preload: {

plugins: [externalizeDepsPlugin()],

build: {

rollupOptions: {

input: { index: 'src/preload/index.ts' }

}

}

},

renderer: {

root: 'src/renderer',

resolve: { alias: { '@renderer': 'src/renderer/src' } },

plugins: [react()],

build: {

rollupOptions: {

input: { index: 'src/renderer/index.html' }

}

}

}

})

`externalizeDepsPlugin()` 是这里最关键的一行:它把 `dependencies` 里的包从打包产物中排除,改为运行时从 `node_modules` 加载。这样打包体积能小很多,同时避免了 Electron 内置 Node 版本与依赖版本不兼容导致的诡异报错。

二、主进程、预加载、渲染进程的三层边界

Electron 的安全模型要求渲染进程默认拿不到 Node 能力,所有系统级操作都必须经过预加载脚本桥接。

// src/preload/index.ts

import { contextBridge, ipcRenderer } from 'electron'

const api = {

getVersion: () => ipcRenderer.invoke('app:getVersion'),

openFile: (filePath: string) => ipcRenderer.invoke('dialog:openFile', filePath)

}

if (process.contextIsolated) {

contextBridge.exposeInMainWorld('electronAPI', api)

} else {

// @ts-ignore

window.electronAPI = api

}

// src/renderer/src/types/global.d.ts

export interface ElectronAPI {

getVersion: () => Promise<string>

openFile: (filePath: string) => Promise<string[] | null>

}

declare global {

interface Window {

electronAPI: ElectronAPI

}

}

打开 `contextIsolation`、关掉 `nodeIntegration`,是 Electron 官方对新项目的默认建议。哪怕是纯内部工具,也建议从第一天就按这个规范写,省得后期重构。

三、打包配置:electron-builder

真正决定产物形态的是 `electron-builder`。一个可用的最小配置:

{

"appId": "com.example.desktop",

"productName": "示例桌面应用",

"directories": { "output": "release" },

"files": ["dist/**/*", "resources/**/*"],

"win": {

"target": [{ "target": "nsis", "arch": ["x64"] }],

"icon": "build/icon.ico"

},

"nsis": {

"oneClick": false,

"allowToChangeInstallationDirectory": true,

"perMachine": false,

"createDesktopShortcut": true

}

}

几个经验值:

• `oneClick: false` 会生成带安装向导的版本,用户可以选择安装路径。国内环境里这比免安装单文件友好得多。

• `files` 数组务必用白名单,只打进 `dist` 和静态资源。默认行为会把整个项目目录塞进去,`node_modules`、源码、测试文件全进包,体积能到几百 MB。

• 图标必须是多尺寸 ICO。直接拿一张 256×256 的 PNG 塞进去,Windows 任务栏会显示成模糊的色块。

• `asar: true` 是默认值,不要随手关掉。关掉后 `app.asar` 变成明文目录,代码直接可读,不利于保护。

四、几个真实踩过的坑

**1. 打包后白屏,开发时一切正常**

这是最常见的问题。原因是开发环境由 Vite 提供页面,打包后 `file://` 协议下的资源路径解析方式不同。解决方式是把渲染进程的 base 设为相对路径:

renderer: {

base: './',

...

}

同时主进程加载窗口时用 `loadFile` 而不是 `loadURL`:

win.loadFile(path.join(__dirname, '../renderer/index.html'))

**2. 动态 import 的原生模块在打包后找不到**

如果项目里用了 `better-sqlite3`、`sharp` 这类含原生二进制的依赖,除了 electron-builder 的 rebuild,还需要在打包配置里显式声明:

"asarUnpack": ["**/node_modules/better-sqlite3/**"]

否则运行时会报 `Cannot find module`。

**3. Electron 版本与依赖的 Node ABI 不匹配**

表现是启动即崩,日志里是一堆 `NODE_MODULE_VERSION` 不匹配。解决办法是固定 Electron 版本,并在 `postinstall` 里加一句 `electron-builder install-app-deps`,让原生依赖按当前 Electron 的 ABI 重新编译。

**4. Windows 上杀不掉进程**

单实例应用的实现里,第二次启动应该把信号传给主窗口而不是新开一个。Windows 上 `app.requestSingleInstanceLock()` 配合 `second-instance` 事件,比自己写锁文件可靠得多。

五、产出物与分发

`electron-builder` 会在 `release` 目录产出三类文件:未签名的安装包、免安装的绿色版目录、以及 `latest.yml`(用于 `electron-updater` 的差分更新)。

如果打算做自动更新,`latest.yml` 必须和安装包一起上传到更新服务器,且 `publish` 配置指向该地址:

"publish": [{ "provider": "generic", "url": "https://example.com/releases" }]

自动更新包必须做代码签名,否则 Windows SmartScreen 会直接拦截,升级体验会非常差。没有签名证书时,至少要在 `nsis` 里配置 `perMachine: false` 并跳过高权限请求,减少被杀软误报的概率。

六、小结

把 Electron 项目交付出去,核心就三件事:构建目标分清楚(主进程 Node、渲染进程浏览器)、依赖外置加白名单、原生依赖按目标 ABI 重编译。剩下的坑基本都集中在路径解析和权限声明上。

工具链本身很成熟,`electron-vite` + `electron-builder` 这套组合已经能覆盖绝大多数中小型桌面应用的发布需求。与其自己拼 webpack 配置,不如把这套经过大量项目验证的脚手架用熟,把时间花在业务本身上。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。