拓冰建站拓冰建站
首页 / 资讯中心 / 正文

从VSCode扩展到Electron桌面打字游戏:架构迁移与打包实战

1. 为什么要把一个跑得好好的 VSCode 扩展改造成独立 Electron 应用先说背景。我一开始做的打字练习工具其实是个 VSCode 扩展名叫 TypingForge随便起的后来也没用在正式版本里。它能在编辑器里开一个 Webview 面板显示英文段落然后捕获键盘输入来做实时测速。说实话在 VSCode 里跑得不错命令面板输个TypingForge: Start Session就能拉起来练习截图发朋友圈也还挺唬人。但问题就出在VSCode 扩展这个身份上。因为 VSCode 本身是一个宿主应用扩展运行在它的进程模型和 UI 框架里有几个根深蒂固的限制扩展没有自己的独立窗口Webview 面板始终嵌在 VSCode 的标签页体系中用户一开别的文件练习就会被挤到一边。键盘事件捕获受 VSCode 编辑器的全局快捷键拦截比如CtrlShiftP、CtrlB这种组合键你根本没法在自己的输入框里优先拦截下来做打字判定。Webview 的 CSP内容安全策略有一套默认配置不能随便放开很多想用的 API 和远程资源加载方案都受限制。打包分发时要走 VSCode 插件市场或 VSIX 包用户得先装了 VSCode 才能用对只想练个打字的人来说太重了。后来有一个机械键盘群的群友在群里问我你这工具能不能做成一个独立软件我不想为了练打字打开编辑器我想双击图标就打开一个干净的练习窗口。这一句话点醒了我。既然核心逻辑已经写完了UI 也是基于 Web 技术Vue 3 Vite写的那最顺理成章的路子就是把它迁移到 Electron 上做一次架构改造。这就是这篇博文要讲的完整过程如何把一个面向 VSCode 宿主的扩展架构改造成一个独立的 Electron Vue 3 桌面打字游戏应用。整个过程涉及主进程/渲染进程的职责拆解、IPC 通信协议设计、键盘事件捕获的迁移、打包配置的调整以及在 Linux 桌面环境包括国产桌面系统上的分发经验。如果你是第一次做 Electron 项目或者你手上正好有一个Web 应用想打包成桌面软件的诉求这篇文章里的思路和踩坑记录应该能帮你省掉至少两天的调研时间。2. 架构改造的整体设计从 VSCode API 到 Electron 主进程的职责迁移2.1 扩展生命周期和 Electron 生命周期的映射关系VSCode 扩展的入口文件要导出一个activate函数VSCode 在扩展被激活时调用它返回一个包含deactivate函数的对象用于清理资源。Electron 则完全不同它的入口是一个主进程脚本通过app.whenReady()来启动应用监听window-all-closed、activate等生命周期事件。我当时做的第一个改造动作就是画一张对应关系的表把所有 VSCode API 的调用点找出来替换成 Electron 的等价物VSCode API承担职责Electron 替换方案vscode.commands.registerCommand注册命令触发逻辑IPC 通道 菜单项 全局快捷键vscode.window.createWebviewPanel承载 UI 界面new BrowserWindow({ webPreferences })vscode.window.showInformationMessage弹出提示dialog.showMessageBox或渲染进程自绘 Toastvscode.workspace.getConfiguration读写配置electron-store 或 JSON 文件读写vscode.window.setStatusBarMessage显示状态信息渲染进程内的状态栏组件context.subscriptions.push资源清理app.on(before-quit)里统一释放这个映射表是改造的骨架。我建议你在做类似迁移时先把原扩展里所有vscode.开头的调用全部列出来逐个确认在 Electron 里的替代方案。这个步骤不能省因为 VSCode API 数量庞大有些冷门的 API 比如vscode.languages可能根本不需要迁移但像vscode.Uri.file、vscode.Disposable这种你会在不知不觉中用到必须逐个处理。2.2 Vue 3 项目如何嵌入生产环境 loadFile 与开发环境 loadURL 的切换接下来是渲染进程的接入。我在原来的 VSCode 扩展里已经写好了一个完整的 Vue 3 打字游戏界面里面包含计时器组件、键盘按键高亮组件、成绩图表组件。迁移到 Electron 后这个 Vue 项目本身不需要动核心业务逻辑需要改的是如何被加载。Electron 的 BrowserWindow 加载页面有两种方式开发环境用loadURL(http://localhost:5173)Vite 的默认端口生产环境用loadFile(dist/index.html)。这里有两个关键点第一loadFile加载的是相对路径必须用path.join(__dirname, ../dist/index.html)这种方式来定位否则打包后文件路径会找不到。我见过很多新手直接用loadFile(dist/index.html)开发时没问题一打包就白屏就是因为 Electron 打包后__dirname指向的是app.asar内部相对路径的基准完全不同。第二Vite 的base配置必须改成./。Vite 默认base是/也就是产出的index.html里引用的 JS/CSS 路径是/assets/index-xxx.js这在 web 服务器下没问题但 Electron 用file://协议加载时/assets/...会指向磁盘根目录直接 404。把base: ./设置好后所有资源路径都变成相对路径才能正常加载。我最终的 vite.config.ts 核心配置长这样import { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ base: ./, plugins: [vue()], server: { port: 5173, strictPort: true }, build: { outDir: dist, assetsDir: assets, sourcemap: false } })然后主进程里做环境判断import { app, BrowserWindow } from electron import path from node:path function createMainWindow() { const win new BrowserWindow({ width: 1024, height: 720, webPreferences: { preload: path.join(__dirname, preload.js), contextIsolation: true, nodeIntegration: false } }) if (process.env.VITE_DEV_SERVER_URL) { win.loadURL(process.env.VITE_DEV_SERVER_URL) } else { win.loadFile(path.join(__dirname, ../dist/index.html)) } }contextIsolation: true和nodeIntegration: false这两个配置是安全底线千万不要为了省事改成nodeIntegration: true。特别是打字游戏这种需要接收键盘输入的软件如果渲染进程里某个第三方依赖被污染开启了 Node 集成就等于把整个系统权限暴露给了网络内容。用 preload 脚本暴露最小化的 API 才是正确姿势后面会详细说。2.3 IPC 通信协议设计把命令调用改造成统一的消息通道在 VSCode 扩展里前端界面通过acquireVsCodeApi()拿到vscode对象然后调用vscode.postMessage({ type: xxx, payload: ... })来和扩展通信。Electron 里虽然有ipcRenderer和ipcMain但最佳实践是不要直接在每个组件里乱发消息而是先在 preload 脚本里把通信接口封装好。我当时设计了一套类似于事件总线的通信协议。规则很简单所有消息都带type和payload两个字段主进程和渲染进程各自维护一个消息分发表。这样后期的维护性要好很多不至于满项目都是ipcRenderer.send(save-score, data)这种裸调用。preload 脚本暴露的 API 如下// preload.ts import { contextBridge, ipcRenderer } from electron contextBridge.exposeInMainWorld(typingAPI, { // 单次请求-响应 getGameConfig: () ipcRenderer.invoke(game:getConfig), saveGameResult: (result: GameResult) ipcRenderer.invoke(game:saveResult), getScores: () ipcRenderer.invoke(game:getScores), // 事件监听 onTimerTick: (callback: (time: number) void) { ipcRenderer.on(game:timerTick, (_event, time) callback(time)) return () ipcRenderer.removeListener(game:timerTick, callback) }, // 窗口控制 windowMinimize: () ipcRenderer.send(window:minimize), windowClose: () ipcRenderer.send(window:close) })为什么用contextBridge因为它能在隔离的上下文之间建立桥接渲染进程拿到的typingAPI是一个经过校验的对象不能直接访问底层的ipcRenderer或者Node的process。这样即使渲染进程被注入恶意代码攻击面也被限制住了。关于 IPC 还有一个性能问题值得提一下主进程和渲染进程的消息传递是有序列化开销的频繁的消息会在进程间复制数据。打字游戏里如果每一帧都往主进程发送当前光标位置之类的数据大概率会造成 UI 卡顿。我的做法是渲染进程自己维护所有实时状态当前字符索引、计时器、WPM只在游戏结束时一次性把结果上报给主进程持久化。像定时器这种看似应该在主进程做的事其实也让渲染进程自己用requestAnimationFrame或setInterval来做避免每秒钟 60 次的 IPC 洪峰。3. 打字游戏核心模块的实现与踩坑实录3.1 键盘输入捕获为什么不能再依赖 VSCode 的编辑器焦点在 VSCode 扩展时代键盘输入是依赖编辑器获得焦点后输入字符就会进入编辑器这个机制我在 Webview 的message事件里拿到文本变更来做对比。到了独立应用里这个逻辑完全不适用了。Electron 的 BrowserWindow 本身不提供用户打了哪个字符这种高层抽象你只有一个底层事件keydown/keypress/keyup。我最终实现是全局监听keydown关键代码如下window.addEventListener(keydown, (event: KeyboardEvent) { // 排除组合键 if (event.ctrlKey || event.metaKey || event.altKey) { return } // 排除功能键 if (event.key.startsWith(F) event.key.length 2) { return } const expected currentWord[currentIndex] if (event.key expected) { handleCorrectInput(event.key) } else if (event.key.length 1) { handleWrongInput(event.key) } })这里有一个很重要的坑event.key和event.code的区别。如果你用event.code比如KeyA它会忽略键盘布局无论用户用的是 QWERTY 还是 DvorakKeyA都是同一个位置。而打字测速场景要测的是用户能不能打出正确的字符所以必须用event.key它返回的是用户实际输入的字符。如果你期望的文本里有大小写混排还需要额外处理shift键的情况event.key已经区分了大小写比如Shifta会返回A所以判断时直接用expected event.key就行不需要自己维护shift状态。3.2 文本渲染与光标定位巧用绝对定位模拟逐字符高亮打字游戏界面的核心是显示一段带空格的英文文本用户每打对一个字符这个词高亮变成灰色当前待输入字符用一个竖线光标指示。最直观的渲染方式是整段文本用一个p标签通过v-html输出带span包裹的高亮 HTML。但v-html有 XSS 风险虽然文本来自本地词库不是用户输入仍然不够优雅。我换了一种方案用一个相对定位的容器把原文切成一个个字符span每个字符用position: absolute或直接inline-block排列根据状态动态切换 class。但切太碎对于性能有压力。一段英文大概 300~500 个字符500 个 span 在 Vue 里渲染没有压力但如果每秒都在更新 class会产生大量 diff 计算。优化方式是只在索引变化时更新三个 class已完成灰色、当前高亮 光标、未开始默认色。这样最多只有 3 个 span 的 class 会变化其他 span 的 class 不变Vue 的 patch 不会去动它们。光标位置其实不用真的放一个span并定位我用 CSS 的border-left和一点负 margin 就拼出来了.char.current { background: rgba(255, 200, 0, 0.15); box-shadow: -1px 0 0 0 #ffc800; }这个小技巧做出来的效果接近真实编辑器的竖线光标而且完全不依赖定时器去闪烁动画。3.3 实时 WPM 计算滑动窗口比整体平均更直观打字游戏除了最基本的正确率最核心的反馈是实时 WPM每分钟单词数。最简单的算法是打完所有字符后总字数除以总分钟数但这样游戏进行到一半时用户看不到实时变化挫败感很强需要改成滑动窗口计算。我的做法是维护一个长度为 20 的输入事件队列每个事件记录两个字段time时间戳和length本次输入的字符数。计算 WPM 时只统计过去 5 秒内的事件公式为WPM (最后5秒内正确输入的字符数 / 5) * 60 / 5这里的/5是把字符数转换为单词数约定 5 个字符等于 1 个单词* 60是转换成每小时的量。用 5 秒窗口的好处是既能反映用户当前节奏又不会被单次大的停顿完全归零。如果窗口太短比如 1 秒WPM 会剧烈抖动太长比如整个游戏时长又失去了实时性。另外统计正确率时我额外记录了一个错误尝试次数。用户在同一个字符上连续敲错三次只算一次错误字符因为连续按同一个按键可能是输入法卡顿或者键盘抖动不应该被重复惩罚。这个细节在真实测速软件里也有体现很多正规的 typing test 工具就是这么判定的。3.4 焦点管理独立应用里最常见的键盘失效问题打字游戏最怕的情况是窗口明明是打开的但敲键盘没反应。在 VSCode 扩展里Webview 面板自带焦点管理不大会遇到这个问题。迁移到 Electron 后用户只要点了一下菜单栏、或者调出开发者工具焦点就会从 body 上移走keydown事件就捕获不到了。解决方案有两个层面。第一在body上设置tabindex0这样 body 本身可以被聚焦。第二监听窗口的blur事件一旦失去焦点就自动重新聚焦并且在点击任何非交互元素时都preventDefault避免按钮等元素抢走键盘焦点。// 渲染进程 document.body.addEventListener(click, (event) { if ((event.target as HTMLElement).tagName ! INPUT (event.target as HTMLElement).tagName ! TEXTAREA) { document.body.focus() } }) window.addEventListener(blur, () { document.body.focus() })注意不要对全局keydown直接调用preventDefault()。如果你阻止了默认行为用户就没办法用快捷键切换别的窗口这在打字游戏里尤其危险——用户想按AltTab切出去看资料结果被游戏拦截了体验极差。我最后只对event.key.length 1的普通字符调用preventDefault()组合键一律放行。4. 桌面端特有能力的接入菜单、托盘、系统语言和窗口管理4.1 菜单栏定制从模板生成到动态更新VSCode 扩展时代菜单是别人家的东西你只能通过contributes.commands添加命令罢了。到了 Electron 里菜单变成了自己的领地既可以配置一套符合桌面应用习惯的菜单栏也可以通过右键菜单扩展功能。我用了Menu.buildFromTemplate()来做应用菜单核心结构是文件、练习、模式三个一级菜单。这里有一个值得提的点菜单项的click回调里写具体业务逻辑会让主进程代码变得很臃肿正确做法是菜单项只负责往渲染进程发消息const menuTemplate: Electron.MenuItemConstructorOptions[] [ { label: 文件, submenu: [ { label: 开始新练习, accelerator: CmdOrCtrlN, click: () { mainWindow?.webContents.send(menu:newGame) } }, { type: separator }, { role: quit, label: 退出 } ] } ]菜单还有一个容易忽略的细节accelerator用CmdOrCtrl而不是写死Ctrl这样在 macOS 上会自动变成 Command 键。做跨平台桌面应用的快捷键声明这是一个基本素养。另外如果应用在 Linux 上跑菜单栏默认是显示在窗口内的样式可能比较丑。我后来在 Linux 打包时选择了隐藏默认菜单栏autoHideMenuBar: true并将核心功能做成右键菜单和快捷键更符合 Linux 桌面的使用习惯。4.2 系统托盘窗口最小化后仍能快速进入练习打字这个场景决定了用户常常会把窗口最小化和其他工作共存。所以我在系统托盘放了一个图标右键可以快速开始新练习或退出应用。托盘功能实现起来不复杂就是TrayMenu的组合但有一个从真实使用中得来的经验应用退出时一定要tray.destroy()否则 Linux 桌面上会出现幽灵托盘图标有些桌面环境得重启 session 才能清掉。托盘菜单的一项核心功能是开始练习和暂停练习。这两种状态需要主进程告诉渲染进程切换模式但当时渲染进程有可能正在游戏中途状态乱掉。最后我加了一层确认逻辑主进程发tray:togglePause渲染进程回复当前状态主进程判断后再更新托盘菜单项文案。这种双向通信的设计比主进程单方面切换状态要稳妥得多。4.3 系统语言检测自动切换练习词库的语言在标题相关的热搜词里有一个 electron 获取系统语言 排得很靠前。这说明很多做桌面应用的人都会遇到需要根据系统语言做本地化的需求。Electron 里获取系统语言很简单主进程用app.getLocale()返回的是一个类似zh-CN、en-US的字符串如果你想拿到更底层的语言代码比如zh、en可以拆前缀function getSystemLanguage(): string { const locale app.getLocale() return locale.split(-)[0] }但打字游戏这个场景下语言检测不能只做一次。因为用户可能在系统设置里切换语言后应用还停留在旧语言的词库里。我的处理是应用启动时检测一次同时监听系统语言的变更事件。Electron 没有直接暴露系统语言变化事件但可以通过powerMonitor的resume事件用户从睡眠唤醒系统时语言设置可能已变更来重新检测实在检测不到变化就保持原样。这里还要多说一句app.getLocale()有个容易踩的坑——如果你在应用启动早期比如ready事件之前调用它拿到的可能是 Electron 的默认值en-US而不是系统真实的语言。务必在app.whenReady()之后再获取。4.4 窗口尺寸自适应与小屏设备适配打字练习的文本区域需要比较大的视觉范围同时键盘高亮图又需要占据屏幕下方位置。我最初把窗口写死成 1024x720后来发现不少用户的笔记本是 1366x768 的屏幕加上系统任务栏和浏览器窗口标题栏实际可视高度只有不到 700px游戏界面会被挤得换行。最终的方案是窗口高度设置成可以缩放的并且在渲染进程里用matchMedia监听视口高度变化动态调整练习文本的字号const mq window.matchMedia((max-height: 700px)) mq.addEventListener(change, (e) { fontSize.value e.matches ? 18 : 24 })还有一个小细节窗口改变大小后打字区域的滚动位置需要重新计算保证当前输入行始终在可视区域中间。这个我用了一个scrollIntoView({ block: center })实现但注意要给容器设置scroll-smooth或自己实现节流不然滚动会顿挫。5. 数据持久化成绩记录与用户配置的存放方式5.1 为什么不用 localStorage文件系统的选择逻辑VSCode 扩展时代Webview 里的数据我直接存在localStorage里。但到了 Electron虽然localStorage仍然可用它本质上是 Chromium 的本地存储但有一个致命问题应用被卸载重装后localStorage数据目录会被一并清理。而且不同窗口的localStorage默认隔离如果以后想开多个练习窗口数据没法共享。我的选择是所有需要持久化的数据都不经过localStorage而是写入用户数据目录下的 JSON 文件。Electron 提供app.getPath(userData)来获取系统分配给当前应用的配置目录Windows 上是%APPDATA%\应用名macOS 上是~/Library/Application Support/应用名Linux 上是~/.config/应用名。在这个目录下写文件既不会被应用更新覆盖也能被系统备份机制纳入备份范围。import fs from node:fs import path from node:path const configDir app.getPath(userData) const scoresFile path.join(configDir, scores.json) function loadScores(): ScoreRecord[] { try { const raw fs.readFileSync(scoresFile, utf-8) return JSON.parse(raw) } catch { return [] } } function saveScores(scores: ScoreRecord[]) { fs.writeFileSync(scoresFile, JSON.stringify(scores, null, 2), utf-8) }5.2 写文件时的原子性避免断电/崩溃导致 JSON 损坏直接writeFileSync有一个隐患如果写入过程中进程崩溃文件会处于半写状态JSON 解析直接报错。更严重的是如果用户磁盘满了写入会抛异常导致游戏直接崩溃。解决方式是用先写临时文件再原子重命名的方式function atomicWrite(filePath: string, data: string) { const tmpPath ${filePath}.tmp fs.writeFileSync(tmpPath, data, utf-8) fs.renameSync(tmpPath, filePath) }rename在同一个文件系统下是原子操作不会出现文件内容被截断的情况。这个技巧在服务端开发中是标配但在 Electron 渲染进程的博客示例里很少提到。我用这个方案后成绩损坏的情况再也没有出现过。5.3 词库管理内置自带与用户导入的双轨制打字游戏的内容来源有两种内置英文段落和用户自定义词库。内置词库直接打进dist目录的 JSON 文件里用fetch相对路径加载因为base: ./所以相对路径没问题。用户自定义词库则放在userData/words.json主进程提供导入和导出功能。这里有一个值得一提的错误开发时fetch(words.json)能正常加载但打包成 asar 后fetch仍然可以访问 asar 内的文件这一点 Electron 做了处理。但你要注意文件路径必须用./相对当前页面不能以/开头。我最初写的是fetch(/words.json)开发环境正常因为 dev server 会把/映射到项目根目录打包后白屏排查了半小时才意识到是路径问题。6. 打包分发从开发机到用户桌面的最后一公里6.1 electron-builder 基础配置与三类产物打包我用的是 electron-builder配置写在electron-builder.yml里。appId、productName、directories.output是最基础的三项。这里想特别强调files字段——如果你不设置它electron-builder 会把整个项目目录打包进去包括node_modules里的开发依赖产物体积直接翻几倍。appId: com.typingforge.app productName: TypingForge directories: output: release files: - dist/**/* - electron/** - package.json asar: trueasar: true会把应用代码打包成一个 asar 归档文件既保护源码不被轻易查看也加快了文件读取速度。但如果你有资源文件需要通过文件系统 API 直接访问像词库 JSON必须在asarUnpack里排除否则打包后路径会变成 asar 内部路径Node 的fs模块默认无法读取 asar 内的文件路径。6.2 三平台打包的差异化处理Electron 的优势是跨平台但打包配置并不能一套走天下。Windows 上最常规的是 NSIS 安装包需要注意nsis.oneClick要设为false这样才会出现选择安装目录的界面用户习惯性的要能看到自定义安装路径的选项。另外 win 平台下的图标格式必须是.icomacOS 是.icnsLinux 是pngelectron-builder 不会自动转换格式如果你只提供了一个.png图标Windows 打包时会直接用默认 Electron 图标很掉价。macOS 上如果有开发者证书可以配置hardenedRuntime: true和gatekeeperAssess: false。但如果没有证书产物在别的 Mac 上运行时会提示无法验证开发者。这个问题说实话到现在也没有零成本解法只能老老实实签名。如果只是给自己用可以通过右键打开的方式绕过 Gatekeeper。Linux 上最核心的坑是运行时依赖。electron-builder 默认产出的AppImage在很多老旧的发行版上会缺少libnss3、libatk等依赖用户跑起来当场报错。后来我改成同时发deb包和tar.gz并在 README 里明确标注了需要安装哪些基础依赖。这些都是书到用时方恨少的典型问题建议你在打包 Linux 版本时去一台干净的无图形环境虚拟机里实测启动。6.3 Linux 桌面系统的分发注意点含国产桌面环境在 Linux 分发上有一个绕不开的话题国产桌面系统。不少政务、教育场景的电脑预装的是基于 Linux 内核的桌面环境这类系统对 Electron 应用的兼容性总体是好的因为 Chromium 内核的适配面很广但有几个细节必须提前处理第一字体渲染。很多 Linux 桌面系统默认没有微软雅黑、苹方这类中文字体应用内中英文混排时中文会回退到点阵字体观感很差。打包时直接把中用到的字体比如思源黑体的部分字重打进extraResources并在渲染进程的 CSS 里用font-face声明。不要指望用户会自己装字体。第二窗口管理器的兼容性。有些轻量级桌面环境没有全局菜单栏autoHideMenuBar: true能让窗口更干净。同时托盘支持参差不齐我实测在某些桌面环境上托盘图标不显示所以核心功能不能只放在托盘菜单里窗口内的按钮必须有同样的入口。第三GPU 加速问题。部分 Linux 设备尤其是虚拟机和无独显的办公机的 GPU 驱动不完整Chromium 默认开启的 GPU 加速可能导致白屏或闪烁。遇到这类情况可以在启动参数里加app.disableHardwareAcceleration()或者在打包时提供一个兼容模式的启动参数比如--disable-gpu。这不是什么高深技巧但对用户体验的提升非常明显。// 主进程入口检测到 --disable-gpu 参数时禁用硬件加速 if (process.argv.includes(--disable-gpu)) { app.disableHardwareAcceleration() }6.4 体积优化为什么初始包能砍掉 40%Electron 应用被人诟病最多的就是体积大。一个空 Electron 应用打包后就有 80MB 起步加上 Vue 3 和依赖轻松破 120MB。我做了两件事把安装包体积从 128MB 压到了 76MB一是electron-builder的compression: maximum选项这个直接决定最终安装包使用的压缩算法默认是normal改成maximum后能压缩 20% 左右代价是打包时间变长。二是检查files里是否混入了不需要的文件。我最初打包时把node_modules里的typescript、eslint这些纯开发依赖也打进去了占了 20 多 MB。用electron-builder --dir可以先打出一个未压缩的目录然后手动检查里面的node_modules把不必要的依赖从files排除掉或者干脆在package.json的dependencies里只保留运行时依赖其他全部放devDependencies。还有一个对打字游戏场景特别有用的优化课件文本里不需要用到任何图片资源所以 Vue 项目里所有静态图片都删掉或转到 CDN打包后的dist目录只剩 JS 和 CSS体积非常干净。7. 常见问题与排查技巧实录从开始改造到真正可以日常使用我踩了不少坑这里按症状-原因-解决的格式整理一份速查表都是实测经验不是从文档里抄的。症状原因解决方案打包后白屏打开开发者工具有 404Vite 的base没有改成./资源路径以/开头修改vite.config.ts的base: ./重新构建loadFile路径找不到使用相对路径没有基于__dirname用path.join(__dirname, ../dist/index.html)键盘事件捕获不到焦点不在 body 上被按钮/菜单抢走了tabindex0blur时重新聚焦打字后界面卡顿每个字符都触发 Vue 响应式更新只更新currentIndex用计算属性/预渲染处理样式成绩数据偶尔丢失直接写文件时进程崩溃导致 JSON 损坏改用临时文件 rename 的原子写方案Linux 上托盘图标不显示桌面环境不支持 StatusNotifier核心功能在窗口内提供按钮入口托盘只做增强窗口内输入法弹不出来nodeIntegration相关配置影响了 IME确认contextIsolation仍为 true使用 preload 暴露 API打包的 exe 被杀毒软件误报未签名 exe asar 打包有时会触发启发式误报尽量签名官网发布时设置白名单反馈入口游戏窗口无法最小化到托盘窗口关闭时应用直接退出window-all-closed事件里判断平台macOS 不quit其他平台看托盘状态再补充一个比较隐蔽的问题如果用户在渲染进程中调用了alert()、confirm()在 Electron 里弹的是 Chromium 原生的阻塞对话框样式和操作系统不搭而且在 Linux 某些环境下会直接卡死。我后来把所有确认类弹窗都改成了自绘的 Vue 弹窗组件主进程的dialogAPI 也只在文件保存路径选择这种真需要系统对话框的场景下使用。还有一个性能调优经验打字游戏里有大量的样式切换如果currentIndex变化时把整段文本的 class 都重算一遍会产生几毫秒的卡顿打字时手速一上来就明显了。我最终的方案是用一个Map维护已经处理过的字符索引只有currentIndex前后各一个字符需要重新计算 class其他字符完全不动。配合 Vue 的v-once指令未开始的字符部分在渲染一次后就不再参与 diff。关于开发调试强烈建议在main.ts里这样区分环境if (process.env.NODE_ENV development) { // 打开开发者工具 win.webContents.openDevTools({ mode: detach }) }开发时把开发者工具单独分离成窗口可以很方便地同时观察主进程和渲染进程的日志。等你做完所有功能一定要记得把这个自动打开的代码删掉否则用户一打开应用就是一个开发者工具窗口体验很难看。最后分享一个我自己实践中的体会从 VSCode 扩展迁移到 Electron 绝对不只是把 UI 换个壳这么简单。VSCode 扩展给你提供了一整套成熟的 API 和 UI 规范你不需要操心窗口、菜单、存储、安全模型而 Electron 把这些决定权全部交还给开发者同时也把所有责任压到了你身上。做架构改造时先想清楚职责边界主进程管什么、渲染进程管什么、定义好 IPC 协议、规划好数据存放方式比埋头写代码重要得多。希望这篇实战记录能帮你少走一些弯路。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门