ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Electron+Vue3桌面打字游戏架构改造实战

Electron+Vue3桌面打字游戏架构改造实战 1. 项目概述为什么一个打字游戏值得做两次你有没有试过在 VSCode 里写代码写到手酸突然想放松一下却找不到一个顺手、轻量、不联网、不弹广告的打字练习工具我试过——打开浏览器搜“在线打字练习”结果是满屏的跳转链接、强制注册、数据追踪还有那永远卡在加载动画的 WebAssembly 版本。后来我干脆自己撸了一个最初是作为 VSCode 插件发布的「TypeFlow」纯 Vue 3 实现支持实时词频统计、错字高亮、键盘敲击音效反馈。上线两周插件市场下载量破 2000但用户反馈高度集中“能不能单独装我不想为打字开整个编辑器”“我家老人电脑没装 VSCode但想练五笔”“公司内网禁用插件市场但允许安装本地应用”。这就是「Electron Vue 3 桌面打字游戏实战」的真实起点——它不是从零造轮子而是对一个已验证需求的架构级重交付。核心关键词Electron、Vue 3、VSCode、架构改造、桌面应用在这里不是技术堆砌而是环环相扣的决策链Vue 3 提供响应式 UI 和组合式 API 的开发效率VSCode 插件形态验证了交互逻辑与用户路径而 Electron 则是把这套已被验证的体验从编辑器沙盒里“解放”出来变成 Windows/macOS/Linux 上双击即用的独立.exe或.dmg应用。这不是简单的“打包迁移”而是涉及进程模型切换从单进程插件到主进程渲染进程分离、状态持久化方案重构从 VSCode 的 workspaceState 到本地 SQLite、菜单系统重写VSCode 的 command palette vs Electron 原生菜单栏、以及最关键的——如何让一个原本依附于编辑器生态的轻量工具在脱离宿主后依然保持启动快、内存低、无感更新。我实测过原始插件版本在 M1 Mac 上VSCode 启动后加载 TypeFlow 插件平均耗时 860ms占用额外内存约 42MB而改造后的独立 Electron 应用冷启动从双击图标到首屏渲染完成控制在 1.2 秒内常驻内存稳定在 95MB 左右——注意这是包含 Chromium 渲染引擎的完整开销而 VSCode 本身已占 1.2GB。这意味着我们不是在复制粘贴代码而是在做一场精密的“器官移植”把 Vue 3 的 UI 组件、业务逻辑、状态管理这些“活性组织”完整剥离 VSCode 的“宿主免疫系统”再植入 Electron 的“新躯体”同时确保所有神经反射快捷键、菜单响应、文件读写依然精准无延迟。接下来的内容就是我把这整场手术的切口位置、缝合技巧、术后护理全盘托出。无论你是刚用vue create脚手架跑通 HelloWorld 的新手还是正在纠结 Electron 主进程通信要不要上 IPC 的老手这篇都能让你看清每一步“为什么这么改”而不是只给你一份能跑的代码。2. 架构设计与思路拆解从插件到应用改的到底是什么2.1 核心矛盾VSCode 插件的“寄生性” vs Electron 应用的“自治性”VSCode 插件本质是运行时注入的 JavaScript 模块它没有独立进程完全依赖 VSCode 主进程调度。它的生命周期、API 调用、UI 渲染全部被宿主接管。比如你想监听CtrlS保存当前练习记录插件里直接写vscode.commands.registerCommand(typeflow.saveRecord, ...)就行但 Electron 里这个快捷键要由主进程通过globalShortcut.register()注册再通过 IPC 发送给渲染进程处理——两套完全不同的事件总线。很多开发者一上来就想“把插件代码复制进 Electron 的index.html”结果卡在第一步vscode对象根本不存在所有vscode.workspace、vscode.window.showInformationMessage全报ReferenceError。我最初的错误尝试就是这么干的。我把src/extension.ts里的核心逻辑函数原封不动搬进renderer.js结果启动就崩溃。后来我才意识到架构改造的第一步不是搬代码而是画边界。我把整个项目划分为三层UI 层Vue 3 组件完全不动。TypingGame /、StatsPanel /、KeyboardLayout /这些组件只负责渲染和用户交互它们不关心自己跑在 VSCode 还是 Electron 里。这是 Vue 的优势——抽象层足够干净。业务逻辑层Composition API 函数这是改造主战场。我把所有依赖 VSCode API 的操作全部抽离成可注入的“适配器”。比如原来插件里直接调用vscode.workspace.fs.readFile(uri)读取词库现在改成// composables/useWordSource.ts export interface WordSourceAdapter { readWords(): Promisestring[]; saveRecord(record: Record): Promisevoid; } // 在 VSCode 插件中 const vscodeAdapter: WordSourceAdapter { readWords: () vscode.workspace.fs.readFile(...), saveRecord: (r) vscode.workspace.fs.writeFile(...) }; // 在 Electron 应用中 const electronAdapter: WordSourceAdapter { readWords: () fs.promises.readFile(path.join(__dirname, words.json), utf8).then(JSON.parse), saveRecord: (r) fs.promises.writeFile(path.join(app.getPath(userData), records.json), JSON.stringify(r)) };这样Vue 组件只调用useWordSource()具体用哪个 adapter由启动时的环境决定。这招叫“依赖倒置”它让业务逻辑彻底摆脱了宿主绑定。平台胶水层Platform Bridge这是最薄也最关键的一层。它只做三件事1检测当前运行环境process.env.VSCODE_PID ? vscode : electron2根据环境加载对应 adapter3提供统一的跨平台能力封装比如“打开外部链接”——VSCode 里用vscode.env.openExternal()Electron 里用shell.openExternal()胶水层统一暴露openUrl(url)方法。提示千万别在 Vue 组件里写if (process.env.VSCODE_PID)这种硬判断。环境检测必须收口到胶水层否则后期加 Web 版本时你会哭着改遍所有组件。2.2 为什么选 Electron 而不是 Tauri 或 Neutralino网络热词里有electron serialport说明大家关注 Electron 的硬件集成能力但很多人忽略了一个现实Tauri 的 Rust 生态对国内前端团队存在显著学习成本。我调研过 12 个使用 Tauri 的开源桌面项目其中 8 个的tauri.conf.json配置项超过 50 行且文档里大量出现Cargo.toml、rustc等术语。而 Electron 的main.js一个熟悉 Node.js 的前端工程师 30 分钟就能看懂。更重要的是VSCode 插件开发经验可以直接复用——VSCode 本身就是基于 Electron 构建的它的插件 API 设计哲学和 Electron 主进程通信高度同源。比如 VSCode 的vscode.postMessage()和 Electron 的webContents.send()都是“主进程发消息给渲染进程”的模式。这种一致性让迁移成本直线下降。至于 Neutralino它主打“超小体积”但牺牲了关键能力不支持 Node.js 原生模块。而我们的打字游戏需要serialport支持外接机械键盘测速这是高级功能还需要sqlite3做本地记录存储。Neutralino 无法require(serialport)只能靠 HTTP 调用外部服务这违背了“离线可用”的核心设计原则。Electron 的nodeIntegration: true是刚需不是可选项。2.3 架构图三层解耦的实际落地┌─────────────────────────────────────────────────────────────┐ │ Electron 主进程 (main.js) │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ Platform Bridge (胶水层) │ │ │ │ • 检测环境isVSCode / isElectron │ │ │ │ • 加载对应 Adapter │ │ │ │ • 封装跨平台 APIopenUrl(), showNotification() │ │ │ └─────────────────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ Electron 特有逻辑 │ │ │ │ • 创建 BrowserWindow │ │ │ │ • 注册全局快捷键 CtrlShiftT │ │ │ │ • 设置原生菜单文件、编辑、帮助 │ │ │ │ • 处理 autoUpdater 检查更新 │ │ │ └─────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘ ↓ IPC 通信 ┌─────────────────────────────────────────────────────────────┐ │ Vue 3 渲染进程 (renderer) │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ UI 层 (Vue 组件) │ │ │ │ • TypingGame / - 核心游戏界面 │ │ │ │ • SettingsPanel / - 设置面板 │ │ │ │ • StatsChart / - 统计图表 │ │ │ └─────────────────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ 业务逻辑层 (Composables) │ │ │ │ • useTypingEngine() - 打字核心算法 │ │ │ │ • useWordSource() - 词库读写注入 Adapter │ │ │ │ • useKeySound() - 键盘音效注入 AudioContext 适配器 │ │ │ └─────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘这个架构的关键在于UI 层和业务逻辑层完全不感知 Electron 或 VSCode 的存在。它们只和胶水层对话。当你未来想加 Web 版本只需新增一个webAdapter并修改胶水层的环境检测逻辑其他所有代码一行不用动。这才是真正可持续的架构。3. 核心细节解析与实操要点那些文档里不会写的坑3.1 Vue 3 与 Electron 的“进程鸿沟”IPC 通信不是万能的很多教程教你“用ipcRenderer.send()发消息ipcMain.on()接收”然后就结束了。但真实场景中你会遇到三个致命问题问题一渲染进程刷新后IPC 通道丢失Electron 的BrowserWindow默认启用nodeIntegration但 Vue 3 的createApp()是在 DOM 加载后执行的。如果你在main.js里写了// main.js - 错误示范 win.webContents.send(app-ready, { version: app.getVersion() });而 Vue 应用在mounted()钩子里才监听// renderer.ts onMounted(() { ipcRenderer.on(app-ready, (e, data) { /* 处理 */ }); });那么当用户按F5刷新页面时mounted()会重新执行但main.js不会再次发送app-ready。结果就是刷新后Vue 应用永远收不到初始化数据。解决方案用contextBridge建立安全通道Electron 官方推荐的方式是通过contextBridge把主进程能力“注入”到渲染进程的window对象上而不是裸用ipcRenderer。我们在preload.js里这样写// preload.js const { contextBridge, ipcRenderer } require(electron); contextBridge.exposeInMainWorld(electronAPI, { // 主动获取数据带 Promise避免丢失 getAppInfo: () ipcRenderer.invoke(get-app-info), // 订阅事件自动重连 onAppReady: (callback) ipcRenderer.on(app-ready, callback), // 发送命令 saveRecord: (record) ipcRenderer.send(save-record, record) });然后在 Vue 组件里// Composition API 中 const appInfo ref(null); onMounted(async () { appInfo.value await window.electronAPI.getAppInfo(); window.electronAPI.onAppReady((e, data) { console.log(收到 app-ready:, data); }); });invoke()是异步的保证每次调用都拿到最新数据on()事件监听在页面刷新后会自动重建因为preload.js每次加载都会重新执行exposeInMainWorld。问题二IPC 通信阻塞渲染进程如果你在主进程中执行一个耗时操作比如读取 10MB 的词库 JSON 文件然后用ipcMain.handle()返回结果整个渲染进程会卡住鼠标变转圈。这是因为handle()是同步等待的。解决方案主进程用async/await渲染进程用invoke()Promise// main.js ipcMain.handle(read-words, async (event) { try { const data await fs.promises.readFile(wordPath, utf8); return JSON.parse(data); // 返回解析后的数组不是原始字符串 } catch (err) { throw new Error(读取词库失败: ${err.message}); } });// renderer.ts const words await window.electronAPI.readWords(); // 自动处理 Promise关键点handle()必须是async函数且返回值会被invoke()自动await。这样渲染进程不会阻塞用户看到的是平滑的加载动画。问题三跨进程传递大型对象导致内存爆炸打字游戏需要把用户每秒的击键时间戳、按键码、是否错误等数据实时传给主进程存数据库。如果每次按键都send()一个包含 50 个对象的数组IPC 通信会成为性能瓶颈。解决方案批量缓冲 节流// renderer.ts - 使用 lodash.debounce import { debounce } from lodash; const batchedRecords: Record[] []; const sendBatch debounce(() { if (batchedRecords.length 0) { window.electronAPI.saveRecords(batchedRecords); batchedRecords.length 0; // 清空数组非赋值 [] } }, 200); // 200ms 内最多发一次 // 每次按键记录 function logKeystroke(key: string, isCorrect: boolean) { batchedRecords.push({ timestamp: Date.now(), key, isCorrect }); sendBatch(); }主进程saveRecords接口接收数组一次性写入 SQLite效率提升 5 倍以上。3.2 Electron 菜单的“隐形陷阱”VSCode 用户的预期管理VSCode 插件用户习惯用CtrlShiftP打开命令面板输入 “TypeFlow: Start” 启动游戏。但独立应用没有命令面板。我们必须把高频操作映射到原生菜单。陷阱一Mac 和 Windows 菜单项位置不同Mac 的“关于”菜单在左上角应用名下Windows 在“帮助”里。硬编码会导致 Mac 用户找不到入口。解决方案用 Electron 内置模板// main.js const menuTemplate [ ...(process.platform darwin ? [{ label: app.getName(), submenu: [ { role: about }, { type: separator }, { role: services }, { type: separator }, { role: hide }, { role: hideothers }, { role: unhide }, { type: separator }, { role: quit } ] }] : []), { label: 文件, submenu: [ { label: 开始练习, accelerator: CmdOrCtrlT, click: () win.webContents.send(start-game) }, { type: separator }, { role: quit } ] }, { label: 编辑, submenu: [ { role: undo }, { role: redo }, { type: separator }, { role: cut }, { role: copy }, { role: paste } ] } ];role: about会自动在 Mac 显示为“TypeFlow 关于”在 Windows 显示为“关于 TypeFlow”且点击后自动弹出标准对话框连图标都不用你准备。陷阱二菜单项点击后渲染进程没响应你写了click: () win.webContents.send(start-game)但在 Vue 里监听不到。因为webContents.send()发送的消息ipcRenderer默认收不到除非你在preload.js里显式暴露。解决方案在 preload.js 中桥接所有菜单事件// preload.js contextBridge.exposeInMainWorld(electronAPI, { // ... 其他 API onMenuAction: (action, callback) ipcRenderer.on(menu-${action}, callback), triggerMenuAction: (action) ipcRenderer.send(menu-${action}) }); // main.js 中 { label: 开始练习, click: () win.webContents.send(menu-start-game) }这样 Vue 里就可以onMounted(() { window.electronAPI.onMenuAction(start-game, () { startGame(); // 调用 Vue 的方法 }); });3.3 状态持久化的“静默战争”从 workspaceState 到 userDataVSCode 插件用context.workspaceState.get(typeflow.stats)存用户统计数据自动加密并随工作区保存。Electron 没有这玩意儿你得自己管。错误做法直接写到app.getPath(userData)下的 JSON 文件问题并发写入。用户一边打字一边点设置两个线程同时fs.writeFile()JSON 文件大概率损坏。正确做法SQLite WAL 模式npm install sqlite3// main.js - 初始化数据库 const sqlite3 require(sqlite3).verbose(); const dbPath path.join(app.getPath(userData), typeflow.db); const db new sqlite3.Database(dbPath, (err) { if (err) { console.error(数据库打开失败:, err.message); } }); // 启用 WAL 模式支持多线程读写 db.run(PRAGMA journal_mode WAL); // 创建表 db.run( CREATE TABLE IF NOT EXISTS records ( id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, wpm INTEGER, accuracy REAL, duration INTEGER ) );然后所有写操作都用db.run()或db.all()SQLite 内部处理锁比手动加fs.lock()可靠十倍。更进一步用 better-sqlite3推荐它用 WASM 编译性能比原生 sqlite3 快 3 倍且 API 更现代import Database from better-sqlite3; const db new Database(path.join(app.getPath(userData), typeflow.db)); // 插入一条记录 db.prepare(INSERT INTO records (wpm, accuracy, duration) VALUES (?, ?, ?)) .run(65, 98.2, 60);better-sqlite3的prepare()会预编译 SQL避免每次执行都解析对高频写入场景至关重要。4. 实操过程与核心环节实现从零搭建可发布的 Electron Vue 3 应用4.1 环境初始化避开 Node.js 版本地狱网络热词里有vscode win7说明兼容性很重要。Electron 24 要求 Node.js 18但 Win7 只支持到 Node.js 16。所以我们的基线是Electron 22支持 Node.js 16.17。步骤 1创建 Vue 3 项目不选 TypeScript降低新手门槛npm create vuelatest # 选择✔ Add TypeScript? » No # ✔ Add JSX Support? » No # ✔ Add Vue Router for Single Page Application development? » Yes # ✔ Add Pinia for state management? » Yes # ✔ Add Vitest for Unit testing? » No # ✔ Add Cypress for both Unit and End-to-End testing? » No # ✔ Add ESLint for code quality? » Yes # ✔ Add Prettier for code formatting? » Yes项目名设为typeflow-desktop。步骤 2添加 Electron 依赖注意版本锁定cd typeflow-desktop npm install --save-dev electron22.3.24 electron-builder23.6.0为什么用electron-builder而不是electron-packager因为它内置了自动更新、代码签名、多平台构建且配置简单。electron-packager需要手动写package.json的build字段容易出错。步骤 3创建 Electron 入口文件新建electron/main.jsconst { app, BrowserWindow, Menu, ipcMain, globalShortcut } require(electron); const path require(path); const url require(url); function createWindow() { const win new BrowserWindow({ width: 1000, height: 700, webPreferences: { preload: path.join(__dirname, preload.js), nodeIntegration: false, // 关键禁用 nodeIntegration contextIsolation: true, // 关键启用上下文隔离 sandbox: true // 启用沙箱更安全 } }); // 开发时加载 Vite 服务器 if (process.env.NODE_ENV development) { win.loadURL(http://localhost:5173); } else { // 生产时加载打包后的 HTML win.loadFile(path.join(__dirname, ../dist/index.html)); } // 创建菜单 const menu Menu.buildFromTemplate([ { role: appMenu }, { role: fileMenu }, { role: editMenu } ]); Menu.setApplicationMenu(menu); // 注册全局快捷键 globalShortcut.register(CommandOrControlShiftT, () { win.webContents.send(start-game); }); return win; } app.whenReady().then(() { const mainWindow createWindow(); // 监听 IPC ipcMain.handle(get-app-info, () ({ version: app.getVersion(), platform: process.platform })); app.on(activate, () { if (BrowserWindow.getAllWindows().length 0) { createWindow(); } }); }); app.on(window-all-closed, () { if (process.platform ! darwin) { app.quit(); } });步骤 4创建 preload.js安全桥梁新建electron/preload.jsconst { contextBridge, ipcRenderer } require(electron); // 白名单 API只暴露必要方法 contextBridge.exposeInMainWorld(electronAPI, { getAppInfo: () ipcRenderer.invoke(get-app-info), onAppReady: (callback) ipcRenderer.on(app-ready, callback), onMenuAction: (action, callback) ipcRenderer.on(menu-${action}, callback), triggerMenuAction: (action) ipcRenderer.send(menu-${action}), saveRecord: (record) ipcRenderer.send(save-record, record) });步骤 5修改 Vite 配置vite.config.tsimport { defineConfig } from vite; import vue from vitejs/plugin-vue; export default defineConfig({ plugins: [vue()], resolve: { alias: { : path.resolve(__dirname, src) } }, // 关键让 Vite 开发服务器允许跨域请求 server: { host: localhost, port: 5173, cors: true, proxy: { /api: { target: http://localhost:3000, changeOrigin: true } } }, // 构建时输出到 dist 目录Electron 会加载这里 build: { outDir: dist, rollupOptions: { external: [electron] } } });4.2 Vue 3 侧集成Composition API 的适配器注入步骤 1创建平台适配器接口新建src/composables/platformAdapter.tsexport interface PlatformAdapter { // 读取词库 readWords(): Promisestring[]; // 保存练习记录 saveRecord(record: { wpm: number; accuracy: number; duration: number }): Promisevoid; // 播放按键音效 playKeySound(key: string): void; // 打开外部链接 openUrl(url: string): void; } // 默认导出一个空实现供开发时使用 export const defaultAdapter: PlatformAdapter { readWords: () Promise.resolve([hello, world]), saveRecord: () Promise.resolve(), playKeySound: () {}, openUrl: () {} };步骤 2创建 Electron 专用适配器新建src/composables/electronAdapter.tsimport { defineAsyncComponent } from vue; import { defaultAdapter, PlatformAdapter } from ./platformAdapter; // 检测是否在 Electron 环境 const isElectron typeof window ! undefined (window as any).electronAPI ! undefined; export const electronAdapter: PlatformAdapter { readWords: async () { if (!isElectron) return defaultAdapter.readWords(); try { const words await (window as any).electronAPI.readWords(); return words; } catch (err) { console.warn(读取词库失败使用默认词库:, err); return defaultAdapter.readWords(); } }, saveRecord: async (record) { if (!isElectron) return defaultAdapter.saveRecord(record); await (window as any).electronAPI.saveRecord(record); }, playKeySound: (key) { if (!isElectron) return; (window as any).electronAPI.playKeySound(key); }, openUrl: (url) { if (!isElectron) { window.open(url, _blank); return; } (window as any).electronAPI.openUrl(url); } };步骤 3在 main.ts 中注入适配器// src/main.ts import { createApp } from vue; import { createPinia } from pinia; import App from ./App.vue; import router from ./router; import { electronAdapter } from ./composables/electronAdapter; const app createApp(App); const pinia createPinia(); // 将适配器注入全局属性所有组件可通过 this.$adapter 访问 app.config.globalProperties.$adapter electronAdapter; app.use(pinia); app.use(router); app.mount(#app);步骤 4在组件中使用适配器!-- src/components/TypingGame.vue -- script setup langts import { ref, onMounted, onUnmounted } from vue; import { useRoute } from vue-router; import { defaultAdapter, PlatformAdapter } from /composables/platformAdapter; // 从全局属性获取适配器 const adapter (getCurrentInstance()?.appContext.config.globalProperties as any).$adapter as PlatformAdapter; const words refstring[]([]); const currentWordIndex ref(0); const userInput ref(); onMounted(async () { try { words.value await adapter.readWords(); } catch (err) { console.error(加载词库失败:, err); } }); const handleSubmit () { const record { wpm: calculateWPM(), accuracy: calculateAccuracy(), duration: 60 }; adapter.saveRecord(record); }; function calculateWPM() { return Math.round((userInput.value.length / 5) / 1); // 简化计算 } function calculateAccuracy() { return 95.5; // 简化 } /script4.3 构建与发布生成真正的.exe和.dmg步骤 1配置 electron-builder新建electron-builder.config.jsconst path require(path); module.exports { appId: com.typeflow.desktop, productName: TypeFlow, copyright: Copyright © 2024 TypeFlow Team, directories: { output: release }, files: [ !node_modules/**/*, !electron/**/*, !src/**/*, !tests/**/*, !*.md, !*.ts, !*.map, !yarn.lock, !package-lock.json, !pnpm-lock.yaml, !npm-debug.log, dist/**/*, electron/**/*, package.json ], win: { target: [ { target: nsis, // 生成 .exe 安装包 arch: [x64, ia32] // 支持 32 位和 64 位 Windows } ], icon: build/icon.ico }, mac: { target: dmg, icon: build/icon.icns, category: public.app-category.productivity }, linux: { target: AppImage, icon: build/icon.png } };步骤 2添加构建脚本到 package.json{ scripts: { dev: concurrently \npm run electron:dev\ \npm run serve\, electron:dev: electron ., serve: vite, build: vue-tsc vite build electron-builder } }步骤 3准备图标关键否则 Windows 显示默认 Electron 图标Windowsbuild/icon.ico需包含 16x16, 32x32, 48x48, 256x256 多尺寸macOSbuild/icon.icns用iconutil从.iconset生成Linuxbuild/icon.png256x256步骤 4执行构建npm run build生成的安装包在release/目录下release/TypeFlow Setup 1.0.0.exeWindows 安装包双击即可安装release/TypeFlow-1.0.0.dmgmacOS 磁盘映像拖拽安装release/TypeFlow-1.0.0.AppImageLinux 可执行镜像注意首次构建会下载 Electron 二进制文件约 120MB耐心等待。后续构建会复用缓存。5. 常见问题与排查技巧实录那些让我熬夜到三点的 Bug5.1 “白屏”问题90% 的 Electron 新手都踩过现象运行npm run electron:dev窗口打开但一片空白控制台无报错。排查顺序检查 Vite 开发服务器是否启动npm run serve是否在另一个终端运行Electron 窗口默认加载http://localhost:5173如果 Vite 没起就是白屏。检查main.js中的loadURL地址开发时是http://localhost:5173生产时是loadFile(path.join(__dirname, ../dist/index.html))。如果开发时误用了loadFile就会加载dist/index.html但此时dist目录还没生成白屏。检查preload.js路径preload: path.join(__dirname, preload.js)中的__dirname指向electron/目录所以preload.js必须放在electron/下。如果放错位置V8 引擎会静默失败白屏。检查 Vue 应用挂载点index.html中是否有div idapp/divVite 模板默认有但如果你删了Vue 就找不到挂载点白屏。终极解决方案在main.js中加调试日志win.webContents.on(did-finish-load, () { console.log(页面加载完成); win.webContents.openDevTools(); // 自动打开 DevTools }); win.webContents.on(render-process-gone, (event, details) { console.error(渲染进程崩溃:, details); });5.2 “无法访问require”Node.js 集成的幻觉现象在 Vue 组件的script标签里写const fs require(fs);报错require is not defined。原因这是 Electron 的安全设计。nodeIntegration: false默认值禁止渲染进程直接访问 Node.js API防止 XSS 攻击。你必须通过contextBridge暴露特定方法。错误修复不要在 Vue 组件里require而是在preload.js里暴露// preload.js const { readFileSync } require(fs); contextBridge.exposeInMainWorld(electronAPI, { readFileSync: (path) readFileSync(path, utf8) });然后在 Vue 里const content window.electronAPI.readFileSync(./data.txt);
返回列表