Lesson 01 · 进程与边界
在 Electron 项目里,「代码跑在哪个进程」决定了你该去哪个控制台找问题。 分不清,你会在错的地方找一下午。
gameboxclient 工作区快照。 文中每条实现描述都带
file:line,写的时候逐条核对过。
你熟悉的 Laya 项目跑起来是一件事:起个服务,浏览器打开,完事。 浏览器里的代码就是全部的代码。
Electron 不是。它同时跑着两个完全独立的 Node/Chromium 进程, 它们之间连变量都不共享。你写的代码分散在这两边,
而且从长相上看不出区别 —— 都是 TypeScript,都能写 import。
这带来一个非常具体的后果。假设界面上一个按钮点了没反应,你打开 DevTools,控制台干干净净,什么错误都没有。
这时候不是「没有错误」,是「错误发生在你看不到的地方」。 那个按钮的处理函数可能把活儿交给了主进程,主进程报的错在另一个终端窗口里。 这一课就是让你不再掉进这个坑。
| 层 | 代码在哪 | 它是什么 | 能干什么 |
|---|---|---|---|
| 渲染进程 | src/** |
一个 Chromium 页面。DOM、Vue、CSS 都在这 | 画界面、发 HTTP 请求。本项目里它还能直接用 Node —— 后面会讲这为什么不正常 |
| 主进程 | electron/main/** |
一个 Node.js 进程,负责管窗口和操作系统交互 | 开关窗口、系统菜单、自动更新、文件对话框、fork 子进程 |
| 子进程 | extend/worker/*.js |
主进程 fork() 出来的独立 Node 进程 |
干耗时的脏活:下载几个 G 的资源包、解压。放在主进程会卡住整个应用 |
主进程的入口小得惊人,只有两行 (electron/main/index.ts):
import { ready } from './handler/ready'
ready()
真正的启动流程在 electron/main/handler/ready.ts:23。 完整的八步时序画在了 架构图 FIG.02,这里不重复。
直接 npm i 会失败
# ❌ peer dependency 冲突
npm i
# ✅
npm i --legacy-peer-deps
# ✅ 或者用项目自带的(等价,还会顺带跑 cpsvn 脚本)
npm run i
原因不在你。仓库里原本有个 .npmrc 写着
legacy-peer-deps=true,git 历史里还能看到当初专门为此做的提交
(chore(npm): .npmrc 增加 legacy-peer-deps,让裸 npm i 也能装上), 但当前工作区里这个文件不见了。
❌ 别学
把「怎么装依赖」这种信息藏在一个叫
i 的脚本名里。
新人打开项目的第一反应永远是 npm i, 没有人会想到去翻
package.json 找一个单字母脚本。
规范做法:装依赖的特殊要求要么写进 .npmrc
让它自动生效,要么写在 README 第一屏。
你可以顺手修:把 .npmrc 加回去。
一行改动,是个很好的第一次提交。
npm run dev
它实际是 vite --mode base。发生的事情有两件:
src/ 下的代码;
vite-plugin-electron 编译 electron/main/,
然后拉起 Electron 进程去加载那个 dev server (vite.config.ts
的 onstart 里
options.startup(['.', '--no-sandbox']))。
所以你的终端里同时混着两个进程的输出: Vite 的构建日志和
Electron 主进程的 console.log。
而渲染进程的日志不在终端里,在应用窗口的 DevTools 里。
按 Shift+Cmd/Ctrl+I。 这个快捷键不是
Electron 默认给的,是项目自己注册的: 菜单模板定义在
electron/main/config/menu.ts 的
debugMenu,由渲染层在
src/App.vue:50 发一个 debug IPC
消息触发装配。
在多数项目里,登录就是「换个页面」。在这个项目里不是。
登录接口 /user/info 的响应里带着一个 game 字段,
里面装着
SVN 账号密码、Jenkins 地址账号 token、GM 接口地址、项目编码。 src/utils/extend/envOption.ts:8 的
initEnv() 把它们逐个写进 Pinia,此后整个应用都从那里取配置。
源码里用到约 50 个 VITE_* 键, 而
.env 系列文件加起来只定义了 11 个。
差额全靠登录下发。
登录成功后立刻做这一步
打开 DevTools 控制台,敲:
JSON.parse(localStorage.getItem('GameENV'))
应该能看到 VITE_SVN_URL、VITE_JENKINS_URL、
VITE_API_BASEPATH 这些键有值。
记住这条命令。它是你未来排查「点了没反应」类问题的第一条命令, 能省掉大量无效排查。完整链路见 架构图 FIG.03「钥匙链」。
现在给你三段真实代码。先别往下看答案, 自己判断每一段跑在哪一层。
const mainWindow = new BrowserWindow(mergeOption)
manager.addWindow(mainWindow, windowNames.main, true)
mainWindow.setMinimumSize(1280, 960)
process.on('message', (msg) => {
if (msg.type === 'start') {
const { url, outputPath, name } = msg
const file = fs.createWriteStream(outputPath)
// …
}
})
const clickDownloadCosResources = async () => {
const pathDir = DialogFunctions.showSaveDialog()
// …
Fs.writeFileSync(localPath, data)
ElMessage({ type: 'success', message: '下载成功' })
}
片段 C 跑在哪一层?
答案:C(渲染进程)。这段代码出自
src/views/GameEditor/cosDownload/index.vue, 是一个
<script setup> 里的按钮处理函数。
它为什么能写文件?因为这个项目开了
nodeIntegration,渲染层可以直接
import Fs from 'node:fs'。
这在标准 Electron 应用里是做不到的 ——
正是下一节要讲的重点。
片段 A 是主进程(electron/main/helpers/window.ts:33),
片段 B 是子进程(extend/worker/download.js, 用
process.on('message') 跟父进程通信是 fork 子进程的标志)。
标准的 Electron 应用里,渲染进程不能碰 Node。
它就是个普通网页,想读文件必须请主进程代劳。 中间那道门叫
preload:一个特殊脚本, 在页面加载前运行,用
contextBridge 把明确列出的几个方法 挂到
window 上。页面只能用这几个,别的一概摸不到。
本项目没有这道门。三件事叠加:
electron/main/config/options.ts:6-8 ——
窗口配置里 nodeIntegration: true、contextIsolation: false、 webSecurity: false,三个安全开关全关。
vite.config.ts:58-72 —— preload
的构建配置整段被注释掉。 文件
electron/preload/index.ts 存在,里面还写着规规矩矩的
contextBridge.exposeInMainWorld,但它从来没被编译过。
src/utils/extend/electron.ts:1 —— 取而代之的是
@electron/remote:渲染层可以直接抓到主进程的
dialog、app、process、
BrowserWindow 对象。
所以片段 C 里的 DialogFunctions.showSaveDialog()
—— 一个本该属于主进程的系统对话框 —— 在渲染层被直接调用了。
⚠️ 隔离
渲染层全开 Node 权限。这是架构级决定,
src/ 下几百个文件建立在它之上。你接手第一个月绝不该动它。
但要清楚代价:渲染层加载的任何内容 ——
包括从服务端拿到的、被渲染进页面的任何东西 —— 都拥有读写整台机器的能力。
webSecurity: false 还额外关掉了同源策略。
规范做法是保持 contextIsolation: true, 用
preload + contextBridge 只暴露必要方法。见
Electron · Context Isolation
和
Electron · Security Checklist。
你现在该遵守的规矩:新写的代码不要继续扩散 @electron/remote。要用主进程能力就老实加一条 IPC, 至少让边界是显式的、能在
channel.ts 里一眼数完的。
✅ 照做
重活 fork 给子进程(片段 B)。 下载和解压是 CPU / IO
密集的长任务,放在主进程会让整个应用的窗口失去响应
(主进程也负责窗口消息循环)。项目把它们放到
extend/worker/*.js,用 process.send() 回传进度
—— 这是标准做法,新功能可以照抄这个模式。
| 症状 | 先怀疑哪层 | 去哪看 |
|---|---|---|
| 页面白屏、样式错乱、按钮点了没反应 | 渲染进程 | DevTools 控制台 |
| 请求 404 / 403 / 打到了错的服务器 | 渲染进程 |
DevTools Network(展开 Payload 看 cmd 字段)
|
| 窗口起不来、应用直接白着不动 | 主进程 | 启动 Electron 的那个终端 |
| 菜单 / 快捷键 / 系统对话框失灵 | 主进程 | 同上 |
| 下载或解压进度条卡住不动 | 子进程 |
最难查。子进程自己的 console 看不到,只能沿
process.send → 主进程 → 渲染层这条链加日志
|
| dev 正常,打包后才出问题 | 主进程的路径计算 |
handler/ready.ts:10-20。PUBLIC 显式按
app.isPackaged 分两种算法,其余路径全从
__dirname 推 —— 而 __dirname
在打包前后指向完全不同的位置
|
electron-log 只在非 development 模式下才初始化 (electron/main/handler/ready.ts:24-26)。 开发时你在 Logger.info() 里写的东西只会出现在终端,
不会落进日志文件。反过来,用户报障时要的日志文件,只有正式包才有。
npm i --legacy-peer-deps 装完没有报错npm run dev,应用窗口起来了JSON.parse(localStorage.getItem('GameENV')) 能打出一个带
VITE_SVN_URL 等键的对象
require('os').platform()。它能返回结果
—— 在正常的网页里这行会直接报 require is not defined。
这一行就是「渲染层全开 Node 权限」最直观的证据
如果你在 macOS 上
登录时控制台会报几个错误,跟 initNginx /
moveTemp 有关 (views/ReLogin/index.vue:256,273)。 它们要跑的是 .exe,Mac 上必然失败。
这不影响登录本身,现阶段可以忽略。 但它是一个信号:这个工具实际上是 Windows 优先的。 完整影响范围见依赖清单。