手把手搭 AnchorChat 空壳:Vite 双入口、主进程开窗口、Vue 3 挂载。让你 npm start 看见第一个 Electron 窗。
开场:白板上的框,得先能弹出来
Day 1 我们在白板上画好了三栏:左账号、中聊天、右工具。老王问:「啥时候能跑?」我说:「明天。」——然后 Day 2 真的来了,发现 Electron 项目不是 create-react-app 一行命令就完事。
AnchorChat 要同时跑三个世界:
- 主进程(Node):开窗口、注册 IPC、以后管 SQLite
- 预加载(preload):/renderer 和主进程之间的安检通道
- 渲染进程(Vue):你看得见的 UI
今天目标很克制:不搞登录、不搞 webview、不搞翻译。只要 npm start 之后,屏幕上出现一个能点的 Electron 窗口,里面 Vue 能挂载——就像婴儿迈出第一步,丑没关系,能站住就行。
今天做什么:从 0 到 npm start
1. 目录先摆对
我们用的是 Vite 渲染 + vite-plugin-electron 编主进程 的方案,根目录大致这样:
anchor-chat-client/
├── electron/
│ ├── main.ts # 主进程入口
│ └── preload.ts # 预加载
├── src/
│ ├── index.ts # Vue 入口
│ ├── App.vue
│ └── router/ # 路由(今天只 做到登录页占位)
├── index.html # Vite 挂载点
├── vite.config.ts
└── package.json
别被「三个入口」吓到——开发时你只敲一条命令,插件帮你把 main/preload 编译进 dist-electron/。
2. package.json:告诉 Electron 主文件在哪
Electron 启动时会读 package.json 的 main 字段。开发模式下 Vite 插件会把 electron/main.ts 编译到 dist-electron/main.js,所以脚本长这样:
{
"name": "anchor-chat",
"type": "module",
"scripts": {
"start": "vite",
"build": "vite build && electron-builder"
},
"main": "dist-electron/main.js",
"devDependencies": {
"electron": "^29.4.6",
"vite": "^5.1.6",
"vite-plugin-electron": "^0.28.6",
"vue": "^3.4.21"
}
}
npm start 本质是跑 Vite dev server,同时插件 watch 编译主进程,编译完自动拉起 Electron——热更新双轨并行,比老 webpack 时代省心一截。
3. vite.config.ts:双入口一条线串起来
核心就一段 electron() 插件配置:
import { defineConfig } from 'vite'
import path from 'node:path'
import electron from 'vite-plugin-electron/simple'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [
vue(),
electron({
main: {
entry: 'electron/main.ts',
},
preload: {
input: path.join(__dirname, 'electron/preload.ts'),
},
}),
],
resolve: {
alias: [{ find: '@', replacement: path.resolve('src') }],
},
})
这里发生了三件事:
- Vue 插件处理
src/和index.html(渲染进程) main.entry把electron/main.ts打成dist-electron/main.jspreload.input把 preload 打成dist-electron/preload.mjs
4. electron/main.ts:主进程只干一件事——开窗
Day 2 的 main.ts 可以瘦到只剩骨架(托盘、单实例、IPC 都是 Day 3 起的活):
import { app, BrowserWindow } from 'electron'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
process.env.APP_ROOT = path.join(__dirname, '..')
const VITE_DEV_SERVER_URL = process.env['VITE_DEV_SERVER_URL']
const RENDERER_DIST = path.join(process.env.APP_ROOT, 'dist')
let win: BrowserWindow | null = null
function createWindow() {
win = new BrowserWindow({
width: 1250,
height: 690,
autoHideMenuBar: true,
minWidth: 900,
minHeight: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.mjs'),
contextIsolation: true,
nodeIntegration: false,
},
})
// 开发:加载 Vite 地址;打包:加载 dist/index.html
if (VITE_DEV_SERVER_URL) {
win.loadURL(VITE_DEV_SERVER_URL)
} else {
win.loadFile(path.join(RENDERER_DIST, 'index.html'))
}
}
app.whenReady().then(() => {
createWindow()
})
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit()
})
两个分支很重要:
- 开发:
VITE_DEV_SERVER_URL由插件注入,走 HMR,改 Vue 秒刷新 - 生产:
loadFile读打包后的dist/index.html
contextIsolation: true + nodeIntegration: false 是 Electron 12+ 的默认安全姿势——渲染进程不能直接 require('fs'),以后走 preload + IPC。
5. 渲染进程:src/index.ts 挂上 Vue
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'
import router from './router'
createApp(App)
.use(ElementPlus)
.use(router)
.mount('#app')
App.vue 里一个 <router-view /> 就够。路由默认 / → /login,所以今天窗口里大概率是 登录页占位——别慌,今天看见登录框说明链路通了。
跑通后的目录产物:
dist/— 渲染进程打包结果(开发时走 dev server,不一定落盘)dist-electron/main.js、preload.mjs— 主进程与 preload
踩坑与思考
main路径不对 → Electron 启动即崩。一定要等 Vite 插件先把main.ts编译完;第一次npm start多等几秒是正常的。- 白屏但无报错:多半是
loadURL的端口和 Vite 不一致,或index.html里#app写错。DevTools 里看 Network 最快。 - 安全默认值别省。为了赶进度开
nodeIntegration: true能省半小时,以后 preload 重构能省你三天——Trust me bro(别 trust)。
你们 Electron 项目用的是 Vite 还是 webpack?第一次 npm start 卡了多久?评论区报时——Day 3 见。