1. 为什么 Vue3 项目需要 Electron 桌面化?
很多 Vue3 项目最初都是标准 Web 应用:开发时跑 Vite,部署时把 dist 交给 Nginx,接口通过 /api 反向代理,页面由浏览器承载。这个模式非常成熟,但一旦应用进入桌面端交付场景,问题会集中暴露。
最典型的变化不是“页面从浏览器搬进 Electron”,而是运行边界变了:
- 浏览器窗口变成了可编程的
BrowserWindow。 - 单页面应用变成了可以同时打开多个窗口的桌面端应用。
/api代理不再天然存在,file://会成为真实生产环境。- 用户希望拿到的是
.exe、安装包或绿色版,而不是一套启动说明。 - 多显示器、全屏、快捷键、窗口编排等能力需要由桌面容器托管。
一个“示例项目”的桌面化目标可以概括为:
同一套 Vue3 页面
↓
Web 模式:Nginx 托管静态资源
↓
Desktop 模式:Electron 加载本地文件
↓
打包为 Windows .exe / NSIS 安装包 / zip 绿色版
↓
支持多窗口展示系统和多显示器自动布局
这也是 Vue3 + Electron 的价值所在:不推翻已有前端体系,只在运行容器、窗口管理和交付链路上补齐桌面端能力。
桌面化不是把网页套一层壳,而是把“浏览器替你做的事”重新显式设计出来。
整体思路如下:
本章结论:如果需求只是在浏览器里访问页面,Electron 没有必要;如果交付目标包含窗口控制、多屏展示、本地运行和 .exe 安装包,Electron 就不是装饰层,而是新的运行时。
2. Web → Desktop 的第一个坑:file:// 运行环境差异
桌面化改造中第一个高频问题是:Web 开发环境一切正常,打包成 Electron 后页面白屏。
这个问题很典型,现象通常是:
npm run dev正常。npm run build后放到 Nginx 正常。- Electron 开发模式正常。
- Electron 生产包打开后白屏,或者控制台出现资源加载失败。
经典踩坑案例:为什么 Web 正常,Electron 白屏?
先看一个常见 Vite 默认输出:
<!-- 典型问题:资源路径以 / 开头 -->
<script type="module" src="/assets/index.js"></script>
<link rel="stylesheet" href="/assets/index.css">
在浏览器和 Nginx 下,/assets/index.js 表示站点根目录资源,没问题。但 Electron 生产环境常用:
// 生产包直接加载本地文件。
win.loadFile(path.join(__dirname, '../dist/index.html'))
页面协议会变成:
file:///.../dist/index.html
此时 /assets/index.js 不再是 Nginx 站点根目录,而会被理解为文件系统根路径下的资源。结果就是资源找不到,页面白屏。
解决一:Vite base 使用相对路径
核心修复是把 Vite 的资源基路径改成相对路径:
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
// 关键配置:让构建产物中的 assets 使用相对路径。
// 这样同一份 dist 可以同时被 Nginx 托管,也可以被 Electron file:// 加载。
base: './',
})
构建后资源会变成:
<!-- 修复后:资源路径相对于 index.html -->
<script type="module" src="./assets/index.js"></script>
<link rel="stylesheet" href="./assets/index.css">
解决二:使用 hash 路由
第二个问题是路由。Web 应用如果使用 history 路由,刷新 /dashboard 依赖服务端 rewrite;但 Electron 生产环境加载的是本地 index.html,没有 Nginx 帮你兜底。
更稳的方式是使用 hash 路由:
file:///.../dist/index.html#/control
file:///.../dist/index.html#/visual
file:///.../dist/index.html#/info
Electron 主进程可以通过 loadFile 的 hash 参数打开指定页面:
function loadRoute(win: BrowserWindow, route: string) {
// 开发模式加载 Vite dev server。
if (process.env.VITE_DEV_SERVER_URL) {
const url = new URL(process.env.VITE_DEV_SERVER_URL)
// 统一使用 hash 路由,开发和生产保持同一套路由策略。
url.hash = route
win.loadURL(url.toString())
return
}
// 生产模式加载本地 dist/index.html。
win.loadFile(path.join(__dirname, '../dist/index.html'), {
// 通过 hash 打开指定 Vue 路由。
hash: route,
})
}
现象 → 原因 → 解决
| 现象 | 原因 | 解决 |
|---|---|---|
| Electron 生产包白屏 | 静态资源以 /assets 绝对路径加载 | Vite 设置 base: './' |
| Web 路由正常,桌面端深层路由失败 | file:// 下没有服务端 rewrite | 使用 hash 路由 |
| 开发模式正常,打包后异常 | 开发模式走 Vite server,生产走本地文件 | 开发和生产都按 hash 路由设计 |
本章结论:file:// 是 Electron 生产环境的第一道分水岭,所有路径、路由和资源加载都要按“本地文件运行”重新审视。
3. 第二个坑:Web / Desktop API 分裂问题
解决白屏后,第二个问题很快出现:页面出来了,但接口全部失败。
在 Web 模式下,前端经常这样请求:
// Web 模式下通常没问题,因为 /api 会被 Nginx 或 Vite proxy 转发。
axios.get('/api/users')
但在 Electron 生产环境里,页面是:
file:///.../dist/index.html
此时 /api/users 不再等价于 Nginx 代理地址。桌面端没有天然的 /api 代理层,接口地址必须变成完整地址,例如:
https://api.example.com/api/users
这就是 Web / Desktop 的 API 分裂问题:
错误做法:到处写 if Electron
最容易想到的写法是到处判断:
// 不推荐:判断逻辑散落在请求代码里,后期很难维护。
const baseURL = window.electronAPI
? 'https://api.example.com/api'
: '/api'
这种写法短期能跑,长期会把运行环境判断扩散到请求层、WebSocket 层、页面层和调试脚本里。
真正需要的是一个统一的运行时抽象层。
本章结论:Web 和 Desktop 的差异不应该散落在页面代码里,应该被收敛到一个专门的 runtime layer。
4. 核心架构设计:运行时抽象层(runtime layer)
运行时抽象层的目标很明确:页面不关心自己跑在浏览器还是 Electron,只从一个统一配置里读取 API、WebSocket 和模式信息。
设计后,调用方只依赖:
import { runtimeConfig } from '@/config/runtime'
apiClient.defaults.baseURL = runtimeConfig.apiBaseUrl
而不是在每个模块里判断 file:// 或 window.electronAPI。
runtimeConfig 示例
// src/config/runtime.js
// Desktop 模式下的默认 API 地址。这里使用公开占位域名,实际项目按环境注入。
const DESKTOP_DEFAULT_API_BASE_URL = 'https://api.example.com/api'
// Desktop 模式下的默认 WebSocket 地址。
const DESKTOP_DEFAULT_WS_URL = 'wss://api.example.com/ws'
// Electron 生产包通常通过 file:// 加载页面。
const isFileRuntime = window.location.protocol === 'file:'
// preload 暴露的 window.electronAPI 也可以作为 Electron 运行时信号。
const isElectronRuntime = Boolean(window.electronAPI) || isFileRuntime
function resolveApiBaseUrl() {
// Web 模式默认使用相对路径,让 Nginx 或 Vite proxy 转发。
const value = import.meta.env.VITE_API_BASE_URL || '/api'
// Electron file:// 下没有代理层,不能继续使用 /api。
if (isElectronRuntime && value.startsWith('/')) {
return DESKTOP_DEFAULT_API_BASE_URL
}
// 如果构建时已经注入完整地址,则直接使用外部配置。
return value
}
function resolveWsUrl() {
// WebSocket 一旦通过环境变量指定,就优先使用显式配置。
const value = import.meta.env.VITE_WS_URL || ''
if (value) return value
// Electron 模式给出完整地址,避免 file:// 下拼接出错误 URL。
if (isElectronRuntime) return DESKTOP_DEFAULT_WS_URL
// Web 模式可以为空,由具体模块决定是否启用。
return ''
}
export const runtimeConfig = {
// mock 适合演示和本地 UI 开发,api 适合接口联调。
dataMode: import.meta.env.VITE_DATA_MODE || 'mock',
apiBaseUrl: resolveApiBaseUrl(),
wsUrl: resolveWsUrl(),
isElectronRuntime,
}
Web 与 Desktop 的职责边界
环境变量策略
# 数据模式:mock 用于演示,api 用于真实接口联调。
VITE_DATA_MODE=api
# Web 模式可以保留 /api,由 Nginx 或 Vite proxy 转发。
VITE_API_BASE_URL=/api
# Desktop 模式建议注入完整 WebSocket 地址。
VITE_WS_URL=wss://api.example.com/ws
注意:Vite 只会把 VITE_ 前缀变量注入渲染进程。主进程如果也需要读取配置,需要通过 process.env 获取,再通过 preload 暴露必要字段。
本章结论:runtime layer 的价值不在于多写一个配置文件,而在于把“运行环境差异”从页面代码里隔离出去。
5. Electron 三进程模型工程实践(main / preload / renderer)
Electron 工程质量很大程度取决于三进程边界是否清晰。
vite-plugin-electron 接入方式
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import electron from 'vite-plugin-electron/simple'
export default defineConfig(({ mode }) => {
// 只有 Electron 模式才启用主进程和 preload 构建。
const isElectron = mode === 'electron'
return {
// 同时兼容 Nginx 和 file://。
base: './',
plugins: [
vue(),
isElectron && electron({
main: {
// 主进程入口。
entry: 'electron/main.ts',
},
preload: {
// preload 入口。
input: 'electron/preload.ts',
},
}),
].filter(Boolean),
server: {
// Electron 开发模式建议固定端口,避免主进程连接错服务。
port: 5173,
strictPort: isElectron,
proxy: {
'/api': {
// 示例占位地址,实际项目按环境配置。
target: 'https://api.example.com',
changeOrigin: true,
ws: true,
},
},
},
}
})
实际 package.json 不支持注释。为了便于解释,下面使用 jsonc 展示脚本意图:
{
"scripts": {
// 纯 Web 开发。
"dev": "vite",
// 纯 Web 静态构建。
"build": "vite build",
// Electron 开发模式,renderer HMR,main/preload 自动重启或重载。
"electron:dev": "vite --mode electron",
// Electron 生产构建,再交给 electron-builder 打包。
"electron:build": "vite build --mode electron && electron-builder --win --linux --x64"
}
}
preload:只暴露最小 API
preload 是安全边界,不是便利通道。不要暴露 ipcRenderer 本体,也不要让页面直接获得 Node.js 能力。
// electron/preload.ts
import { contextBridge, ipcRenderer } from 'electron'
const electronAPI = {
// 获取应用版本号。
getVersion: () => ipcRenderer.invoke('app:get-version'),
// 获取运行平台,例如 win32 / linux / darwin。
getPlatform: () => ipcRenderer.invoke('app:get-platform'),
// 获取经过主进程筛选后的环境信息。
getEnv: () => ipcRenderer.invoke('app:get-env'),
}
// 只暴露白名单 API。
contextBridge.exposeInMainWorld('electronAPI', electronAPI)
主进程注册对应处理器:
// electron/main.ts
ipcMain.handle('app:get-version', () => app.getVersion())
ipcMain.handle('app:get-platform', () => process.platform)
ipcMain.handle('app:get-env', () => ({
// 只返回渲染进程确实需要的信息。
nodeEnv: process.env.NODE_ENV,
apiBaseUrl: process.env.VITE_API_BASE_URL,
wsUrl: process.env.VITE_WS_URL,
}))
窗口安全配置建议保持收紧:
webPreferences: {
// preload 是唯一桥接入口。
preload: path.join(__dirname, 'preload.mjs'),
// 禁止渲染进程直接访问 Node.js。
nodeIntegration: false,
// 开启上下文隔离,页面只能访问 preload 显式暴露的 API。
contextIsolation: true,
// 保持浏览器安全策略。
webSecurity: true,
}
本章结论:Electron 的稳定性来自主进程、preload、renderer 的明确分工;边界越清晰,后期越不容易失控。
6. 多窗口系统设计(核心亮点必须强化)
如果说 file:// 和 API 切换是桌面化的底层坑,那么多窗口系统就是 Electron 真正区别于 Web 的核心能力。
浏览器里要做多屏展示,往往依赖用户手动打开多个窗口;Electron 可以在应用启动时自动创建多个 BrowserWindow,每个窗口加载不同路由,并分配到不同显示器。
典型多窗口展示系统包括:
- 控制屏
- 数据屏
- 可视化屏
- 信息屏
多窗口不是多开页面,而是桌面端窗口编排
screen API:识别显示器
import { screen } from 'electron'
function getOrderedDisplays() {
// Electron 返回 Display[],每个 Display 都包含 bounds。
// bounds.x / bounds.y 可以用于推断物理布局顺序。
return screen.getAllDisplays()
.sort((a, b) => a.bounds.x - b.bounds.x || a.bounds.y - b.bounds.y)
}
默认按坐标排序可以满足大多数场景,但现场显示器顺序经常会因为系统设置不同而变化。因此需要提供覆盖配置:
# 逗号分隔显示器索引,用于覆盖默认显示器顺序。
ELECTRON_DISPLAY_ORDER=1,0,2,3
function getOrderedDisplays() {
const displays = screen.getAllDisplays()
.sort((a, b) => a.bounds.x - b.bounds.x || a.bounds.y - b.bounds.y)
// 从环境变量读取显示器顺序覆盖配置。
const order = process.env.ELECTRON_DISPLAY_ORDER
?.split(',')
.map(value => Number(value.trim()))
.filter(value => Number.isInteger(value) && value >= 0 && value < displays.length)
if (!order?.length) return displays
// 先使用指定显示器,再追加未指定显示器,避免漏掉窗口。
const selected = order.map(index => displays[index])
const rest = displays.filter((_, index) => !order.includes(index))
return [...selected, ...rest]
}
路由表:把窗口和页面解耦
窗口不应该写死在创建逻辑里。更好的方式是维护一个路由表:
const screenRoutes = [
// 控制屏加载控制路由。
{ id: 'control', route: '/control', title: '控制屏', fallbackWidth: 1920, fallbackHeight: 1080 },
// 数据屏加载数据路由。
{ id: 'data', route: '/data', title: '数据屏', fallbackWidth: 1920, fallbackHeight: 1080 },
// 可视化屏加载可视化路由。
{ id: 'visual', route: '/visual', title: '可视化屏', fallbackWidth: 1920, fallbackHeight: 1080 },
// 信息屏加载信息路由。
{ id: 'info', route: '/info', title: '信息屏', fallbackWidth: 1920, fallbackHeight: 1080 },
]
现场如果需要调整窗口数量或页面顺序,可以通过环境变量覆盖:
# 主进程按顺序创建窗口。
ELECTRON_SCREEN_ROUTES=/control,/data,/visual,/info
fallback 机制:显示器不足时也能调试
多窗口系统最容易忽视的是 fallback。开发机可能只有一个显示器,现场也可能临时少接一块屏。如果窗口创建逻辑假设显示器一定足够,应用就会变得很脆。
function getWindowBounds(route, index, displays) {
// 优先把第 index 个窗口放到第 index 个显示器。
const display = displays[index]
if (display) {
return {
bounds: display.bounds,
hasDisplay: true,
}
}
// 显示器不足时,回退到主显示器工作区。
const primaryBounds = screen.getPrimaryDisplay().workArea
// 多余窗口错位显示,避免完全重叠,便于开发调试。
const offset = 42 * (index - displays.length + 1)
return {
bounds: {
x: primaryBounds.x + offset,
y: primaryBounds.y + offset,
width: Math.min(route.fallbackWidth, Math.max(1024, primaryBounds.width - offset)),
height: Math.min(route.fallbackHeight, Math.max(720, primaryBounds.height - offset)),
},
hasDisplay: false,
}
}
自动全屏:让桌面端更像应用,而不是浏览器
const win = new BrowserWindow({
// 使用目标显示器的坐标和尺寸。
x: bounds.x,
y: bounds.y,
width: bounds.width,
height: bounds.height,
// 无边框窗口更适合多屏展示。
frame: false,
fullscreenable: true,
autoHideMenuBar: true,
// 页面准备好后再显示,减少白屏闪烁。
show: false,
webPreferences: {
preload: path.join(__dirname, 'preload.mjs'),
nodeIntegration: false,
contextIsolation: true,
},
})
win.once('ready-to-show', () => {
win.show()
// 有真实显示器时默认全屏;调试时可通过环境变量关闭。
if (hasDisplay && process.env.ELECTRON_AUTO_FULLSCREEN !== 'false') {
win.setFullScreen(true)
}
})
调试时关闭自动全屏:
# 关闭启动自动全屏,方便在开发机查看多个窗口。
ELECTRON_AUTO_FULLSCREEN=false npm run electron:dev
调试快捷键:多窗口系统必须给自己留出口
全屏多窗口一旦进入现场环境,调试出口非常重要。
// 退出所有窗口全屏。
globalShortcut.register('CommandOrControl+Shift+F', exitAllFullscreen)
// 切换所有窗口全屏状态。
globalShortcut.register('CommandOrControl+Alt+F', toggleAllFullscreen)
// 退出应用。
globalShortcut.register('CommandOrControl+Shift+Q', () => app.quit())
单窗口也建议处理基础按键:
win.webContents.on('before-input-event', (event, input) => {
if (input.type !== 'keyDown') return
if (input.key === 'F11') {
// F11 切换当前窗口全屏。
event.preventDefault()
win.setFullScreen(!win.isFullScreen())
}
if (input.key === 'Escape' && win.isFullScreen()) {
// Esc 退出当前窗口全屏。
event.preventDefault()
win.setFullScreen(false)
}
if ((input.control || input.meta) && input.key.toLowerCase() === 'q') {
// Ctrl/Cmd + Q 退出应用。
event.preventDefault()
app.quit()
}
})
多窗口系统的价值,不是一次打开四个页面,而是把显示器、路由、窗口状态和调试能力统一纳入主进程管理。
本章结论:多窗口展示系统是 Electron 桌面端的核心竞争力,关键不只是创建窗口,而是显示器识别、fallback、全屏控制和调试出口的完整设计。
7. 打包体系详解(portable / NSIS / zip)
Electron 集成跑通后,真正的交付问题才开始:到底应该给用户什么产物?
Windows 常见产物有三类:
electron-builder 基础配置
实际 package.json 不支持注释。下面使用 jsonc 解释配置含义:
{
// 主进程构建后的入口。
"main": "dist-electron/main.js",
"build": {
// 应用唯一 ID,示例使用占位命名。
"appId": "com.example.desktop",
// 安装包和可执行文件展示名称。
"productName": "DemoDesktopApp",
"directories": {
// 统一输出目录。
"output": "release"
},
"files": [
// Vue3 页面构建产物。
"dist/**/*",
// Electron 主进程和 preload 构建产物。
"dist-electron/**/*",
// 版本号、入口等元信息。
"package.json"
],
// 减少散文件数量。
"asar": true,
"nsis": {
// 关闭差分 blockmap,减少本地和容器混合构建时的权限问题。
"differentialPackage": false
},
"win": {
"target": [
{
// 默认正式安装包。
"target": "nsis",
"arch": ["x64"]
}
]
},
"linux": {
// Linux 常用产物。
"target": ["AppImage", "deb"],
"category": "Utility"
}
}
}
策略选择逻辑
不要把打包目标理解为“越多越好”。更合理的选择方式是:
| 产物 | 适用阶段 | 优点 | 风险 |
|---|---|---|---|
| portable exe | 内测、演示、快速验证 | 拷贝即用,构建链路短 | 不适合复杂安装流程 |
| NSIS installer | 正式交付 | 安装体验完整,支持卸载和快捷方式 | 在 Wine/QEMU 下更容易失败 |
| zip 绿色版 | 测试、兼容性兜底 | 解压即用,跨平台构建成功率较高 | 用户需要手动解压 |
对应命令:
# 构建渲染进程、主进程和 preload。
npx vite build --mode electron
# 输出 Windows portable exe。
npx electron-builder --win portable --x64
# 输出 Windows NSIS 安装包。
npm run electron:build:win
# 输出 Windows zip 绿色版。
npm run electron:build:win:zip
建议保留三种策略:
- 开发验证优先 portable。
- 正式交付优先 NSIS。
- 跨平台构建失败时用 zip 兜底。
本章结论:打包不是把配置填满,而是为不同交付场景选择最稳的产物策略。
8. 跨平台构建:Docker + Wine 的现实问题
如果开发机不是 Windows,却要构建 Windows 包,通常会想到 Docker + Wine。这个方案可行,但要对它的限制有现实预期。
推荐把 Docker + Wine 当作补充方案
Docker + Wine 适合:
- 在非 Windows 机器上构建 portable 或 zip。
- 在 CI 中统一构建环境。
- 规避开发机本地 Electron 下载和依赖差异。
但它不一定适合:
- 稳定产出 NSIS 安装包。
- 在 Apple Silicon 上强行跑完整 Windows 安装器后处理。
- 处理所有 Wine/QEMU 兼容问题。
Dockerfile 示例
为了避免暴露任何真实镜像命名,下面用占位基础镜像表示“带 Wine 的 Electron 构建镜像”。实际使用时替换为团队可访问的构建镜像或公开 builder 镜像。
# 选择一个包含 Node.js、Wine、Electron Builder 依赖的构建镜像。
FROM <electron-builder-wine-image>
# 容器内工作目录。
WORKDIR /app
# 可选:配置 Electron 和 electron-builder 下载镜像。
ENV ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ \
ELECTRON_BUILDER_BINARIES_MIRROR=https://npmmirror.com/mirrors/electron-builder-binaries/ \
npm_config_registry=https://registry.npmmirror.com
# 先复制依赖描述文件,利用 Docker layer 缓存。
COPY package*.json ./
RUN npm ci --cache /tmp/npm-cache
# 再复制完整源码。
COPY . .
# 默认构建 zip 绿色版,跨平台成功率通常高于 NSIS。
CMD ["npm", "run", "electron:build:win:zip"]
构建镜像:
# 固定 linux/amd64,避免在 Apple Silicon 上默认生成 arm64 构建环境。
docker build --platform linux/amd64 -f Dockerfile.electron -t example-electron-builder .
运行构建:
# --rm:构建结束后删除临时容器。
# --platform linux/amd64:与 Windows x64 构建目标保持一致。
# -v "$PWD/release:/app/release":把构建产物写回宿主机 release 目录。
docker run --rm \
--platform linux/amd64 \
-v "$PWD/release:/app/release" \
example-electron-builder
Apple Silicon 上的典型问题
现象:
wine process failed
qemu: uncaught target signal
原因:
- Apple Silicon 是 arm64 主机。
- Windows x64 包需要 amd64 构建环境。
linux/amd64容器在 arm64 主机上通常依赖 QEMU 模拟。- NSIS 安装器后处理会调用 Wine,组合链路更长,更容易失败。
解决策略:
- 优先在 Windows 或 amd64 Linux CI 上构建正式 NSIS。
- Apple Silicon + Docker/Wine 优先输出 portable 或 zip。
- 如果必须跨平台构建,先固定
--platform linux/amd64。 - 不要把 Wine/QEMU 失败误判为 Vue 或 Electron 主进程代码问题。
本章结论:Docker + Wine 是跨平台构建的工具,不是银弹;正式安装包最好交给 Windows 或 amd64 CI,portable 和 zip 更适合作为跨平台兜底。
9. 工程优化与踩坑总结
这一类项目真正消耗时间的,往往不是“第一次跑起来”,而是各种环境差异带来的边角问题。
坑一:Electron 下载超时
现象:
Timeout awaiting request
原因:Electron 运行时和 electron-builder 辅助二进制需要下载,网络不稳定时容易失败。
解决:
# Electron 运行时下载镜像。
export ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/
# electron-builder 辅助二进制下载镜像。
export ELECTRON_BUILDER_BINARIES_MIRROR=https://npmmirror.com/mirrors/electron-builder-binaries/
# 带镜像配置执行构建。
npm run electron:build:win
结论:Electron 构建依赖下载链路,CI 和本地都应该显式管理镜像与缓存。
坑二:blockmap 权限问题
现象:本地构建和 Docker 构建交替执行后,release 目录里历史文件无法覆盖。
原因:容器写入的产物可能带来宿主机权限差异,NSIS 差分包还可能生成 .blockmap。
解决:
{
"build": {
"nsis": {
// 如果暂时不需要差分更新,可以关闭差分包。
"differentialPackage": false
}
}
}
结论:release 目录要保持可清理、可重建,不要把历史中间产物当成稳定依赖。
坑三:Web 镜像构建时下载 Electron
现象:只是构建 Web Docker 镜像,却开始下载 Electron,导致构建慢或失败。
原因:package.json 中存在 Electron 依赖,npm ci 触发下载。
解决:
# Web 镜像只需要构建 dist,不需要 Electron 运行时。
ENV ELECTRON_SKIP_BINARY_DOWNLOAD=1
结论:Web 和 Desktop 共用仓库时,构建阶段也要区分目标环境。
坑四:图标配置过早启用
现象:配置了 public/icon.ico,但文件不存在,electron-builder 直接失败。
解决:图标文件准备好之前先不要启用 win.icon 和 linux.icon。示例阶段应优先保证打包链路稳定。
结论:打包配置不是越完整越好,未准备好的资源配置会变成构建风险。
坑五:外链在 Electron 内打开
现象:页面中的外部链接在 Electron 内新建窗口,带来不可控页面。
解决:
win.webContents.setWindowOpenHandler(({ url }) => {
// 外链交给系统浏览器。
shell.openExternal(url)
// 阻止应用内打开新窗口。
return { action: 'deny' }
})
结论:Electron 应用应该只承载自己的页面,外部链接交给系统浏览器更稳。
坑六:把桌面端调试出口忘了
现象:应用启动后自动全屏,现场无法退出、无法打开调试窗口。
解决:保留环境变量和快捷键:
# 调试时关闭自动全屏。
ELECTRON_AUTO_FULLSCREEN=false npm run electron:dev
// 退出所有窗口全屏。
globalShortcut.register('CommandOrControl+Shift+F', exitAllFullscreen)
// 退出应用。
globalShortcut.register('CommandOrControl+Shift+Q', () => app.quit())
结论:越是接近桌面端交付,越要提前设计调试出口。
本章结论:工程优化的本质,是把一次性经验沉淀成配置、脚本、fallback 和调试机制。
10. 可复用架构总结
回看整个 Vue3 + Electron 桌面化过程,真正可复用的不是某一段配置,而是一套分层思路:
可以把这套架构总结成五句话:
file://是 Electron 生产环境的第一性问题,先解决路径和路由。/api代理是 Web 容器能力,不是前端天然能力,Desktop 必须有运行时切换。- preload 是安全边界,不是随手暴露 Node.js 的通道。
- 多窗口系统的核心是显示器识别、窗口路由、fallback、全屏和调试出口。
- 打包策略要按交付场景选择,portable、NSIS、zip 各有位置。
最终可落地的工程路径是:
Vue3 + Vite 保持页面主体
↓
Vite base: './' + hash 路由解决 file:// 兼容
↓
runtimeConfig 收敛 Web / Desktop 环境差异
↓
Electron main / preload / renderer 明确分层
↓
screen API 实现多窗口展示系统
↓
electron-builder 输出 Windows / Linux 产物
↓
Docker + Wine 作为跨平台构建补充
一个可维护的 Electron 项目,不是“能打包”的 Vue 项目,而是把运行时、窗口系统和交付链路都显式设计过的前端桌面端应用。
本章结论:Vue3 + Electron 的最佳实践不是追求一次跑通,而是把 Web 和 Desktop 的差异抽象出来,让同一套前端能力可以稳定运行在两种容器里。

2428

被折叠的 条评论
为什么被折叠?



