1. 从零开始理解这个启动器到底要解决什么问题很多人第一次听到游戏启动器这个词脑子里浮现的可能是 Steam、Epic 那种庞然大物。但我要聊的这个东西完全不是那个量级——它是一个跑在本地、用 Electron 套壳、前端用 HTML/CSS/JavaScript 写的轻量级工具核心目标只有一个把散落在硬盘各个角落的游戏快捷方式、安装目录、启动参数、封面图统一管起来点一下就能启动不用再满桌面找图标。我自己收藏的游戏大概有三百多个分布在三块硬盘上。早期我用的是文件夹分类法后来发现根本不够用——同一个游戏可能同时存在本体汉化补丁修改器存档备份四个目录时间一长自己都记不清哪个是哪个。更麻烦的是有些游戏需要特定的启动参数比如窗口化、跳过开场动画、指定显卡每次手动敲命令行简直是折磨。这就是我决定自己动手写一个启动器的直接动机。这个项目的技术栈选择其实很朴素Electron 负责跨平台桌面壳HTML/CSS 负责界面JavaScript 负责全部逻辑。没有用 Vue、React 这类框架原因后面会详细说。关键词里出现的electron 主渲染进程 ipc 通信、electron 菜单、electron 打包 vue 项目这些说明很多人关心的其实是 Electron 的工程化问题而我想从一个已经跑起来并且日常在用的角度把这些点串起来讲清楚。适合读这篇内容的人有三类一是想学 Electron 但不知道拿什么练手的开发者二是游戏库比较乱、想自己搞个管理工具的玩家三是已经会用 HTML/CSS/JS但没接触过桌面端打包的前端。我会尽量把每个决策背后的为什么讲透而不是只丢一堆代码。2. 为什么我放弃了 Vue 而选择原生三件套2.1 框架带来的收益在这个场景里几乎为零一开始我确实试过用 Vue 搭这个启动器。脚手架一跑路由、状态管理、组件库全给你配好看起来很爽。但真正写起来才发现这个应用的界面复杂度极低一个游戏列表、一个详情面板、一个设置页总共就三个视图。用 Vue 的话我得为每个游戏卡片写一个组件为列表写一个容器组件为详情写一个 props 传递链——结果代码量比原生写法还多。更关键的是打包体积。Vue 的运行时加上 vue-router、pinia压缩后大概 100KB 出头。听起来不多但 Electron 本身的 Chromium 内核已经让安装包到了 80MB 级别再叠这些其实无所谓。真正让我放弃的是调试链路变长改一个样式要等热更新偶尔热更新抽风还得重启整个 Electron 进程。原生写法下我直接改 CSS 文件CtrlR刷新渲染进程一秒看到结果。所以我的结论是界面状态少于 5 个、组件复用率低于 30% 的桌面工具没必要上框架。这不是说框架不好而是工具选型要看场景。如果你要做的是一个有几十个页面、多人协作的中型应用那 Vue 的收益就出来了。2.2 原生写法下如何组织代码才不乱不用框架不代表代码就可以随便堆。我的目录结构是这样的fitgirl-launcher/ ├── main.js # 主进程入口 ├── preload.js # 预加载脚本暴露安全 API ├── renderer/ │ ├── index.html # 主界面 │ ├── style.css # 全部样式 │ ├── app.js # 界面逻辑 │ └── games.js # 游戏数据操作 ├── assets/ │ └── covers/ # 封面图缓存 └── package.jsonmain.js只管窗口创建、菜单、文件系统访问、启动子进程这些重活。renderer目录下的东西全部跑在浏览器环境里只负责画界面和响应用户操作。两者之间通过preload.js暴露的接口通信渲染进程永远不直接碰 Node.js 的 API。这一点非常重要后面讲 IPC 的时候会展开。2.3 一个容易被忽略的坑nodeIntegration的取舍网上很多老教程会告诉你在webPreferences里把nodeIntegration设成true然后渲染进程里就能直接require(fs)。这在开发阶段确实方便但等于把整个 Node.js 环境暴露给了页面。虽然这个启动器只加载本地 HTML不存在远程注入的风险但养成坏习惯总是不好的。我的做法是nodeIntegration: falsecontextIsolation: true然后在preload.js里用contextBridge.exposeInMainWorld暴露有限的几个方法const { contextBridge, ipcRenderer } require(electron); contextBridge.exposeInMainWorld(launcherAPI, { getGames: () ipcRenderer.invoke(games:get), addGame: (game) ipcRenderer.invoke(games:add, game), launchGame: (id) ipcRenderer.invoke(games:launch, id), pickExecutable: () ipcRenderer.invoke(dialog:pickExe) });这样渲染进程里只能调用window.launcherAPI.getGames()这类白名单方法就算页面里不小心执行了恶意脚本也拿不到文件系统的完全控制权。多写这十几行换来的是架构上的干净。3. 主进程与渲染进程的通信把 IPC 讲透3.1 三种通信模式用错一种就等着调 bugElectron 的 IPC 本质上就三种模式但新手最容易混模式主进程 API渲染进程 API适用场景渲染→主单向ipcMain.onipcRenderer.send发个通知不需要回值渲染→主双向ipcMain.handleipcRenderer.invoke查询数据、执行操作并拿结果主→渲染单向webContents.sendipcRenderer.on主进程主动推消息给界面我踩过的第一个坑就是用send去做需要返回值的操作。比如读取游戏列表这个动作渲染进程发出去之后得等主进程读完 JSON 文件再传回来。用send的话你得在主进程里再webContents.send一次然后在渲染进程里监听——两套事件名很容易写错。后来全部改成invoke/handle代码直接少了一半。3.2 启动游戏这个动作的完整链路拿点击启动按钮举例完整流程是这样的渲染进程里用户点了某个游戏卡片上的启动按钮触发launchGame(id)。app.js调用window.launcherAPI.launchGame(id)。preload.js里这个函数执行ipcRenderer.invoke(games:launch, id)。主进程main.js里注册的ipcMain.handle(games:launch, ...)被触发。主进程从内存里的游戏数据中找到对应条目取出可执行文件路径和启动参数。用child_process.spawn启动游戏进程注意detached: true和stdio: ignore否则游戏关掉之前启动器会被卡住。返回一个{ success: true }给渲染进程界面弹个提示。第 6 步那个细节值得展开。默认情况下spawn出来的子进程和父进程是绑定的父进程退出会杀掉子进程子进程的输出也会占用父进程的管道。游戏这种长时间运行的进程必须解绑const child spawn(exePath, args, { detached: true, stdio: ignore, cwd: path.dirname(exePath) }); child.unref();cwd设成可执行文件所在目录也很关键。有些老游戏会从当前工作目录读取配置文件如果你不设置它可能去启动器的目录找结果就是配置丢失或者直接闪退。这个坑我调了整整一个下午才定位到。3.3 主进程主动推送消息的典型场景有一个场景必须用主→渲染的推送游戏进程退出后更新界面状态。用户启动游戏后启动器界面上那个游戏卡片应该显示运行中游戏关掉之后要变回未启动。实现方式是在spawn之后监听exit事件child.on(exit, (code) { mainWindow.webContents.send(game:exited, { id, code }); });渲染进程里window.launcherAPI.onGameExited((data) { updateGameStatus(data.id, idle); });这里有个细节preload.js里不能直接把ipcRenderer.on暴露出去因为那样渲染进程就能监听任意频道了。正确做法是包一层onGameExited: (callback) { ipcRenderer.on(game:exited, (event, data) callback(data)); }只暴露特定频道回调函数由渲染进程传入。这样既实现了功能又没有打开安全口子。4. 游戏数据的存储与封面图管理4.1 为什么用 JSON 文件而不是数据库游戏库这种数据量级最多几千条字段也就十来个用 SQLite 属于杀鸡用牛刀。我直接用了一个games.json存在app.getPath(userData)目录下。这个目录是 Electron 提供的标准用户数据位置Windows 下大概是C:\Users\你的用户名\AppData\Roaming\fitgirl-launcherMac 下是~/Library/Application Support/fitgirl-launcher。用这个目录而不是程序安装目录好处是卸载重装不丢数据而且不会因为程序装在Program Files里导致写入权限问题。我见过有人把配置写在__dirname下面结果用户装到系统盘就各种报错这个坑一定要避开。数据结构大概长这样{ version: 1, games: [ { id: a1b2c3, name: 示例游戏, exePath: D:/Games/Example/game.exe, args: -windowed -skipintro, cover: covers/a1b2c3.jpg, addedAt: 1700000000000, lastPlayed: 1700000000000, playCount: 12 } ] }version字段是为了以后做数据迁移用的。现在只有 v1但万一以后要改结构读文件的时候判断一下版本号就能平滑升级不至于让老用户的数据全废掉。4.2 封面图的两种获取策略封面图是这个启动器好看的关键。我的策略是优先本地、其次手动、最后才考虑网络。本地优先的意思是添加游戏时先扫描可执行文件所在目录找cover.jpg、folder.jpg、poster.png这类常见命名。很多游戏本体或者整合包会自带封面图直接拿来用最省事。如果本地没有就弹一个文件选择框让用户手动指定。这一步不能省因为自动从网络抓取封面涉及版权和稳定性问题——今天能用的图床明天可能就挂了而且不同地区的网络环境差异很大硬编码一个抓取源很容易失效。手动指定的图片会被复制到userData/covers/目录下用游戏 id 重命名。这样做的好处是原图被删除或移动后封面依然可用而且所有封面集中管理备份的时候直接打包这个目录就行。4.3 大图列表的性能处理三百多个游戏每个封面图按 200KB 算全部加载就是 60MB 内存。如果直接在列表里塞img标签滚动会明显卡顿。我的优化手段是懒加载 缩略图。懒加载用IntersectionObserver实现只有进入视口的卡片才真正设置srcconst observer new IntersectionObserver((entries) { entries.forEach(entry { if (entry.isIntersecting) { const img entry.target; img.src img.dataset.src; observer.unobserve(img); } }); }, { rootMargin: 200px });rootMargin设成 200px 是为了提前加载用户快速滚动时不会看到空白。缩略图则是在添加游戏时用sharp这个库把原图压到 400px 宽质量 80。这样列表里加载的是小图详情页才显示原图。实测下来三百个游戏的列表滚动帧率从 30fps 提到了稳定 60fps。5. 界面交互里那些看起来简单做起来烦的细节5.1 自定义标题栏与窗口控制Electron 默认的标题栏又丑又占地方我用了frame: false去掉它然后自己用 HTML 画一个。但去掉之后窗口的拖动、最小化、最大化、关闭都得自己实现。拖动靠 CSS 的-webkit-app-region: drag加在标题栏容器上。但要注意按钮区域必须设成no-drag否则点按钮会变成拖窗口.titlebar { -webkit-app-region: drag; } .titlebar button { -webkit-app-region: no-drag; }窗口控制按钮则通过 IPC 调用主进程的minimize()、maximize()、close()。这里有个小坑最大化按钮应该做成切换式的窗口已经最大化时点击要还原。主进程里监听maximize和unmaximize事件把状态推给渲染进程按钮图标跟着变。5.2 搜索与筛选的即时响应游戏多了之后搜索是刚需。我的实现是监听输入框的input事件但加了 150ms 的防抖。不防抖的话每敲一个字母就遍历一次三百条数据并重绘列表输入会有明显延迟。筛选维度我做了三个按名称模糊匹配、按最近游玩排序、按游玩次数排序。名称匹配用的是简单的includes没有上正则或者模糊匹配库——对于游戏名这种短字符串includes的性能完全够用而且行为可预测不会出现搜 A 出来 B的迷惑结果。5.3 右键菜单与批量操作列表项支持右键弹出上下文菜单选项包括打开所在文件夹编辑启动参数移除。这个菜单不是 HTML 画的而是用 Electron 原生的Menu.buildFromTemplate在主进程构建通过ipcMain.on(show-context-menu)触发。原生菜单的好处是样式和系统一致而且不会被窗口边界裁切。打开所在文件夹这个功能用shell.showItemInFolder(exePath)一行搞定比自己拼explorer /select,命令跨平台兼容性好得多。这也是 Electron 提供的一批实用 API 之一值得花时间翻一遍官方文档。6. 打包与分发从开发到能用的最后一步6.1 electron-builder 的配置要点打包我用的是electron-builder配置文件写在package.json的build字段里。几个关键点{ build: { appId: com.yourname.fitgirl-launcher, productName: FitGirl Launcher, directories: { output: dist }, files: [main.js, preload.js, renderer/**/*, assets/**/*], win: { target: [nsis], icon: assets/icon.ico }, nsis: { oneClick: false, allowToChangeInstallationDirectory: true } } }files字段一定要写清楚不然会把node_modules里一堆开发依赖也打进去安装包能胖一圈。oneClick: false让用户能选安装目录体验上更友好。6.2 打包后路径问题的排查思路开发时用__dirname读文件一切正常打包后经常报文件找不到。原因是打包后代码被塞进了app.asar归档文件里路径结构和开发时不一样。判断方法很简单在代码里打印__dirname对比开发时和打包后的值。如果需要在打包后访问真实文件系统路径比如读取用户放的封面图要用app.getPath(userData)或者process.resourcesPath而不是__dirname。我遇到过一个更隐蔽的问题assets/icon.ico在打包后读不到。解决办法是在build配置里显式声明extraResources把需要保持原始文件形态的资源复制到resources目录然后用process.resourcesPath拼接路径访问。6.3 自动更新的取舍electron-updater能实现自动更新但需要搭配一个更新服务器或者用 GitHub Releases。对于个人项目我最终没上自动更新——维护更新服务器的心力成本比让用户手动下载新版本高得多。我的做法是在设置页放一个检查更新按钮点击后请求一个静态 JSON 文件对比版本号有新版本就弹个提示带下载链接。简单、可控、零维护。7. 实际使用中积累的经验与踩坑记录7.1 启动参数里的空格和引号游戏路径里带空格是常态比如D:/My Games/Some Game/game.exe。用spawn的时候如果直接把整个路径当第一个参数传Electron 会正确处理。但如果路径里同时有空格和特殊字符就得小心了。我的经验是永远不要自己拼命令行字符串而是把路径和参数分开传spawn(exePath, argsArray, options)argsArray是参数数组每个参数独立一项。这样 Electron 内部会做正确的转义不用你操心引号问题。我早期图省事用exec拼字符串结果遇到带中文路径的游戏就各种乱码换成spawn加数组之后彻底解决。7.2 游戏启动失败的诊断方法用户反馈点了没反应是最难查的。我的做法是在主进程里捕获spawn的error事件把错误信息写到一个日志文件里child.on(error, (err) { fs.appendFileSync(logPath, [${new Date().toISOString()}] ${err.message}\n); });常见错误有三类路径不存在游戏被移动或删除、权限不足需要管理员权限的游戏、依赖缺失比如缺少某些运行库。日志里能看到具体是哪种排查起来快很多。设置页里我放了个打开日志按钮用户可以直接把日志发给我。7.3 多显示器下的窗口位置记忆用户把窗口拖到副屏关掉再打开窗口又回到主屏了——这是 Electron 的默认行为。解决办法是在窗口关闭时把x、y、width、height存到配置文件下次启动时读出来传给BrowserWindow的构造函数。但有个边界情况如果用户拔掉了副屏存下来的坐标可能落在不存在的区域窗口就消失了。所以读取坐标后要校验一下用screen.getAllDisplays()拿到所有屏幕的区域判断坐标是否在某个屏幕范围内不在就回退到默认位置。7.4 内存占用的长期观察Electron 应用常被诟病内存占用高。我这个启动器空载时大概占 120MB加载三百个游戏封面后涨到 200MB 左右。对于现代电脑来说完全可以接受但如果你的机器内存紧张有几个优化点一是前面说的懒加载和缩略图二是定期调用webContents.session.clearCache()清理缓存三是避免在渲染进程里保留大数组的副本数据尽量只在主进程存一份。我实测过一个反直觉的结论把游戏数据从渲染进程移到主进程管理后内存反而降了。因为渲染进程的 V8 实例和主进程是分开的数据放两份等于双倍占用。现在渲染进程只保留当前视图需要的那部分数据滚动时按需向主进程请求。8. 这个项目还能往哪些方向继续长写到这里这个启动器已经稳定跑了小半年日常使用没什么大问题。但作为一个持续迭代的个人项目还有几个方向我觉得值得做。一个是游戏时长统计。现在只记录了lastPlayed和playCount如果能在游戏进程启动和退出时打时间戳就能算出每个游戏的实际游玩时长做成周报或者年度总结会很有意思。技术上不难spawn的时候记开始时间exit事件里记结束时间差值累加到游戏数据里就行。另一个是启动前检查。有些游戏依赖特定的运行库或者需要关闭某些后台程序可以在启动前跑一段检查脚本不满足条件就提示用户。这个功能对折腾老游戏的玩家会很实用。还有就是配置文件的导入导出。现在换电脑得手动重新添加所有游戏如果能导出一个包含所有游戏信息的 JSON在新机器上导入后自动匹配路径比如从D:/Games换到E:/Games迁移成本会低很多。这些想法都不复杂但每一个都需要在实际使用中验证价值。我的原则是自己用着不爽了才动手做而不是为了功能列表好看去堆砌。毕竟这是个自用工具解决自己的问题才是第一位的。
企业数字化 ERP 产品动态
相关推荐
Flipper Zero BadUSB实战:绕过物理隔离的USB HID攻击链 1. 这不是玩具,是能敲开物理隔离系统的“数字万能钥匙”Flipper Zero BadUSB Payloads——光看这个标题,很多人第一反应是“黑客玩具”“极客玩具”,甚至觉得就是个带屏幕的U盘。但我在工控安全渗透测试现场连续三年用它做红队演练࿰… · 2026/9/26 4:56:33
长期生酮饮食伤心脏?机制解析与护心自救底线 生酮群里的打卡记录往往分两种:一种是体重秤的数字持续下滑,配一张满足的餐盘;另一种是同一个ID过几周又冒出来,问“最近心慌得厉害、早搏也变多了,还在坚持生酮,要不要紧”。大多数回答是:你是… · 2026/9/26 4:56:27
商务洽谈总记不住客户需求?我用这套方案,告别“会后失忆症” 做销售和商务的朋友应该都有过这种体验:一场客户面谈聊了两个小时,对方说了很多需求、顾虑、期望,当时觉得都记住了,可回到公司写跟进记录的时候,大脑却一片空白——客户到底强调了哪三点?那个预算范围是多… · 2026/9/26 5:26:00
SSE流式传输实战:从协议原理到生产环境避坑指南 1. 从一次线上事故说起:为什么流式传输值得单独拎出来讲去年帮一个团队排查线上问题,现象很典型:AI 对话页面在回答较长内容时,用户要盯着空白转圈十几秒,然后整段文字"啪"地一下全冒出来。产品经理觉得是模… · 2026/9/26 5:26:00
Steam游戏启动卡在正在启动?17步底层诊断与修复指南 1. 项目概述:为什么“正在启动”成了Steam玩家最熟悉的等待界面 你点开《赛博朋克2077》,鼠标悬停在“播放”按钮上,指尖一按——屏幕右下角弹出小窗口:“正在启动”,进度条纹丝不动。你盯着它看了30秒、60秒、两分钟… · 2026/9/26 5:25:48
【行空板K10】从环境搭建到用华为云码道生成「中秋快乐」 文章目录一、前言二、软件安装与工程配置2.1 安装 PlatformIO(以 VSCode 为例)2.2 新建工程并配置 platformio.ini2.3 跑通官方测试代码三、踩坑记录:中文路径/文件名导致的编译错误四、用华为云码道(CodeArts)生成「中秋快乐」彩色文字4.1 需… · 2026/9/26 5:25:48
SSM后端+微信小程序:社区垃圾回收管理系统全栈实战教程 简介:一套基于微信小程序的社区垃圾回收管理系统SSM后端毕业设计源码案例,面向计算机专业毕业生、课程设计学习者及微信小程序/后端开发爱好者。系统涵盖用户管理、垃圾回收请求提交、垃圾分类指导、任务分配、进度跟踪与数据统计等核心功能,… · 2026/9/26 5:25:48
SSM+微信小程序社区养老服务系统:环境搭建、业务走读与避坑指南 简介:基于微信小程序与SSM后端的高分毕业设计完整源码包可用于毕业设计、课程设计及期末大作业,面向计算机专业毕业生和需要项目实战练习的学习者。项目以社区养老服务为业务场景,围绕护理预约、健康管理、日常生活照料、文化娱乐活动等模块展… · 2026/9/26 5:25:48
数据库课后习题答案别硬背:当测试用例集刷,效率翻倍 简介:万常选版《数据库原理与设计》课后习题答案资源,覆盖第2至6章及第9章,适合正在学习关系模型、数据库建模、关系数据理论与模式求精的本科生、自学者作为复习与自测材料。压缩包共7个文件,含3个doc参考答案、2个sql示例脚本、… · 2026/9/26 0:00:21
OpenClaw 替代品?Hermes Agent 踩坑实录:macOS 飞书接入 TaoToken 配置 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 0:00:40
向下兼容与向上兼容:接口设计中的兼容性策略与工程实践 一次版本升级事故,是很多团队绕不过去的坎。线上环境里,服务端明明已经上线了新版接口,老的移动端还在照着旧文档传参数。请求一到网关,校验直接拒绝,用户操作失败,客服群炸了锅,开发群里开始互… · 2026/9/26 0:00:46