Vue3 + Electron 实战:从 Web 项目到 Windows .exe 安装包完整指南

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 的价值所在:不推翻已有前端体系,只在运行容器、窗口管理和交付链路上补齐桌面端能力。

桌面化不是把网页套一层壳,而是把“浏览器替你做的事”重新显式设计出来。

整体思路如下:

Vue3 + Vite 页面

运行时抽象层

Web: Nginx + Browser

Desktop: Electron + file://

Windows exe / installer / zip

多窗口展示系统

本章结论:如果需求只是在浏览器里访问页面,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 主进程可以通过 loadFilehash 参数打开指定页面:

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 分裂问题:

否,Electron file://

前端请求 /api

Web 模式?

Nginx proxy 转发到 API 服务

没有代理层,请求地址失效

运行时抽象层切换为完整 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 的职责边界

Vue 页面

runtimeConfig

API Client

WebSocket Client

Web: /api

Nginx proxy

Desktop: 完整 API / WS 地址

API 服务

环境变量策略

# 数据模式: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 工程质量很大程度取决于三进程边界是否清晰。

preload 桥接层

main 主进程

renderer 渲染进程

Vue3 App

runtimeConfig

页面与组件

BrowserWindow 管理

screen 多显示器

globalShortcut

ipcMain

contextBridge

window.electronAPI

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,每个窗口加载不同路由,并分配到不同显示器。

典型多窗口展示系统包括:

  • 控制屏
  • 数据屏
  • 可视化屏
  • 信息屏

多窗口不是多开页面,而是桌面端窗口编排

app.whenReady

screen.getAllDisplays

按显示器坐标排序

读取窗口路由表

控制屏窗口

数据屏窗口

可视化屏窗口

信息屏窗口

显示器 1

显示器 2

显示器 3 / fallback

显示器 4 / fallback

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 常见产物有三类:

vite build --mode electron

electron-builder

portable exe
免安装,适合内测和演示

NSIS installer
正式安装包,支持快捷方式和卸载

zip 绿色版
解压即用,适合规避安装器兼容问题

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 主进程代码问题。
release electron-builder Wine linux/amd64 容器 开发机 release electron-builder Wine linux/amd64 容器 开发机 docker build docker run 挂载 release npm run electron:build:win:zip 处理 Windows 产物 写入 exe / zip / installer 宿主机获取产物

本章结论: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.iconlinux.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 桌面化过程,真正可复用的不是某一段配置,而是一套分层思路:

Vue3 + Vite 页面

runtime layer

Web 运行时

Desktop 运行时

Nginx 静态部署

/api 代理

Electron main

Electron preload

Vue renderer

多窗口展示系统

electron-builder 打包

portable exe

NSIS installer

zip 绿色版

可以把这套架构总结成五句话:

  1. file:// 是 Electron 生产环境的第一性问题,先解决路径和路由。
  2. /api 代理是 Web 容器能力,不是前端天然能力,Desktop 必须有运行时切换。
  3. preload 是安全边界,不是随手暴露 Node.js 的通道。
  4. 多窗口系统的核心是显示器识别、窗口路由、fallback、全屏和调试出口。
  5. 打包策略要按交付场景选择,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 的差异抽象出来,让同一套前端能力可以稳定运行在两种容器里。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值