electron-vite 完整示例教程

目标:从零开始,一步步构建一个可运行的 Electron + electron-vite + Vue + TypeScript 桌面应用,并打包成 macOS 应用。每一步都有预期结果,当前机器(Node 24 / npm 11 / macOS 15)已实测通过。

工作原理见 README 与 electron-vite,上下文隔离/IPC 见 03-ipc。

先决条件

  • Node.js ≥ 18
  • npm / pnpm / yarn 任一
  • 常见国内环境建议先配置镜像(见 07-environment-variables),避免 postinstall 卡住

Step 0:脚手架

命令

npm create @quick-start/electron@latest electron-vite-demo -- --template vue-ts

交互选项取默认值即可:是否加 Electron updater 插件选 No,是否安装依赖选 Yes,是否初始化 git 视情况。完成后进入项目:

cd electron-vite-demo
npm install    # 若上一步跳过了安装;postinstall 会自动重建原生模块

预期结果

Scaffolding project in .../electron-vite-demo...
Done. Now run: cd electron-vite-demo && npm install && npm run dev

生成结构:

src/
├── main/index.ts          # 主进程入口(Electron + electron-vite)
├── preload/index.ts       # 预加载脚本(contextBridge)
└── renderer/
    ├── index.html          # 渲染进程入口
    └── src/App.vue         # Vue 根组件
electron.vite.config.ts     # 三段式配置:main / preload / renderer
electron-builder.yml        # 打包配置

模板依赖约 electron@39、electron-vite@5、vite@7、vue@3 + TS,版本以脚手架实际生成为准。

Step 1:项目结构理解

electron.vite.config.ts:

import { resolve } from 'path'
import { defineConfig } from 'electron-vite'
import vue from '@vitejs/plugin-vue'
 
export default defineConfig({
  main: {},       // 主进程:Node 环境,产物 out/main/index.js
  preload: {},    // 预加载:受限环境,产物 out/preload/index.js
  renderer: {     // 渲染进程:浏览器环境,标准 Vite + HMR
    resolve: { alias: { '@renderer': resolve('src/renderer/src') } },
    plugins: [vue()]
  }
})

Step 2:运行开发模式

命令

npm run dev

预期结果

控制台依次出现「main 构建成功 → preload 构建成功 → dev server running at localhost:5173 → starting electron app」,并弹出 Electron 窗口。此时:

  • 渲染进程有热更新:改 App.vue 模板保存,窗口不刷新即更新
  • 主进程/preload 改动会自动重启应用

HMR 的关键在主进程模板里:开发模式加载 process.env.ELECTRON_RENDERER_URL,生产模式加载 ../renderer/index.html 文件。

Step 3:加一个真实跨进程功能

目标:页面显示系统信息,数据经 渲染进程 → preload → 主进程 的 IPC invoke 返回。

3.1 主进程(src/main/index.ts)导入 node:os,在 app.whenReady() 里注册:

import { cpus, release, totalmem } from 'node:os'
import { ipcMain } from 'electron'
 
app.whenReady().then(() => {
  ipcMain.handle('system:info', () => ({
    platform: process.platform,
    release: release(),
    cores: cpus().length,
    totalMemory: totalmem()
  }))
})

3.2 preload(src/preload/index.ts)暴露 API:

import { contextBridge, ipcRenderer } from 'electron'
import { electronAPI } from '@electron-toolkit/preload'
 
const api = {
  getSystemInfo: () => ipcRenderer.invoke('system:info')
}
 
if (process.contextIsolated) {
  contextBridge.exposeInMainWorld('electron', electronAPI)
  contextBridge.exposeInMainWorld('api', api)
} else {
  // @ts-ignore (define in dts)
  window.electron = electronAPI
  // @ts-ignore (define in dts)
  window.api = api
}

3.3 渲染进程(src/renderer/src/App.vue):

<script setup lang="ts">
import { onMounted, ref } from 'vue'
 
interface SystemInfo {
  platform: string
  release: string
  cores: number
  totalMemory: number
}
 
const info = ref<SystemInfo | null>(null)
onMounted(async () => {
  info.value = await window.api.getSystemInfo()
})
 
const formatMemory = (bytes: number) => `${(bytes / 1024 ** 3).toFixed(1)} GB`
</script>
 
<template>
  <main class="panel">
    <h1>系统信息</h1>
    <section v-if="info" class="grid">
      <div><span>平台</span><strong>{{ info.platform }}</strong></div>
      <div><span>系统版本</span><strong>{{ info.release }}</strong></div>
      <div><span>CPU 核心</span><strong>{{ info.cores }}</strong></div>
      <div><span>内存</span><strong>{{ formatMemory(info.totalMemory) }}</strong></div>
    </section>
    <p v-else>读取中…</p>
  </main>
</template>

Step 4:TypeScript 类型检查

3.1 中 window.api 是 unknown,无法直接调用 getSystemInfo()。 在 src/preload/index.d.ts 补充类型:

import { ElectronAPI } from '@electron-toolkit/preload'
 
interface SystemInfo {
  platform: string
  release: string
  cores: number
  totalMemory: number
}
 
declare global {
  interface Window {
    electron: ElectronAPI
    api: {
      getSystemInfo: () => Promise<SystemInfo>
    }
  }
}

命令

npm run typecheck    # 分 node 和 web 两条,tsc + vue-tsc

预期结果

无输出退出码 0。若不补 d.ts,Step 3 的 window.api.getSystemInfo 会被 vue-tsc 判错。

Step 5:生产构建

命令

npm run build    # 实际是 typecheck && electron-vite build

预期结果

产物集中在 out/:

out/
├── main/index.js        # 主进程单文件
├── preload/index.js     # 预加载单文件
└── renderer/            # 渲染进程:index.html + 压缩过的 assets

三步都显示 ✓ built,渲染层压缩后的 JS/CSS 在 out/renderer/assets/。

Step 6:electron-builder 打包

先验证整个应用能用生产产物运行(不打包):

npm run start    # electron-vite preview,加载 out/ 里的产物

然后产出未压缩应用目录(macOS 得到 .app,比 DMG 快,无签名也能跑):

npx electron-builder --dir

预期产物:

dist/mac/evite-demo.app
  • 首次会额外下载 Electron 发行版,命中 07-environment-variables 的 electron-builder 缓存后再次打包很快
  • 未签名会有 skipped macOS application code signing 提示,本地调试可忽略

也可以跑完整安装包(macOS 生成 .dmg):

npm run build:mac

打包脚本定义在 package.json,build:mac / build:win / build:linux 分别对应;出 DMG 比 --dir 慢。

关键坑回顾

坑解决
渲染进程 window.api 是 unknown在 src/preload/index.d.ts 补全局类型(Step 4)
开发模式正常、打包后窗口空白检查主进程是否按 ELECTRON_RENDERER_URL 分流加载 URL / 本地文件
require('electron-updater') 报 CJS 错误模板生产依赖按需升级 ESM 用法,快速升级请查阅该模块 README
打包下载慢/失败配置 ELECTRON_BUILDER_BINARIES_MIRROR + ELECTRON_MIRROR,见 07-environment-variables
打包产物不可运行先 npm run start 验证生产产物,再排查签名/资源路径
打包配置不生效独立配置文件名前缀固定为 electron-builder,优先级 .yml > .ts;electron-builder.config.ts 不是自动发现名(详见 electron-builder)

下一步