ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Electron+Vue3桌面应用架构改造实战:从VSCode插件到独立打字游戏

2026/9/10 4:10:57 拓冰建站 浏览量
Electron+Vue3桌面应用架构改造实战:从VSCode插件到独立打字游戏 1. 项目概述为什么一个打字游戏值得做两次Electron Vue 3 桌面打字游戏实战从 VSCode 扩展到独立应用的架构改造——这个标题里藏着三个关键动作“打字游戏”是功能载体“VSCode 扩展”是起点形态“独立应用”是演进目标“架构改造”是核心挑战。我去年接手这个项目时原团队已经用 Vue 3 写好了一个纯 Web 版打字训练器能测 WPM、统计错字率、生成练习报告但用户反馈很集中“能不能离线用”“能不能不打开浏览器”“能不能像 TypingMaster 那样固定在任务栏右下角”——这些诉求背后其实是桌面级交互体验的刚性需求。我们没直接重写而是选择了一条更务实的路径先把它做成 VSCode 插件。理由很实在——VSCode 用户基数大、开发调试链路成熟、自带 Markdown 渲染和终端集成能力我们把打字训练嵌入编辑器侧边栏配合快捷键CtrlShiftT启动还能实时读取当前打开的.md或.txt文件作为练习素材。上线两周插件市场下载量破 2000但很快暴露出瓶颈用户想脱离编辑器单独使用企业客户要求打包成.exe或.dmg分发给无 VSCode 环境的员工教育机构需要禁用网络请求、锁定本地词库路径。这时候“架构改造”就不是可选项而是交付门槛。Electron 成为必然选择但绝不是简单套个壳子。我见过太多 Electron 应用卡在 300MB 安装包、启动慢、内存泄漏三连击上。这次改造的核心目标很明确复用 90% 的 Vue 3 业务逻辑重构 100% 的进程模型与通信机制让同一套代码既能跑在 VSCode 的 WebView 里也能跑在 Electron 的主进程/渲染进程架构中。关键词 Electron、Vue 3、VSCode、架构改造、桌面应用每一个都不是装饰词——Electron 决定底层能力边界Vue 3 是状态管理中枢VSCode 是验证场景架构改造是技术分水岭桌面应用是最终交付形态。适合两类人深度参考一是正在把 Web 工具迁移到桌面端的前端工程师二是想理解 VSCode 插件与 Electron 应用本质差异的技术负责人。你不需要会 C但得清楚contextIsolation开关对window.require的影响你不用写原生模块但得明白preload.js里暴露的 API 如何被 Vue 组件安全调用。2. 架构设计思路为什么必须拆开主进程和渲染进程2.1 VSCode 插件的天然局限与 Electron 的能力跃迁VSCode 插件本质是运行在编辑器沙箱里的 JavaScript 模块它通过vscode全局对象调用编辑器 API比如vscode.workspace.openTextDocument()读文件vscode.window.showInformationMessage()弹提示。这种设计保证了安全性——插件无法直接访问文件系统或执行命令行。但这也成了天花板你想读取用户桌面目录下的words.json不行除非用户手动选中文件你想监听键盘全局按键比如检测 Caps Lock 状态VSCode 不提供这类底层事件你想把训练记录导出为 Excel得依赖第三方库且受限于浏览器环境。而 Electron 的主进程Main Process拥有 Node.js 全权限可以fs.readFileSync()读任意路径child_process.execSync()调用系统命令app.setLoginItemSettings()设置开机自启——这些能力对打字游戏至关重要离线词库加载、本地数据持久化、系统级快捷键注册、安装包自动更新。但直接把 VSCode 插件代码扔进 Electron 渲染进程会崩。原因在于进程模型的根本差异VSCode 插件运行在单个渲染上下文里所有模块共享同一个window对象Electron 渲染进程默认启用contextIsolation: true意味着window和 Node.js 全局变量如require,process完全隔离。如果你在 Vue 组件里写const fs require(fs)会报require is not defined。这就是架构改造的第一道坎必须建立安全、可控、可测试的进程间通信IPC通道把主进程的能力“代理”给渲染进程。2.2 三层架构设计主进程、预加载脚本、渲染进程的职责切分我们最终采用经典的三层架构每层只做一件事且接口清晰主进程main.js只负责系统级操作。它初始化窗口、注册全局快捷键globalShortcut.register(CommandOrControlShiftT, ...)、监听文件系统变化chokidar.watch(path.join(app.getPath(userData), words))、处理自动更新逻辑autoUpdater.checkForUpdatesAndNotify()。它不碰任何 UI 逻辑不导入 Vue甚至不引入electron以外的第三方包。预加载脚本preload.js这是 Electron 安全模型的“闸门”。它运行在渲染进程沙箱内但有权访问 Node.js API。我们在这里定义白名单 API// preload.js const { contextBridge, ipcRenderer } require(electron) contextBridge.exposeInMainWorld(electronAPI, { // 只暴露必要方法且参数类型严格校验 getWords: (path) ipcRenderer.invoke(get-words, path), saveRecord: (data) ipcRenderer.invoke(save-record, data), openFolder: () ipcRenderer.invoke(open-folder), // 键盘事件监听需特殊处理主进程捕获后转发 onKeydown: (callback) ipcRenderer.on(key-down, callback), offKeydown: (callback) ipcRenderer.removeListener(key-down, callback) })关键点在于contextBridge.exposeInMainWorld—— 它把 IPC 调用封装成window.electronAPI对象Vue 组件只需this.$electronAPI.getWords(...)即可完全不知道底层是 IPC 还是 Promise。渲染进程Vue 3 应用这才是真正的业务逻辑层。我们把原 VSCode 插件的src/目录整体迁移过来只做两处修改替换掉所有vscode.workspace.fs.readFile()调用改为window.electronAPI.getWords()将vscode.window.showQuickPick()替换为自研的模态对话框组件其内部调用window.electronAPI.openFolder()获取路径。Vue 3 的 Composition API 让状态管理高度解耦useTypingStore()组合式函数封装了所有打字逻辑useWordLoader()封装了词库加载它们完全不依赖 Electron 或 VSCode API只通过约定好的接口与外部通信。这种设计带来的好处是双向兼容VSCode 插件版本保留vscodeAPI 调用Electron 版本注入window.electronAPI业务层代码零修改。我们用 Vite 的defineConfig动态注入环境变量// vite.config.ts export default defineConfig(({ command, mode }) { if (mode electron) { return { define: { __ELECTRON__: true, __VSCODE__: false } } } return { define: { __ELECTRON__: false, __VSCODE__: true } } })Vue 组件里这样写script setup import { onMounted } from vue import { useWordLoader } from /composables/useWordLoader const wordLoader useWordLoader() onMounted(() { if (__ELECTRON__) { wordLoader.loadFromElectron() } else if (__VSCODE__) { wordLoader.loadFromVSCode() } }) /script2.3 为什么放弃 Webview 标签方案一次踩坑实录早期我们考虑过用webview标签加载 VSCode 插件页面认为这样能最大程度复用。实测发现三个致命问题性能断崖webview是 Chromium 的独立渲染进程每个实例额外消耗 80MB 内存而我们的打字游戏需要常驻后台用户切换 5 个标签页后内存飙升至 1.2GB通信延迟webview.send()发送消息平均耗时 47ms键盘高频输入下每秒 10 次按键UI 响应明显滞后WPM 测评误差达 ±3 字/分钟调试地狱webview内部的 DevTools 无法与主窗口共享每次调试都要打开两个独立调试器断点位置错乱。最终我们砍掉webview坚持用标准BrowserWindowpreload.js方案。虽然初期多写了 200 行 IPC 适配代码但换来的是内存占用稳定在 180MB含 Electron 运行时按键响应延迟 8msDevTools 单窗口调试全覆盖。这印证了一个经验桌面应用的性能优化本质是进程模型的选择优化而不是代码层面的微调。3. 核心细节解析从 VSCode 到 Electron 的 7 处关键改造点3.1 词库加载机制从 VSCode 工作区到用户数据目录的路径映射VSCode 插件读取词库的逻辑很简单// VSCode 版本 const uri vscode.Uri.file(path.join(context.extensionPath, data, words.json)) const content await vscode.workspace.fs.readFile(uri) const words JSON.parse(content.toString())但在 Electron 中context.extensionPath不存在且用户词库应存放在系统规范路径Windows:%APPDATA%\TypingGame\words.jsonmacOS:~/Library/Application Support/TypingGame/words.json。我们设计了统一的路径解析器// main/utils/pathResolver.ts import { app } from electron import * as path from path export const resolveWordPath (filename: string): string { // 优先检查用户数据目录 const userDataPath app.getPath(userData) const userPath path.join(userDataPath, words, filename) // 如果不存在回退到应用资源目录打包后 assets if (!fs.existsSync(userPath)) { const resourcePath process.env.NODE_ENV development ? path.join(__dirname, .., .., assets, words, filename) : path.join(process.resourcesPath, assets, words, filename) return resourcePath } return userPath }主进程 IPC 处理器// main/ipcHandlers.ts ipcMain.handle(get-words, async (event, filename) { try { const fullPath resolveWordPath(filename) const content await fs.promises.readFile(fullPath, utf8) return JSON.parse(content) } catch (error) { // 返回内置默认词库兜底 return getDefaultWords() } })这样设计的好处是用户可自由替换userData/words/下的文件无需重新打包应用开发者更新内置词库只需替换assets/words/目录新用户首次启动自动创建空目录结构。我们还加了文件监听// main/index.ts const wordWatcher chokidar.watch( path.join(app.getPath(userData), words, *.json), { ignoreInitial: true } ) wordWatcher.on(change, () { // 通知所有渲染进程刷新词库 BrowserWindow.getAllWindows().forEach(win { win.webContents.send(words-updated) }) })Vue 组件监听script setup import { onBeforeUnmount, onMounted } from vue onMounted(() { window.addEventListener(words-updated, () { wordStore.refreshWords() }) }) onBeforeUnmount(() { window.removeEventListener(words-updated, () {}) }) /script3.2 键盘事件捕获全局快捷键与游戏内按键的冲突解决打字游戏的核心是精准捕获按键但 VSCode 插件和 Electron 应用的键盘事件源完全不同。VSCode 插件监听document.addEventListener(keydown)即可因为编辑器本身已劫持了所有输入焦点。Electron 则需区分两种场景全局快捷键如CmdShiftT唤起游戏窗口由主进程globalShortcut.register()处理游戏内按键如A,S,D需在渲染进程捕获但必须绕过浏览器默认行为如输入框聚焦、页面滚动。难点在于当游戏窗口激活时document.body可能没有焦点keydown事件无法触发。解决方案是强制聚焦并阻止默认行为template div refgameContainer tabindex0 keydown.preventhandleKeydown focusisFocused true blurisFocused false !-- 游戏内容 -- /div /template script setup import { ref, onMounted } from vue const gameContainer ref(null) const isFocused ref(false) onMounted(() { // 页面加载后立即聚焦确保键盘事件可用 setTimeout(() { gameContainer.value?.focus() }, 100) }) const handleKeydown (e: KeyboardEvent) { if (!isFocused.value) return // 过滤修饰键、功能键等非字符键 if (e.key.length ! 1 || e.ctrlKey || e.altKey || e.metaKey) return // 传递给业务逻辑 typingStore.inputChar(e.key) } /script提示tabindex0让 div 可聚焦focus/blur监听焦点状态keydown.prevent阻止浏览器默认行为如F5刷新、Space滚动。实测下来这套方案在 Windows/macOS/Linux 上按键捕获准确率 99.98%唯一例外是某些笔记本的 Fn 组合键如FnF11需在主进程用systemPreferences.isDarkMode()等 API 单独处理。3.3 数据持久化从 VSCode 配置存储到 SQLite 的平滑迁移VSCode 插件用vscode.workspace.getConfiguration().update()存储用户设置但这是键值对存储不适合存训练记录每次打字生成 50 字段的 JSON。Electron 版本我们升级为 SQLite理由很实际查询快按日期范围查历史记录SQLite 的WHERE date BETWEEN ? AND ?比遍历 JSON 数组快 12 倍原子性BEGIN TRANSACTION保证“保存记录更新统计”不中断跨平台better-sqlite3在 Windows/macOS/Linux 上二进制兼容无需编译。主进程初始化数据库// main/database.ts import Database from better-sqlite3 import { app } from electron import * as path from path const dbPath path.join(app.getPath(userData), typing.db) const db new Database(dbPath) db.exec( CREATE TABLE IF NOT EXISTS records ( id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp TEXT NOT NULL, wpm REAL NOT NULL, accuracy REAL NOT NULL, duration INTEGER NOT NULL, words TEXT NOT NULL, errors TEXT NOT NULL ) ) export default dbIPC 处理器ipcMain.handle(save-record, async (event, record) { const stmt db.prepare( INSERT INTO records (timestamp, wpm, accuracy, duration, words, errors) VALUES (?, ?, ?, ?, ?, ?) ) stmt.run( record.timestamp, record.wpm, record.accuracy, record.duration, JSON.stringify(record.words), JSON.stringify(record.errors) ) })Vue 组件调用// composables/useRecord.ts export const useRecord () { const saveRecord async (record: RecordData) { try { await window.electronAPI.saveRecord(record) console.log(记录已保存) } catch (error) { // 降级存到 localStorage localStorage.setItem(record-${Date.now()}, JSON.stringify(record)) } } return { saveRecord } }注意SQLite 的INSERT操作在主线程阻塞但实测单条记录插入 2ms不影响 UI 帧率。如果未来记录量超百万我们会引入 WAL 模式和分表策略但当前 10 万条记录下查询仍 15ms。3.4 菜单系统从 VSCode 命令面板到原生系统菜单的映射VSCode 插件菜单全靠package.json的contributes.commands和contributes.menus声明用户通过CtrlShiftP调用。Electron 需要原生菜单且要适配不同系统macOS 的应用菜单在顶部Windows/Linux 在窗口内。我们用 Electron 的Menu.buildFromTemplate()构建// main/menu.ts import { app, Menu, MenuItemConstructorOptions } from electron const isMac process.platform darwin const template: MenuItemConstructorOptions[] [ // macOS 应用菜单 ...(isMac ? [{ label: app.name, submenu: [ { role: about }, { type: separator }, { role: services }, { type: separator }, { role: hide }, { role: hideothers }, { role: unhide }, { type: separator }, { role: quit } ] }] : []), // 文件菜单 { label: 文件, submenu: [ { label: 新建练习, accelerator: CmdOrCtrlN, click: () mainWindow?.webContents.send(new-exercise) }, { label: 导入词库, accelerator: CmdOrCtrlO, click: () mainWindow?.webContents.send(import-words) }, { type: separator }, { label: 退出, accelerator: CmdOrCtrlQ, role: quit } ] }, // 编辑菜单仅 Windows/Linux ...(isMac ? [] : [{ label: 编辑, submenu: [ { role: undo }, { role: redo }, { type: separator }, { role: cut }, { role: copy }, { role: paste } ] }]), // 帮助菜单 { label: 帮助, submenu: [ { label: 查看文档, click: () shell.openExternal(https://docs.typinggame.dev) }, { label: 检查更新, click: () autoUpdater.checkForUpdatesAndNotify() } ] } ] const menu Menu.buildFromTemplate(template) Menu.setApplicationMenu(menu)关键技巧accelerator字段自动绑定快捷键click回调用webContents.send()触发渲染进程事件避免在菜单项里写业务逻辑。Vue 组件监听// src/main.ts window.addEventListener(new-exercise, () { typingStore.startNewExercise() })3.5 自动更新从 VSCode 扩展商店到 Electron 自托管的无缝切换VSCode 插件更新由 Marketplace 自动完成用户无感知。Electron 应用需自己实现。我们选用electron-updaterSquirrel.Windows / Sparkle macOS但避开了它的“重启后生效”缺陷——打字游戏不能突然中断用户训练。方案是更新下载完成后不立即重启而是弹窗提示“新版已就绪点击此处立即更新”用户点击后先保存当前训练状态到localStorage再调用autoUpdater.quitAndInstall()应用重启时检查localStorage是否有未完成训练自动恢复。主进程更新逻辑// main/updater.ts import { autoUpdater } from electron-updater import { app, dialog, BrowserWindow } from electron autoUpdater.on(update-available, () { dialog.showMessageBox({ title: 发现新版本, message: 新版 TypingGame 已准备好是否立即更新, buttons: [立即更新, 稍后提醒] }).then(result { if (result.response 0) { // 保存当前状态 const currentSession getCurrentSession() localStorage.setItem(pendingUpdateSession, JSON.stringify(currentSession)) autoUpdater.quitAndInstall() } }) })渲染进程恢复逻辑// src/main.ts if (localStorage.getItem(pendingUpdateSession)) { const session JSON.parse(localStorage.getItem(pendingUpdateSession)!) typingStore.restoreSession(session) localStorage.removeItem(pendingUpdateSession) }3.6 打包配置从 VSCode 插件发布到 Electron Builder 的精细化控制VSCode 插件打包用vsce package生成.vsix文件。Electron 应用打包用electron-builder但默认配置会把node_modules全打包导致安装包 300MB。我们做了三处关键压缩依赖分析用depcheck扫描package.json移除未使用的 devDependencies如types/node在生产环境不需要白名单打包electron-builder.yml中指定files字段只包含必要文件files: - !node_modules/** - !src/** - !tests/** - !*.ts - !*.map - dist/** - node_modules/better-sqlite3/** - node_modules/sqlite3/** - assets/** - main.js - preload.js压缩算法启用compression: maximum和asar: true实测将 120MB 的node_modules压缩到 28MB。最终安装包大小Windows x64 为 86MBmacOS arm64 为 72MB比同类工具小 40%。用户反馈“下载快、安装秒完成”这直接影响留存率——我们 A/B 测试显示安装包 100MB 的版本次日留存率高 22%。3.7 调试体系从 VSCode Debugger 到 Electron Inspector 的双轨调试VSCode 插件调试直接 F5 启动 Extension Development Host断点打在哪都生效。Electron 需要两套调试器主进程调试在main.js顶部加require(electron).app.commandLine.appendSwitch(inspect, 5858)然后用 Chrome 访问chrome://inspect渲染进程调试BrowserWindow创建时设webPreferences.devTools: true右键菜单可打开 DevTools。但我们发现频繁切换调试器效率低于是搭建了统一调试入口主进程启动时自动打开http://localhost:3000Vite 开发服务器渲染进程通过fetch(/api/debug-info)获取主进程 PID、内存占用等指标在 Vue 组件里嵌入简易监控面板实时显示process.memoryUsage()、app.getAppMetrics()数据。这样一个浏览器窗口就能同时看业务逻辑和系统指标调试效率提升 3 倍。我们还加了错误上报// main/errorHandler.ts process.on(uncaughtException, (error) { // 记录到本地日志 fs.appendFileSync( path.join(app.getPath(userData), error.log), [${new Date().toISOString()}] ${error.stack}\n ) // 发送到 Sentry脱敏处理 if (app.isPackaged) { sentry.captureException(error, { extra: { isPackaged: true } }) } })4. 实操过程从零开始的 Electron Vue 3 架构改造全流程4.1 环境准备与项目初始化15 分钟第一步不是写代码而是确认 Electron 版本兼容性。Vue 3.4 需要 Electron 22因 V8 引擎升级而 VSCode 1.85 基于 Electron 22所以版本对齐是前提。我们用create-electron-vue脚手架快速初始化npm create electron-vuelatest typing-game -- --preset vue3-vite-ts cd typing-game npm install脚手架生成的目录结构是标准的typing-game/ ├── src/ │ ├── main/ # 主进程代码 │ ├── preload/ # 预加载脚本 │ └── renderer/ # 渲染进程Vue 应用 ├── packages.json └── vite.config.ts关键修改点vite.config.ts中关闭build.lib模式我们不需要生成 UI 组件库src/main/index.ts中注释掉默认的createWindow()改用我们自己的窗口配置src/preload/index.ts中删除contextBridge.exposeInMainWorld(electronAPI, {})的空对象替换成实际 API。此时运行npm run dev应该看到空白窗口和控制台Electron Vue 3 ready日志。这是第一个里程碑——证明 Electron 运行时和 Vue 3 渲染器已打通。4.2 迁移 VSCode 插件业务代码2 小时VSCode 插件的src/目录结构通常是vscode-extension/ ├── src/ │ ├── extension.ts # 插件激活入口 │ ├── webview/ # Webview 页面 │ │ ├── index.html │ │ ├── index.ts │ │ └── index.css │ └── common/ # 通用逻辑 │ └── typing.ts # 打字核心算法迁移步骤将vscode-extension/src/common/复制到typing-game/src/renderer/composables/将vscode-extension/src/webview/index.*复制到typing-game/src/renderer/views/TypingGame.vue修改TypingGame.vue中的 API 调用vscode.workspace.fs.readFile()→window.electronAPI.getWords()vscode.window.showInputBox()→useInputBox()自定义 Hook内部调用window.electronAPI.openFolder()在src/renderer/main.ts中注入electronAPIimport { createApp } from vue import App from ./App.vue const app createApp(App) // 注入全局属性 app.config.globalProperties.$electronAPI window.electronAPI app.mount(#app)实操心得不要试图“完美迁移”先让基础功能跑起来。我们第一天只迁移了词库加载和打字计时其他功能如统计图表、设置面板第二天再补。快速验证比一步到位更重要——毕竟用户不会为“还没完成的完美”买单。4.3 实现 IPC 通信层3 小时这是架构改造的心脏。我们按“先通后优”原则分三步第一步最小可行 IPC主进程main/index.tsimport { app, BrowserWindow, ipcMain } from electron ipcMain.handle(ping, () pong)预加载preload/index.tsimport { contextBridge, ipcRenderer } from electron contextBridge.exposeInMainWorld(electronAPI, { ping: () ipcRenderer.invoke(ping) })Vue 组件测试script setup import { onMounted } from vue onMounted(async () { const res await window.electronAPI.ping() console.log(res) // 输出 pong }) /script跑通后立刻进入第二步。第二步实现核心业务 IPC按优先级实现get-words读取词库带路径解析和错误兜底save-record保存训练记录带 SQLite 事务open-folder选择文件夹返回路径字符串key-down全局键盘监听主进程捕获后webContents.send()。每个 IPC 处理器都加了日志和错误捕获ipcMain.handle(get-words, async (event, filename) { console.time(get-words: ${filename}) try { const result await loadWords(filename) console.timeEnd(get-words: ${filename}) return result } catch (error) { console.error(get-words failed for ${filename}:, error) throw error } })第三步添加 TypeScript 类型定义在src/preload/index.ts中定义接口export interface ElectronAPI { getWords: (filename: string) PromiseWordList saveRecord: (record: RecordData) Promisevoid openFolder: () Promisestring onKeydown: (callback: (e: KeyboardEvent) void) void } declare global { interface Window { electronAPI: ElectronAPI } }这样 Vue 组件里window.electronAPI.getWords()就有完整类型提示减少 80% 的运行时错误。4.4 集成系统级功能4 小时全局快捷键在main/index.ts中注册注意 Windows/macOS 的键位差异const shortcut process.platform darwin ? CommandOrControlShiftT : ControlShiftT globalShortcut.register(shortcut, () { if (mainWindow?.isMinimized()) mainWindow?.restore() mainWindow?.show() mainWindow?.focus() })托盘图标Tray模块支持右键菜单我们加了“显示主窗口”、“退出”两项const tray new Tray(iconPath) tray.setToolTip(TypingGame) tray.setContextMenu(Menu.buildFromTemplate([ { label: 显示主窗口, click: () mainWindow?.show() }, { label: 退出, click: () app.quit() } ]))开机自启app.setLoginItemSettings()一行代码搞定但需用户授权macOS 10.15 需在Info.plist中声明LSBackgroundOnly。4.5 打包与发布1 小时electron-builder配置electron-builder.ymlappId: com.typinggame.app productName: TypingGame copyright: Copyright © 2024 TypingGame artifactName: ${productName}-${version}-${platform}-${arch}.${ext} directories: output: dist files: - dist/** - node_modules/better-sqlite3/** - node_modules/sqlite3/** - assets/** - main.js - preload.js win: target: - target: nsis icon: build/icon.ico mac: target: - target: dmg icon: build/icon.icns category: public.app-category.productivity linux: target: - target: deb icon: build/icons执行npm run build输出在dist/目录。我们用electron-installer-redhat生成 RPM 包供企业客户部署用electron-installer-windows生成 MSI 安装包支持静默安装/quiet参数。5. 常见问题与排查技巧实录12 个真实踩坑场景及解决方案5.1 “require is not defined” 错误90% 的新手卡点现象Vue 组件里const fs require(fs)报错。根因contextIsolation: trueElectron 默认开启隔离了 Node.js 全局变量。解决方案✅ 正确做法在preload.js中通过contextBridge暴露 API如electronAPI.getWords()❌ 错误做法设contextIsolation: false严重安全风险禁止⚠️ 临时调试仅开发时在webPreferences中加nodeIntegration: true但必须在生产构建前删掉。实操心得把这个错误当成“安全红线测试”——每次看到require is not defined就检查preload.js是否暴露了对应 API。我们团队立下规矩渲染进程代码里禁止出现require、__dirname、process字样全部走electronAPI。5.2 窗口白屏Webview 加载失败的 3 种排查路径现象启动后窗口空白控制台无报错。排查路径检查mainWindow.loadFile()路径loadFile(dist/index.html)中的dist/目录是否存在Vite 构建后路径是dist/renderer/index.html需同步修改检查preload.js路径webPreferences.preload必须是绝对路径用path.join(__dirname, ../preload/index.js)检查 CSP 策略Vite 默认加了Content-Security-Policy在 Electron 中需禁用// main/index.ts mainWindow new BrowserWindow({ webPreferences: { preload: path.join(__dirname, ../preload/index.js), contextIsolation: true,