Vue 109 ,从零搭建 Vue3 + Electron 工程:生态、安全桥接与打包选型( Vue + Electron )

前言

Vue 106 讲了 Electron 的进程模型和安全基线,Vue 107 用若依桌面端走了一遍具体项目,Vue 108 按 Star 筛出了值得读的开源应用。这三篇解决的是"Electron 是什么、别人怎么做、该读哪个"。但读别人的项目和从空目录搭自己的工程之间,还隔着一层生态认知 :electron-vite 在工具链里到底管什么、Forge 和 electron-builder 的分歧在哪、preload 的白名单怎么写才不会把安全边界拆掉、原生模块打包后为什么报 Cannot find module。

这篇按 electron-vite + Vue 3 从零走一遍完整搭建。每一步都标注了实际会卡住的地方,命令和代码可以直接复制执行。读完你能拿到一个 contextIsolation + sandbox + preload 白名单 都已配好的 Vue 3 + Electron 工程,并且知道打包时该选 Forge 还是 builder、原生模块的复制链路为什么不能只靠 asar.unpack。

Vue 2 已于 2023 年 12 月 31 日终止支持(EOL),不再接收新功能、安全更新和浏览器兼容修复,所以本篇以 Vue 3 为主线,Vue 2 只在必要处做边界对照,不再单独展开。

图1 封面图:从零搭建 Vue3 + Electron

一、Electron 生态的版本约束与工具链分工

1.1 版本现状:42 已 EOL,当前支持线是 43 / 44 / 45

截至 2026 年 11 月,Electron 的支持策略没有变:只维护最新三个稳定大版本 。42 的 EOL 日期是 2026 年 10 月 20 日 ,和 45 的稳定版发布是同一天。从那天起,支持集合变成 43、44、45,42 不再接收任何补丁------没有安全修复,没有 bug fix,也没有新的 42.x 发布。

具体的时间线:

版本 稳定版发布 EOL Chromium Node.js
43 2026-06-29 2027-01-05 M150 24.17.0
44 2026-08-24 2027-03-02 M152 24.18.1
45 2026-10-20 2027-04-27(tentative) M156 24.22.0

数据来源为 Electron 官方发布时间表。42 已经不在支持窗口内,新项目应该直接从 43.x 起。43 是目前支持线里最老的版本,只接收安全修复;44 和 45 接收的修复范围更宽。 选版本时先看 releases.electronjs.org/schedule,而不是只看 npm 上的 latest。

1.2 工具链的三层分工:vite、Forge、builder 不是替代关系

这三个名字经常被混在一起,职责完全不同。

工具 职责 不负责什么 官网
electron-vite 开发阶段的三层构建:main / preload / renderer 各自独立的 Vite 配置,渲染进程 HMR 不打包安装包,不处理代码签名 cn.electron-vite.org
Electron Forge 官方第一方打包分发:@electron/packager + makers + publishers,ASAR 完整性校验 、Fuses 安全开关 不替代开发服务器 electronforge.io
electron-builder 社区打包工具:内部重写构建逻辑,配置选项更丰富 ,内置自动更新服务器支持 不提供开发阶段的 HMR electron.build

Forge 的官方定位是"把已有的第一方工具组合成一条流水线 ",builder 是"为大多数构建任务重写自己的内部逻辑 "。这不是谁替代谁,是开发体验和分发链路的分工。

1.3 安全基线:2026 年写 Electron,nodeIntegration: true 是反向操作

三条默认值必须记住:

  • contextIsolation 从 Electron 12 开始默认 true,从 20 开始强制不能禁用。
  • nodeIntegration 从 Electron 5 开始默认 false。
  • sandbox 从 Electron 20 开始默认 true ,preload 只能使用 Node.js 的 polyfill 子集,不能直接 require('fs') 或 require('child_process')。

在 43.x 上照搬 2022 年教程里的 nodeIntegration: true,等于把一个有完整 Node 权限的浏览器标签页暴露给渲染层。 官方安全清单在 electronjs.org/docs/latest/tutorial/security,逐项对照比记零散结论可靠。

图2 工具链分工示意图

二、从零创建 Vue 3 工程:electron-vite 脚手架与目录约定

2.1 脚手架命令与模板选择

通过 create-electron 脚手架创建项目:

bash 复制代码
npm create @quick-start/electron@latest my-app -- --template vue-ts

vue-ts 模板已经包含 contextIsolation、sandbox 的安全默认值 ,electron.vite.config.ts 和 preload 的 contextBridge 骨架都已接好。不要选 vanilla-ts 再手动加 Vue,三层配置和 TypeScript 设置都要自己补。

npm 7+ 需要额外的 -- 把 --template 传给脚手架,漏掉双横线会进入交互式选择,而不是直接按模板生成。 用 pnpm 或 yarn 时把 npm create 换成 pnpm create / yarn create 即可。

2.2 生成后的目录结构

复制代码
my-app/
├── src/
│   ├── main/          ← 主进程,创建窗口、生命周期
│   ├── preload/       ← 预加载脚本,contextBridge 暴露面
│   └── renderer/      ← 渲染进程,Vue 3 页面
├── electron.vite.config.ts
├── package.json
└── forge.config.ts    ← 如果脚手架集成了 Forge

main / preload / renderer 三层分离不是风格偏好,electron-vite 的自动入口发现依赖这个结构。 默认入口是 src/main/index.ts、src/preload/index.ts、src/renderer/index.html。三层揉进同一个 src 目录,后续 Vite 配置和打包路径都会对不上。

2.3 electron.vite.config.ts 的三段配置(以 electron-vite v5 为准)

**如果你用的是 electron-vite v5,externalizeDepsPlugin 已经被废弃,依赖外部化改为 build.externalizeDeps 配置项,并且默认启用。**脚手架的 vue-ts 模板如果生成的是 v5 项目,配置文件里不会再出现 externalizeDepsPlugin 的导入。三段配置的结构变成:

ts 复制代码
import { defineConfig } from 'electron-vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'

export default defineConfig({
  main: {
    build: {
      rollupOptions: {
        input: resolve(__dirname, 'src/main/index.ts')
      }
    }
  },
  preload: {
    build: {
      rollupOptions: {
        input: resolve(__dirname, 'src/preload/index.ts')
      }
    }
  },
  renderer: {
    plugins: [vue()],
    root: resolve(__dirname, 'src/renderer'),
    build: {
      rollupOptions: {
        input: resolve(__dirname, 'src/renderer/index.html')
      }
    }
  }
})

build.externalizeDeps 默认启用,只作用于 main 和 preload,不要加到 renderer 配置里。 renderer 运行在浏览器上下文,依赖需要被 Vite 正常打包;加了外部化反而会让 Vue 运行时和组件库的解析出问题。外部化的作用是"不打包",它不负责"复制到输出目录" ------这个区分在第五章的 better-sqlite3 打包坑里会再出现。

2.4 安装依赖与启动

bash 复制代码
cd my-app
npm install
npm run dev

国内环境如果 Electron 二进制下载卡住,在 npm install 之前先设镜像:

bash 复制代码
export ELECTRON_MIRROR="https://npmmirror.com/mirrors/electron/"

Windows PowerShell 用 $env:ELECTRON_MIRROR="https://npmmirror.com/mirrors/electron/"。镜像地址必须指向 Electron 二进制目录,不是普通 npm registry。 可用镜像在 npmmirror.com/mirrors/electron 可以查到。

npm run dev 启动 Vite 开发服务器并拉起 Electron 窗口。窗口出现、DevTools 能打开,只说明三层结构跑通了,不等于 preload 和 IPC 已经生效------这个判断标准在第七章还会再强调一次。

图3 目录结构与运行链路示意图

三、preload 安全桥接:白名单而不是全量转发

3.1 为什么不能在 Vue 组件里直接 require('electron')

contextIsolation 开启后,渲染进程和 preload 运行在两个隔离的 JavaScript 世界 里。渲染层没有 Node.js 访问权,require、process、__dirname 在渲染层都不存在。contextBridge.exposeInMainWorld 是渲染进程访问主进程能力的唯一合法通道。

直接把整个 ipcRenderer 挂上去是错误写法:

ts 复制代码
// 错误:不要这样做
contextBridge.exposeInMainWorld('ipcRenderer', ipcRenderer)

这等于把任意通道的发送和调用能力都交给了渲染层,白名单就失去了意义。

3.2 手写白名单:只暴露明确的通道

ts 复制代码
import { contextBridge, ipcRenderer } from 'electron'

const api = {
  readFile: (path: string): Promise<string> =>
    ipcRenderer.invoke('fs:readFile', path),
  saveFile: (path: string, content: string): Promise<void> =>
    ipcRenderer.invoke('fs:saveFile', path, content)
}

contextBridge.exposeInMainWorld('api', api)

渲染层只看到 window.api.readFile 和 window.api.saveFile,通道名和参数形状固定写在 preload 里,渲染层无法拼接任意通道名。

主进程侧必须有对应的 handler,并对参数做校验:

ts 复制代码
ipcMain.handle('fs:readFile', async (_event, path: string) => {
  if (typeof path !== 'string' || !path.startsWith(app.getPath('documents'))) {
    throw new Error('path not allowed')
  }
  return fs.readFile(path, 'utf-8')
})

preload 的白名单是第一道闸,ipcMain.handle 的参数校验是第二道闸。只做其中一道,都不算完整的边界。 IPC 的完整模式参考 electronjs.org/docs/latest/tutorial/ipc。

electron-vite 生态里还有一个 @electron-toolkit/preload,但它不是白名单的替代品。 它把 ipcRenderer、webFrame、webUtils、process 的常用调用封装成一个 electronAPI 对象,通过 contextBridge.exposeInMainWorld('electron', electronAPI) 暴露给渲染层。用法很直接:

ts 复制代码
import { contextBridge } from 'electron'
import { electronAPI } from '@electron-toolkit/preload'

if (process.contextIsolated) {
  contextBridge.exposeInMainWorld('electron', electronAPI)
}

封装减少的是样板代码,它不会替你定义业务通道,也不会替你在 ipcMain.handle 里做参数校验 。如果你需要暴露 fs:readFile 这种明确的业务能力,仍然要在 preload 里手写白名单,再把 electronAPI 作为通用能力单独暴露。

3.3 Vue 3 组件侧的调用与类型声明

Vue 3 的 <script setup> 里不能假设 window.api 在 onMounted 时一定已经注入。在 src/preload/index.d.ts 声明类型,调用处做一次 availability 检查:

ts 复制代码
// src/preload/index.d.ts
declare global {
  interface Window {
    api: {
      readFile: (path: string) => Promise<string>
      saveFile: (path: string, content: string) => Promise<void>
    }
  }
}
vue 复制代码
<script setup lang="ts">
import { onMounted } from 'vue'

onMounted(() => {
  if (!window.api?.readFile) {
    console.warn('preload API not ready')
    return
  }
  window.api.readFile('/path/to/file').then(console.log)
})
</script>

类型声明文件放在 preload 目录下,而不是 renderer 目录下,这样 TypeScript 的 include 范围才能同时覆盖两层。

四、打包选型:Forge 和 electron-builder 怎么选

4.1 两者的哲学差异

Forge 是官方第一方工具 ,优势在 ASAR 完整性校验 、Electron Fuses (把安全开关烧进二进制)、和 Electron 安全模型更紧的整合。builder 的优势在更高的社区采用率 、更丰富的开箱配置 、内置自动更新服务器支持 、Snap/Flatpak 免额外配置。

维度 Electron Forge electron-builder
维护方 Electron 团队(第一方) 社区
ASAR 完整性校验 原生支持 需要额外配置
Electron Fuses 原生支持 不易集成
自动更新服务器 通过 publisher 扩展 内置
多平台 maker 配置 插件式,较分散 集中式,选项多
新项目官方推荐 是 务实选项

4.2 新项目的推荐路径

新项目优先 Forge。 官方推荐的原因不是"builder 不好",而是 Forge 把 ASAR 完整性校验和 Fuses 做进了默认链路 ,这两项在 43.x 的安全基线下不是可选项。如果团队明确需要 builder 独有的能力------更成熟的多平台配置、内置自动更新服务器、Snap/Flatpak------builder 仍然是务实选项。

两者不是互斥的。builder 提供了四个 Forge maker 包,可以在 Forge 流水线里产出 builder 目标;也可以用 Forge 完成打包和 Fuses,再把已打包目录交给 builder 生成安装包和 latest*.yml 自动更新文件。Forge 的配置文档在 electronforge.io/config,builder 的在 electron.build/configuration。

4.3 脚手架生成的项目里 Forge 在哪

electron-vite 的 vue-ts 模板在创建时会询问是否集成自动更新,选择后生成的项目通常已经包含 forge.config.ts 或对应的 Forge 配置。先打开 package.json 看 config.forge 字段或 forge.config.ts 是否存在,再决定是补 Forge 配置还是切到 builder。 两套打包配置同时存在会让 npm run make 和 npm run build 的行为不一致。

图4 Forge vs builder 决策图

五、原生模块打包:Cannot find module 的根因和解法

better-sqlite3、sharp、serialport 这类带 .node 二进制的模块,开发环境能跑、打包后启动即崩:

复制代码
Error: Cannot find module 'better-sqlite3'
Require stack:
- /path/to/app.asar/.vite/build/main.js

根因不是 Vite 配置错了,是 Vite 的"外部化"和 Forge 的打包是两个独立过程。 外部化只告诉 Vite"不要打包这个模块,运行时用 require()";但 Forge 默认不把 node_modules 复制到输出目录 ,运行时 require('better-sqlite3') 自然找不到。仅配 asar.unpack 也不够------它只解包"已经在 asar 里的文件",模块没有被复制进去,就没有东西可解。

解法是在 forge.config.ts 的 packageAfterCopy 钩子里显式复制原生模块及其依赖。 packageAfterCopy 在 Electron Packager 的 afterCopy 阶段调用,时机是"应用文件已经复制到临时目录之后":

ts 复制代码
import path from 'path'
import { cp, mkdir } from 'fs/promises'
import type { ForgeConfig } from '@electron-forge/shared-types'

const nativeModules = ['better-sqlite3', 'bindings', 'file-uri-to-path']

const config: ForgeConfig = {
  packagerConfig: {
    asar: { unpack: '*.{node,dll}' }
  },
  rebuildConfig: { force: true },
  hooks: {
    packageAfterCopy: async (_forgeConfig, buildPath) => {
      const src = path.resolve(__dirname, 'node_modules')
      const dest = path.resolve(buildPath, 'node_modules')
      await mkdir(dest, { recursive: true })
      await Promise.all(
        nativeModules.map(async (name) => {
          await cp(path.join(src, name), path.join(dest, name), {
            recursive: true
          })
        })
      )
    }
  }
}

nativeModules 数组要包含原生模块本身和它的传递依赖 (better-sqlite3 依赖 bindings,bindings 依赖 file-uri-to-path)。漏掉依赖,require 链会在中间断开,报错变成另一个模块名。

**这个复制逻辑假设 node_modules 是扁平结构。Forge 的模块解析算法是"朴素"的,不处理 symlink,也不支持 Yarn PnP。**如果你用 pnpm,默认的 symlink 结构会让 Forge 在收集依赖时看不到真实路径。官方给出的处理方式是在 .npmrc 里设置:

复制代码
node-linker=hoisted

Yarn 2+ 则需要把 nodeLinker 设为 node-modules。不是复制逻辑本身有问题,是 Forge 根本不会跟随 symlink 去解析依赖树。 不设这个配置,packageAfterCopy 复制进去的模块也可能因为依赖链断裂而在运行时失败。

如果团队用 electron-builder,对应的是 files 和 asarUnpack,原理相同:确保原生模块进入输出目录,并且 .node 文件不在 asar 内部被压缩 。文档在 electron.build/configuration/configuration#Configuration-asarUnpack。

图5 原生模块打包链路图

六、Vue 2 的边界对照(仅限 legacy 项目)

Vue 2 已于 2023 年 12 月 31 日 EOL,不再接收安全更新和浏览器兼容修复。 如果项目还在 Vue 2 上,Electron 的安全基线和 preload 白名单机制完全没有不同,变的只是渲染层的构建链路。

electron-vite 的 vue-ts 模板面向 Vue 3。Vue 2 接入需要手动换渲染层插件,官方的 @vitejs/plugin-vue2 只支持 Vue ^2.7.0 且处于非活跃维护状态。Vue 2 的差异在渲染层构建工具链,不在 Electron 的安全模型,也不存在"Vue 2 可以开 nodeIntegration 偷懒"这种说法。 新项目没有理由从 Vue 2 起步,Vue 3 的 官方文档 和 electron-vite 模板已经覆盖了从零到打包的完整路径。

七、注意事项:从零搭建时真正会卡住的地方

preload 路径不要用相对路径。 打包后 __dirname 的位置和开发阶段不同。如果主进程产物在 dist/main/,preload 在 dist/preload/,正确写法是 path.join(__dirname, '../preload/preload.cjs'),而不是 './preload.js' 或相对于项目根目录的路径。相对路径开发阶段可能"碰巧"能跑,打包后静默失效,窗口起来了但 window.api 是 undefined。

electron-vite v5 不要再用 externalizeDepsPlugin。 v5 已经废弃该插件,改为默认启用的 build.externalizeDeps。如果照着旧教程在 main 和 preload 里手动导入插件,配置会冲突或不起作用。先确认项目的 electron-vite 大版本,再决定用哪种写法。

build.externalizeDeps 的默认启用范围只覆盖 main 和 preload,不要手动扩展到 renderer。 renderer 是浏览器上下文,依赖需要被 Vite 正常打包。加到 renderer 上会让 Vue 运行时和组件库的解析出问题,而且不会带来任何原生模块方面的好处。

国内环境先设镜像再 npm install,顺序不能反。 ELECTRON_MIRROR 必须在 Electron 的 postinstall 下载二进制之前生效。装到一半卡住再补设,缓存可能不完整,删掉 node_modules 重来比反复重试更快。

Electron 内置的 Node 版本和你本地系统 Node 的版本不是一回事。 Electron 43 内置 Node 24.17.0,但 better-sqlite3、node-pty 这类原生模块需要针对 Electron 的 ABI 重新编译(electron-rebuild 或 Forge 的 rebuildConfig),而不是只匹配系统 Node。只匹配系统版本,开发环境可能能跑,打包后原生模块加载失败。

主进程和 preload 的改动不会热重载。 npm run dev 的 HMR 只覆盖渲染进程。改 src/main/ 或 src/preload/ 后需要手动重启开发进程。改了 preload 但没重启,window.api 还是旧的,容易误判为"代码没生效"。状态丢失后出现诡异错误时,优先全量重启,而不是排查业务代码。

pnpm 用户不设 node-linker=hoisted,Forge 打包时可能根本看不到依赖。 这不是 packageAfterCopy 写错了,是 Forge 的模块解析算法不跟随 symlink。在 .npmrc 里加上 node-linker=hoisted 再打包,比在复制钩子里绕开 symlink 更稳。

"窗口能弹出来"不是成功标准。 判断标准应该是:DevTools 里 window.api 有值、能通过 ipcRenderer.invoke 拿到主进程返回值、原生模块(如果有)能正常加载。

八、本文总结

Electron 当前生态的版本约束是硬边界 :42 已于 2026 年 10 月 20 日 EOL,43、44、45 是当前支持线,新项目从 43.x 起,安全修复只跟着受支持版本的 patch 走。

工具链的分工比"谁替代谁"更值得记住:electron-vite 负责开发体验和三层构建,Forge 负责官方打包与安全集成,builder 负责配置自由度和自动更新生态 。从零搭 Vue 3 + Electron 工程时,结构按 electron-vite 的三层约定走,安全按 contextIsolation + sandbox + preload 白名单走,打包按项目对"官方全链路"还是"配置自由度"的偏好选 Forge 或 builder。

原生模块的坑要单独记住:Vite 的"外部化"和 Forge 的"不复制 node_modules"是两件事 ,打包后 Cannot find module 的解法是显式复制模块及其依赖,并且 pnpm 用户要先解决 Forge 不跟随 symlink 的前提问题。

Vue 2 已经 EOL,不再作为新项目的起点。 它的差异只在渲染层构建链路,不在 Electron 的安全模型。

九、更多操作

本文属于 Electron + Vue 桌面应用 专栏的从零搭建篇。专栏持续更新 Vue3 与 Electron 的整合实战,涵盖环境搭建、主进程与渲染进程通信、预加载脚本的安全设计、electron-vite / electron-builder 打包发布、自动更新和性能调优。

👉 Electron + Vue 桌面应用专栏

本专栏聚焦 Electron + Vue 桌面应用实战,系统讲解 Vue3 与 Electron 的整合实践,涵盖环境搭建、主进程与渲染进程通信、预加载脚本、窗口菜单、打包发布、自动更新及安全实践,帮助前端开发者从零构建跨平台桌面应用。

以及

👉 Vue Develop 实战专栏

想继续深挖的读者,可以从这几个方向入手:

  • electron-vite 官方文档 :cn.electron-vite.org 有完整的中文配置说明,v5 的 build.externalizeDeps 和 preload 沙盒模式的细节比二手教程准确。
  • Electron Forge 文档 :electronforge.io 的 Packaging 和 Hooks 章节,特别是 packageAfterCopy 的调用时机和包管理器兼容性说明。
  • Electron 官方安全清单 :electronjs.org/docs/latest/tutorial/security,逐项对照 contextIsolation / sandbox / CSP / will-navigate / setWindowOpenHandler。
  • Electron 版本支持窗口 :releases.electronjs.org/schedule,选版本之前先看这里,不要只看 npm 上的 latest。
  • 下一篇方向 :本篇走通了"从零到能跑、能打包",下一篇可以专门写 Forge 从打包到代码签名安装包的完整流程 ,或者 electron-builder 多平台配置与自动更新服务器搭建。
  • 本地练习建议 :先用脚手架生成项目,手动加一个 fs:readFile 的 IPC 通道,再把 better-sqlite3 接进去打一次包。这三个动作覆盖了 preload 白名单、主进程参数校验和原生模块复制。

如果本文对你有帮助,欢迎点赞、收藏、评论。你在 npm install 或打包阶段卡在哪一步,或者对 Forge 和 builder 的选型有不同实践,欢迎留言交流具体场景。

相关推荐
JarvanMo22 分钟前
Flutter 3.50 & Dart 3.14发布,一起看看有哪些变化!
前端
莪_幻尘36 分钟前
4 个 3D 游戏,总体积 3.82MB:拆解 Claude 的 one-shot 工程链路
前端·ai编程·three.js
Highcharts.js44 分钟前
Highcharts智能数据分析平台Orbit 多语言配置技术文档
java·前端·javascript·highcharts·orbit·智能数据分析·设置文档
ABAP-張旺1 小时前
SAP ABAP 实战:SAP GUI上做 HTML 看板报表
前端·html·sap·abap
时间的情敌2 小时前
Cesium 建筑单体化、3DTiles 分类、楼层显隐、分户高亮(BIM + 倾斜孪生高频)
前端·3d·cesium
IT_陈寒2 小时前
JavaScript项目实战经验分享:从内存泄漏到性能优化的血泪史
前端·人工智能·后端
计算机毕设定制辅导-无忧学长2 小时前
《基于Spring Boot的迷游哈游戏综合服务平台的开发》
java·vue.js·spring boot·mysql·迷游哈游戏综合服务平台
恋猫de小郭3 小时前
Flutter 3.50 & Dart 3.14 发布,快来看看有什么更新吧
android·前端·flutter
非优秀程序员11 小时前
第10章 千团出世|《互联网产品恩仇录》
前端