electron之从无到有

Posted by blueblue on March 13, 2026

全局配置

1. 为什么选择Electron+ Vite +Vue3这套组合?

简单来说,Electron让你能用前端技术写桌面应用,Vite负责极速的开发体验,Vue3提供了现代、响应式的UI框架,TypeScript帮你提前发现代码里的“坑”。这几者结合在一起,就像给开发流程装上了涡轮增压。

2. 环境准备与项目初始化

2.1 确保你的开发环境就绪

在开始之前,你得先检查一下自己的“装备”。Node.js版本很关键,我强烈推荐使用Node.js 20.x LTS或更高版本。为什么不是最新的22.x?因为很多Electron相关的库对20.x的支持最稳定,我在22.x上遇到过一些奇怪的兼容性问题。你可以用node -v命令检查版本。

包管理器方面,pnpm是我的首选。它比npm和yarn快得多,而且磁盘空间占用小,对于Electron这种依赖多的项目特别友好。如果你还没装pnpm,用这个命令安装就行:npm install -g pnpm

另外,我建议装个代码编辑器,VS Code是绝大多数Electron开发者的选择。装上Volar(Vue3官方扩展)、TypeScript扩展和Electron相关的插件,开发体验会提升一个档次。

2.2 一键创建你的第一个项目

准备工作做完,咱们就可以创建项目了。electron-vite官方提供了一个超级方便的脚手架工具,让你不用再手动配置那些繁琐的Vite和Electron集成。

打开你的终端,运行下面这个命令(我用的是pnpm,你用npm或yarn也行):

1
pnpm create @quick-start/electron@latest mytemplate

运行后,命令行会问你几个问题,就像下面这样:

1
2
3
4
5
✔ Project name: … mytemplate
✔ Select a framework: › vue
✔ Add TypeScript? … Yes
✔ Add Electron updater plugin? … Yes
✔ Enable Electron download mirror proxy? … No(可选)

这里有几个选择我解释一下。框架肯定选Vue,TypeScript强烈建议选Yes,虽然刚开始可能有点不习惯,但用久了你会发现它帮你避免了很多低级错误。Electron updater插件是用来做自动更新的,如果你打算发布正式版本,最好选上。最后一个镜像代理,如果你在国内网络环境不太好,可以选Yes加速下载。

选完之后,脚手架会自动创建项目结构并安装依赖。整个过程大概一两分钟,取决于你的网速。我第一次用的时候,看到那一堆依赖自动安装,感觉特别省心,以前手动配置至少要折腾半小时。

2.3 理解生成的项目结构

项目创建好后,用VS Code打开,你会看到这样的目录结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
mytemplate/          # 项目根目录
├── resources/               # 应用静态资源目录(开发/打包均需用到)
│   ├── icon.icns            # Mac 系统应用图标
│   └── icon.ico             # Windows 系统应用图标
├── src/                     # 项目源码核心目录
│   ├── main/                # Electron 主进程目录(Node.js 环境)
│   │   └── index.ts         # 主进程入口文件(创建窗口、IPC 通信等核心逻辑)
│   ├── preload/             # 预加载脚本目录(桥接主进程和渲染进程)
│   │   └── index.ts         # 预加载入口文件(暴露安全的 API 给渲染进程)
│   └── renderer/            # 渲染进程目录(Vue3 + Web 环境)
│       ├── src/             # 渲染进程源码子目录(符合 Vue 项目通用规范)
│       │   ├── assets/      # Vue 静态资源(图片、样式、字体等)
│       │   ├── components/  # Vue 公共组件(可复用的 UI 组件)
│       │   ├── App.vue      # Vue 根组件(渲染进程页面入口)
│       │   └── main.ts      # 渲染进程入口文件(创建 Vue 实例、挂载 DOM)
│       └── index.html       # 渲染进程入口 HTML(Vue 挂载的根容器)
├── electron.vite.config.ts  # Electron + Vite 核心配置文件(配置别名、打包规则等)
├── package.json             # 项目依赖/脚本配置文件
├── tsconfig.json            # TypeScript 根级配置(公共编译规则)
├── tsconfig.node.json       # 主进程/预加载脚本的 TS 配置(Node.js 环境)
└── tsconfig.web.json        # 渲染进程的 TS 配置(Web 环境,适配 Vue)

这个结构比传统的Electron项目清晰多了。以前我经常搞混主进程和渲染进程的文件该放哪,现在electron-vite帮你分得清清楚楚。main目录下放Electron主进程的逻辑,比如创建窗口、处理系统菜单这些;preload是预加载脚本,负责在渲染进程和主进程之间安全地通信;renderer就是你的Vue3前端代码了。

我特别喜欢这种分离的设计,因为它强制你遵循Electron的安全最佳实践。很多新手容易犯的错误就是把Node.js的模块直接在前端页面里引入,这会有安全风险。现在有了预加载脚本这个明确的边界,你就知道哪些API该通过预加载暴露给前端。

3. 核心配置详解与优化

3.1 electron.vite.config.ts深度解析

这个配置文件是整个项目的“大脑”,理解了它,你就掌握了electron-vite的精髓。默认生成的配置已经能工作了,但根据我的经验,有几个地方你最好调整一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import { resolve } from 'path'
import { defineConfig } from 'electron-vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  main: {
    build: {
      rollupOptions: {
        input: {
          index: resolve(__dirname, 'src/main/index.ts')
        },
        external: [] // 如需外部化特定依赖,可在这里配置,例如 ['electron']
      }
    }
  },
  preload: {
    build: {
      rollupOptions: {
        input: {
          index: resolve(__dirname, 'src/preload/index.ts')
        },
        external: []
      }
    }
  },
  renderer: {
    root: resolve(__dirname, 'src/renderer/'),
    plugins: [vue()],
    resolve: {
      alias: {
        '@': resolve(__dirname, 'src/renderer/src')
      }
    },
    server: {
      port: 5173,
      strictPort: true
    }
  }
})

我重点说几个关键点。

resolve.alias配置了@指向renderer目录,这样你在Vue组件里就可以用@/components/Button.vue这样的路径了,比相对路径清晰很多。

server.port我固定为了5173,strictPort: true确保端口被占用时会报错而不是自动换端口。为什么这么做?因为Electron启动时需要连接这个开发服务器,如果端口变来变去,Electron就找不到你的前端页面了。这个坑我踩过,调试了半天才发现是端口问题。

3.2 TypeScript配置的实战技巧

TypeScript的配置在tsconfig.jsontsconfig.web.jsontsconfig.node.json里。默认配置能用,但如果你想获得更好的开发体验,我建议加几个选项。

tsconfig.web.json里,加上这些:

1
2
3
4
5
6
7
8
9
{
  "compilerOptions": {
    "strict": true,
    "types": ["vite/client"],
    "paths": {
      "@/*": ["src/renderer/*"]
    }
  }
}

strict: true开启所有严格检查,刚开始可能会有点痛苦,报很多错,但坚持下来,你的代码质量会高很多。types: ["vite/client"]让TypeScript认识Vite特有的导入语法,比如导入.vue文件。

对于主进程的TypeScript配置,有个特别的地方。Electron主进程运行在Node.js环境,所以你需要安装Node.js的类型定义:pnpm add -D @types/node。然后在主进程的代码里,你可能会遇到Electron API的类型问题,这时候可以这样处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// src/main/index.ts
import { app, BrowserWindow, type BrowserWindowConstructorOptions } from 'electron'
import path from 'path'

// 这样就有完整的类型提示了
const createWindow = (): void => {
  const options: BrowserWindowConstructorOptions = {
    width: 1200,
    height: 800,
    webPreferences: {
      preload: path.join(__dirname, '../preload/index.js'),
      contextIsolation: true,
      nodeIntegration: false
    }
  }
  const mainWindow = new BrowserWindow(options)
  // ... 其他代码
}

我刚开始用的时候,总觉得TypeScript麻烦,一个简单的窗口创建都要写类型。但用久了发现,它真的能帮你避免很多运行时错误。比如有一次我写window.width(应该是window.getSize()[0]),TypeScript直接报错,省了我调试的时间。

3.3 开发脚本与工作流优化

package.json里的scripts是每天都要用的,配置好了能极大提升效率。除了默认的,我一般还会加几个:

1
2
3
4
5
6
7
8
{
  "scripts": {
    "preview": "electron-vite preview",
    "dev:debug": "electron-vite dev --debug",
    "dev:inspect": "electron-vite dev --inspect",
    "type-check": "vue-tsc --noEmit"
  }
}

dev:debugdev:inspect是我强烈推荐的。Electron调试有时候挺头疼的,特别是主进程的bug。加上--debug参数,你可以用Chrome DevTools调试主进程;--inspect则开启Node.js调试器,可以在VS Code里打断点。

type-check命令单独做类型检查,有时候开发时Vite的TypeScript检查不够全面,用这个命令做一次全面检查,确保没有类型问题。

lint用ESLint统一代码风格,团队协作时特别重要。我建议在提交代码前跑一下这个命令,保持代码库整洁。

实际开发中,我的工作流是这样的:开一个终端跑pnpm dev,另一个终端跑pnpm type-check --watch(实时类型检查),VS Code里开着ESLint插件。这样写代码的时候,类型错误、语法问题都能实时看到,基本不用等到编译时才发现问题。


Vue配置

第一步:用 pnpm 安装依赖

在项目根目录执行(不要进 render 文件夹):

1
2
3
4
5
# 安装核心依赖(对应 npm install)
pnpm add pinia vue-router axios

# 安装 TypeScript 类型支持(开发依赖,对应 npm install -D)
pnpm add -D @types/vue-router

第二步:配置 Vue Router

render/src 下创建 router 目录,新建 index.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// render/src/router/index.ts
import { createRouter, createWebHashHistory } from 'vue-router'
import Home from '../views/Home.vue' // 如果你有这个页面

const routes = [
  { path: '/', name: 'Home', component: Home },
  { path: '/about', name: 'About', component: () => import('../views/About.vue') } // 懒加载
]

const router = createRouter({
  history: createWebHashHistory(), // Electron 推荐用 Hash 模式,避免打包后路径问题
  routes
})

export default router

第三步:配置 Pinia

render/src 下创建 stores 目录,新建 counter.ts(示例 Store):

1
2
3
4
5
6
7
8
9
10
11
// render/src/stores/counter.ts
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => ({ count: 0 }),
  actions: {
    increment() {
      this.count++
    }
  }
})

第四步:封装 Axios

封装 Axios

render/src 下创建 utils 目录,新建 request.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// render/src/utils/request.ts
import axios from 'axios'

const service = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL,
  timeout: 10000
})

// 请求拦截器(比如添加 Token)
service.interceptors.request.use(
  (config) => {
    // 示例:从 Pinia 或 localStorage 取 Token
    // const token = localStorage.getItem('token')
    // if (token) config.headers.Authorization = `Bearer ${token}`
    return config
  },
  (error) => Promise.reject(error)
)

// 响应拦截器(处理错误)
service.interceptors.response.use(
  (response) => response.data,
  (error) => {
    console.error('请求失败:', error)
    return Promise.reject(error)
  }
)

export default service

创建 API 接口

src 目录下创建 api/index.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import request from '../utils/request'

// 示例接口
export function getExampleData(params) {
  return request({
    url: '/example',
    method: 'get',
    params
  })
}

export function postExampleData(data) {
  return request({
    url: '/example',
    method: 'post',
    data
  })
}

配置环境变量

在项目根目录创建 .env.development

1
2
# 开发环境配置
VITE_API_BASE_URL=http://localhost:3000/api

创建 .env.production

1
2
# 生产环境配置
VITE_API_BASE_URL=https://api.example.com

第五步:在页面中挂载

修改main.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// render/src/main.ts
import './assets/main.css'

import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'

const app = createApp(App)

const pinia = createPinia()
app.use(pinia)

app.use(router)

app.mount('#app')

第六步:创建示例页面并使用

1. 创建 render/src/views/Home.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<!-- render/src/views/Home.vue -->
<template>
  <div>
    <h1>首页</h1>
    <p>Pinia 计数: </p>
    <button @click="counterStore.increment">点我 +1</button>
    <button @click="goToAbout">跳转到 About 页</button>
    <button @click="testAxios">测试 Axios 请求</button>
  </div>
</template>

<script setup lang="ts">
import { useCounterStore } from '@/stores/counter' // 用 @ 别名导入
import { useRouter } from 'vue-router'
import request from '@/utils/request' // 导入封装的 Axios

const counterStore = useCounterStore()
const router = useRouter()

// 路由跳转
const goToAbout = () => {
  router.push('/about')
}

// 测试 Axios
const testAxios = async () => {
  try {
    const res = await request.get('/api/hello') // 替换成你的测试接口
    console.log('请求成功:', res)
  } catch (err) {
    console.error('请求失败:', err)
  }
}
</script>

2. 创建 render/src/views/About.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- render/src/views/About.vue -->
<template>
  <div>
    <h1>关于页</h1>
    <button @click="router.push('/')">返回首页</button>
  </div>
</template>

<script setup lang="ts">
import { useRouter } from 'vue-router'

const router = useRouter()
</script>

3. 修改 render/src/App.vue

1
2
3
4
<!-- render/src/App.vue -->
<template>
  <router-view /> <!-- 路由出口 -->
</template>

第七步(可选):安装vue devtools

步骤 1:获取 Vue Devtools 扩展文件

  • GitHub 仓库下载源码压缩包。
  • 解压进入项目目录,并安装依赖
  • 依赖安装成功后,进行打包:
1
2
pnpm install
pnpm build:chrome-extension

步骤 2:集成到electron 项目 在 Electron 的主进程文件(如 electron/main/index.ts)中添加:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
import { app, shell, BrowserWindow, ipcMain, session } from 'electron' // 新增 session,用于加载 Chrome 扩展
import { join, resolve } from 'path' // 新增 resolve,用于精准解析扩展路径
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
import icon from '../../resources/icon.png?asset'

function createWindow(): void {
  const mainWindow = new BrowserWindow({
    width: 900,
    height: 670,
    show: false,
    autoHideMenuBar: true,
    ...(process.platform === 'linux' ? { icon } : {}),
    webPreferences: {
      preload: join(__dirname, '../preload/index.js'),
      sandbox: false
    }
  })

  mainWindow.on('ready-to-show', () => {
    mainWindow.show()
  })

  mainWindow.webContents.setWindowOpenHandler((details) => {
    shell.openExternal(details.url)
    return { action: 'deny' }
  })

  if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
    mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
  } else {
    mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
  }
}

// 仅开发环境加载 Vue DevTools 的函数
async function loadVueDevTools(): Promise<void> {
  if (!is.dev) return // 仅开发环境执行,生产环境直接返回

  try {
    // 根据实际路径改动
    const devtoolsPath = resolve(__dirname, '../../devtools/7.7.7_0')
    // 使用 session 加载本地 Chrome 扩展,loadExtension会显示已弃用,但仍然可用
    const extension = await session.defaultSession.loadExtension(devtoolsPath)
    console.log(`Vue DevTools 加载成功: ${extension.name}`)
  } catch (err) {
    console.error('Vue DevTools 加载失败:', err)
  }
}

// 将回调函数改为 async,以支持 await 加载扩展
app.whenReady().then(async () => {
  electronApp.setAppUserModelId('com.electron')

  app.on('browser-window-created', (_, window) => {
    optimizer.watchWindowShortcuts(window)
  })

  // 在创建窗口前加载 Vue DevTools(仅开发环境)
  await loadVueDevTools()

  ipcMain.on('ping', () => console.log('pong'))

  createWindow()

  app.on('activate', () => {
    if (BrowserWindow.getAllWindows().length === 0) createWindow()
  })
})

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit()
  }
})

最后:启动项目

在根目录执行:

1
pnpm dev

全局配置 节选自CSDN 黑虾电影并稍作改动