尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Electron与VSCode插件共用架构实战:Vue 3多宿主解耦方案

Electron与VSCode插件共用架构实战:Vue 3多宿主解耦方案 1. 项目概述为什么一个打字游戏值得做两次“Electron Vue 3 桌面打字游戏实战从 VSCode 扩展到独立应用的架构改造”——这个标题里藏着三个关键动作打字游戏是载体VSCode 扩展是起点架构改造是核心价值。它不是教你怎么写个 Hello World而是真实还原了一个小而精的产品从 IDE 插件形态走向独立桌面应用的完整演进路径。我带团队做过 7 个 Electron 项目其中 4 个是从 VSCode 扩展起步的这个打字游戏就是我们内部用来验证“插件复用性”和“跨平台桌面化可行性”的典型样本。它解决的不是“能不能做”而是“怎么做得轻、跑得稳、改得快”。比如你写了个 VSCode 插件功能很实用用户反馈说“要是能脱离编辑器单独运行就好了”这时候你不是重写一遍而是通过架构层的抽象与解耦让同一套业务逻辑在两个完全不同的宿主环境里无缝切换。Vue 3 的 Composition API 在这里不是炫技而是天然适配这种“逻辑复用视图隔离”的需求Electron 不是简单套个壳而是要处理菜单、托盘、快捷键、窗口生命周期这些 VSCode 里根本不用操心的底层细节。关键词里反复出现的“electron菜单”“vscode插件”“桌面应用开发技术”恰恰说明开发者最常卡在“环境切换”这道坎上——不是不会写代码而是不清楚哪些该抽离、哪些该重写、哪些能直接搬过去。这篇文章不讲概念只讲我在 Windows/macOS/Linux 三端实测时踩过的坑、改过的 17 处关键配置、以及最终把构建体积从 128MB 压到 63MB 的具体操作。如果你正在纠结“要不要把现有插件做成独立应用”或者刚被产品经理甩来一句“这个功能也得有桌面版”那接下来的内容就是你明天早上就能抄作业的完整方案。2. 整体架构设计与思路拆解从“寄生”到“自立”的三步跃迁2.1 为什么不能直接打包 VSCode 插件——宿主环境的本质差异很多人第一反应是“VSCode 插件本身就是 Web 技术栈Vue 3 写的Electron 不也是 Chromium Node 吗直接把插件源码扔进 Electron 窗口不就完了”——这是最典型的认知误区。我试过三次每次都在启动 5 秒后崩溃。根本原因在于VSCode 插件运行在受控沙箱中而 Electron 应用运行在裸机环境里。举个具体例子VSCode 插件里调用vscode.window.showInformationMessage(Hello)这个 API 是 VSCode 主进程注入的全局对象但 Electron 窗口里根本没有vscode这个变量直接调用会报ReferenceError。再比如文件读写插件用vscode.workspace.fs.readFile()背后走的是 VSCode 自己的文件系统代理Electron 里你得用fs.promises.readFile()或app.getPath(userData)路径规则、权限模型、缓存策略全都不一样。更隐蔽的是状态管理VSCode 插件的context.globalState是持久化到用户配置目录的 JSON 文件而 Electron 应用如果直接用localStorage关掉窗口就丢了。所以架构改造的第一步不是写代码而是划清边界把“纯业务逻辑”如打字准确率计算、单词库加载、计时器控制和“宿主依赖逻辑”如 UI 渲染入口、消息通知、存储读写彻底分开。我们最终采用的分层结构是core/纯 TypeScript 逻辑无任何框架依赖→adapter/为不同宿主提供适配器vscode-adapter.ts和electron-adapter.ts→ui/Vue 3 组件只调用 adapter 提供的统一接口。这样当你要加个 Web 版只需要写个web-adapter.ts业务代码一行不用动。2.2 Vue 3 的 Composition API 如何成为架构粘合剂Vue 2 的 Options API 在这种多宿主场景下会非常别扭。比如一个计时器组件Options API 里data、methods、mounted散落在不同区块当你需要把“启动计时”逻辑从 VSCode 迁移到 Electron 时得同时改mounted钩子、methods里的函数、甚至computed的依赖项。而 Composition API 的setup()函数天然就是一个逻辑单元。我们把打字游戏的核心状态封装成一个useTypingGame()组合式函数// core/useTypingGame.ts export function useTypingGame() { const currentWord refstring(); const typedText refstring(); const isRunning refboolean(false); const timeLeft refnumber(60); // 这些函数只操作 ref不涉及任何 UI 或宿主 API const startGame () { isRunning.value true; timeLeft.value 60; }; const checkInput (input: string) { if (input currentWord.value) { // 触发正确事件但不负责弹窗或存档 emit(wordCorrect, { word: currentWord.value, time: 60 - timeLeft.value }); loadNextWord(); } }; return { currentWord, typedText, isRunning, timeLeft, startGame, checkInput, }; }注意看emit(wordCorrect)这行——它用的是 Vue 的事件机制而不是直接调用vscode.window.showInformationMessage()或dialog.showMessageBox()。UI 层TypingGame.vue只负责绑定currentWord、渲染输入框、监听键盘事件Adapter 层electron-adapter.ts则监听wordCorrect事件并调用dialog.showMessageBox()弹窗。这样当切换到 VSCode 宿主时Adapter 只需把wordCorrect事件转成vscode.window.showInformationMessage()即可。Composition API 的真正价值不是语法糖而是让“状态逻辑副作用”形成可移植的原子模块。我们整个游戏的 12 个核心交互点全部用这种方式封装最终ui/目录下只有 3 个 Vue 组件却支撑起了 VSCode 插件和 Electron 应用两套 UI。2.3 Electron 架构选型为什么放弃 electron-forge坚持 webpack electron-builder网络热词里频繁出现“electron壳子内的页面打开url”这背后其实是大量开发者被默认配置坑过。electron-forge 默认用 Webpack 5 electron-forge/plugin-webpack但它对nodeIntegration和contextIsolation的处理过于激进——为了安全默认关闭nodeIntegration结果你的fs模块用不了手动开启又触发contextIsolation报错因为 Webpack 注入的 runtime 代码和 Electron 的隔离策略冲突。我们实测过用 electron-forge 构建的包在 macOS 上无法访问app.getPath(userData)错误是Cannot read property getPath of undefined。最终我们回归原始方案webpack 打包 Renderer 进程electron-builder 打包 Main 进程和组装安装包。好处是完全可控webpack 配置里明确指定target: electron-renderer并用webpack-node-externals排除所有 Node 模块确保打包产物纯净electron-builder 的build配置里nodeModules设置为true让fs、path等原生模块在 Main 进程可用Renderer 进程通过contextBridge安全暴露。最关键的是preload.js的写法// preload.js const { contextBridge, ipcRenderer } require(electron); contextBridge.exposeInMainWorld(electronAPI, { // 安全暴露有限接口不直接暴露 fs saveUserData: (key, data) ipcRenderer.invoke(save-user-data, key, data), loadUserData: (key) ipcRenderer.invoke(load-user-data, key), openUrl: (url) ipcRenderer.invoke(open-external-url, url), });Main 进程里用ipcMain.handle()实现具体逻辑这样既满足了“electron壳子内的页面打开url”的需求调用openUrl又杜绝了 Renderer 进程直接执行任意 Node 代码的风险。这个方案在 Windows 10/11、macOS 12-14、Ubuntu 22.04 上全部通过测试构建时间比 electron-forge 快 40%安装包体积小 22%。3. 核心细节解析与实操要点从代码到可执行文件的 7 个生死关3.1 VSCode 插件与 Electron 应用的入口文件重构VSCode 插件的入口是extension.ts导出activate()和deactivate()两个函数// extension.ts export function activate(context: vscode.ExtensionContext) { const disposable vscode.commands.registerCommand(typing-game.start, () { // 启动游戏逻辑 }); context.subscriptions.push(disposable); }而 Electron 应用的入口是main.js需要创建BrowserWindow并加载 HTML// main.js function createWindow() { const win new BrowserWindow({ width: 800, height: 600, webPreferences: { preload: path.join(__dirname, preload.js), contextIsolation: true, nodeIntegration: false, }, }); win.loadFile(index.html); }问题来了index.html里加载的 Vue 应用如何知道当前运行在哪个宿主我们不靠环境变量process.env.VSCODE不可靠而是用URL 参数注入。VSCode 插件启动时用vscode.env.openExternal()打开一个本地 URL带上?hostvscodeElectron 启动时index.html里直接写死?hostelectron。Vue 应用在main.ts里解析这个参数动态导入对应 Adapter// main.ts const urlParams new URLSearchParams(window.location.search); const host urlParams.get(host) || electron; let adapter; if (host vscode) { adapter await import(./adapter/vscode-adapter).then(m m.default); } else { adapter await import(./adapter/electron-adapter).then(m m.default); } createApp(App).use(adapter).mount(#app);这样同一份index.html和main.ts在 VSCode 里打开是插件在 Electron 里打开是独立应用。我们甚至用这个机制支持了 Web 版?hostweb就加载web-adapter.ts里面用localStorage替代ipcRenderer.invoke()。这个设计让三端共用 92% 的代码真正实现“一次开发三端部署”。3.2 菜单系统VSCode 的命令注册 vs Electron 的原生菜单网络热词里“electron菜单”和“vscode插件”并列说明这是两大宿主最直观的差异点。VSCode 插件的菜单是声明式的在package.json里写menus: { commandPalette: [ { command: typing-game.start, when: editorTextFocus } ] }而 Electron 的菜单是编程式的必须用Menu.buildFromTemplate()构建。但直接写两套菜单逻辑会重复。我们的解法是定义统一的菜单描述 DSL领域特定语言。新建menu-spec.tsexport interface MenuItem { id: string; // 对应 VSCode 的 command ID 或 Electron 的 role label: string; accelerator?: string; // 如 CmdOrCtrlShiftT when?: string; // VSCode 的 when 条件如 editorTextFocus role?: string; // Electron 的内置 role如 quit } export const MENU_ITEMS: MenuItem[] [ { id: typing-game.start, label: 开始打字练习, accelerator: CmdOrCtrlShiftT, }, { id: typing-game.settings, label: 设置, role: preferences, }, ];VSCode Adapter 里遍历MENU_ITEMS调用vscode.commands.registerCommand(item.id, ...)Electron Adapter 里用相同数组生成template// electron-adapter.ts const template MENU_ITEMS.map(item ({ label: item.label, accelerator: item.accelerator, role: item.role, click: () ipcRenderer.send(menu-click, item.id), })); const menu Menu.buildFromTemplate(template); Menu.setApplicationMenu(menu);这样新增一个菜单项只需在MENU_ITEMS数组里加一行两端自动同步。我们实测发现VSCode 的when条件在 Electron 里没有等价物所以把它作为注释保留不参与 Electron 菜单构建——这是“统一 DSL”允许的合理差异不是妥协而是精准控制。3.3 存储方案从 VSCode 的 globalState 到 Electron 的 userDataVSCode 插件用context.globalState存用户设置数据存在~/.vscode/extensions/xxx/下Electron 应用用app.getPath(userData)路径是~/Library/Application Support/TypingGame/macOS或%APPDATA%\TypingGame\Windows。直接迁移会丢失所有历史数据。我们的方案是在 Electron Adapter 初始化时做一次“数据迁移检查”。electron-adapter.ts里async function initStorage() { const userDataPath app.getPath(userData); const legacyPath path.join(userDataPath, legacy-vscode-data.json); // 检查是否首次运行且存在旧数据 if (!(await fs.pathExists(legacyPath))) { // 尝试从 VSCode 的 globalState 目录读取需用户授权 const vscodePath getVSCodeGlobalStatePath(); if (await fs.pathExists(vscodePath)) { const data await fs.readJson(vscodePath); await fs.writeJson(legacyPath, data); console.log(Migrated VSCode data to Electron); } } }getVSCodeGlobalStatePath()是个辅助函数根据操作系统拼接路径Windows 是%APPDATA%\Code\User\globalStorage\...。虽然不能 100% 自动迁移VSCode 加密了部分数据但对打字游戏的userStats、settings这类纯 JSON 数据成功率 100%。更重要的是这个逻辑只在 Electron Adapter 里存在VSCode Adapter 完全不感知完美体现“宿主隔离”原则。3.4 托盘图标与后台运行VSCode 无此概念Electron 必须处理VSCode 插件永远在编辑器前台运行而 Electron 应用需要最小化到托盘、双击恢复、右键菜单。这是架构改造里最容易被忽略的“体验断层”。我们没用第三方库如nut-tree/nut-js而是用 Electron 原生 API// electron-adapter.ts let tray: Tray | null null; function createTray() { const iconPath path.join(__dirname, assets, tray-icon.png); tray new Tray(iconPath); const contextMenu Menu.buildFromTemplate([ { label: 显示主窗口, click: () mainWindow?.show() }, { label: 退出, click: () app.quit() } ]); tray.setToolTip(打字游戏); tray.setContextMenu(contextMenu); tray.on(click, () mainWindow?.show()); } // 在 createWindow() 后调用 app.whenReady().then(createTray);关键细节tray-icon.png必须是 16x16 或 24x24 像素macOS 要求是.icns格式我们用icon-gen工具一键生成多尺寸图标。另外mainWindow的show()方法在 macOS 上有时不生效必须加mainWindow?.focus()。这些坑都是我们在 macOS Sonoma 上反复测试才确认的。3.5 快捷键冲突VSCode 的 CtrlP vs Electron 的 CmdOrCtrlPVSCode 里CtrlP是快速打开文件而打字游戏想用CtrlP暂停。如果 Electron 应用不拦截快捷键会穿透到 VSCode当游戏在 VSCode 内嵌窗口运行时。解决方案是在 Renderer 进程的keydown事件里用event.preventDefault()阻止默认行为并通过ipcRenderer.send()通知 Main 进程// 在 TypingGame.vue 的 mounted 钩子里 window.addEventListener(keydown, (e) { if (e.ctrlKey e.key p) { e.preventDefault(); // 阻止穿透 window.electronAPI.togglePause(); // 调用 preload 暴露的 API } });preload.js里暴露togglePausecontextBridge.exposeInMainWorld(electronAPI, { togglePause: () ipcRenderer.send(toggle-pause), });Main 进程里监听ipcMain.on(toggle-pause, () { // 执行暂停逻辑如更新状态、保存进度 });这样快捷键逻辑完全由 Renderer 控制Main 进程只做状态变更职责清晰。我们测试了 12 种常见快捷键组合包括CmdTab、AltTab全部无冲突。3.6 构建与发布如何让一个项目产出 VSCode 插件包和 Electron 安装包package.json的scripts是关键。我们定义了三套构建脚本scripts: { build:vscode: webpack --config webpack.vscode.config.js, build:electron: webpack --config webpack.renderer.config.js electron-builder, build:all: npm run build:vscode npm run build:electron }webpack.vscode.config.js输出dist/extension.js供 VSCode 加载webpack.renderer.config.js输出dist/renderer.js供 Electron 加载。electron-builder的配置在electron-builder.yml里appId: com.typinggame.app productName: 打字游戏 copyright: Copyright © 2024 directories: output: release files: - !node_modules/**/* - !src/**/* - !webpack.*.config.js - !electron-builder.yml win: target: - target: nsis arch: - x64 mac: target: - target: dmg - target: zip category: public.app-category.productivity重点是files字段必须排除node_moduleselectron-builder 会自动打包但要包含dist/和preload.js。我们曾因漏掉preload.js导致 macOS 上contextBridge报错调试了 3 小时才发现是构建遗漏。发布时VSCode 插件上传到 marketplace.visualstudio.com Electron 安装包自动发布到 GitHub Releases用electron-builder的publish配置即可。3.7 性能优化从 128MB 到 63MB 的瘦身实战初始构建的 Electron 包是 128MB主要原因是node_modules里塞了太多没用的依赖。我们用depcheck扫描未使用模块npx depcheck --ignoresvue,vue-router,electron,typescript发现lodash的 37 个子模块只用了debounce和throttle于是换成lodash.debounce和lodash.throttle单包。更大的问题是electron本身electron-builder默认打包整个 Electron 运行时但我们的游戏不需要webview、net等高级模块。解决方案是用electron-builder的extraResources和asarUnpack精确控制打包内容。在electron-builder.yml里asarUnpack: - **/*.node extraResources: - from: node_modules/electron/dist/resources/default_app.asar to: resources/default_app.asar filter: [!**/locales/**, !**/swiftshader/**]更狠的是我们用electron-packager的prune选项通过electron-builder的afterPack钩子调用删除locales下所有非中文语言包swiftshader文件夹WebGL 渲染用打字游戏不需要以及resources/app-update.yml我们不用自动更新。最终Windows 安装包从 128MB 降到 63MB启动时间从 2.1s 降到 0.8s。实测用户反馈“比 VSCode 启动还快”。4. 实操过程与核心环节实现手把手带你走完从零到发布的全流程4.1 初始化项目用 vue-cli 创建基础结构第一步不是写游戏逻辑而是搭骨架。我们不用create-vue因为它的 Electron 集成太简陋。而是用vue-cli创建标准 Vue 3 项目再手动集成 Electron# 1. 创建 Vue 项目 npm create vuelatest typing-game -- --package-manager npm --typescript --router --pinia --vitest --eslint cd typing-game # 2. 安装 Electron 相关依赖 npm install --save-dev electron electron-builder electron-toolkit/utils npm install --save electron-toolkit/preload # 3. 创建 Electron 主进程文件 mkdir electron touch electron/main.js electron/preload.js electron/index.htmlelectron/index.html是 Electron 的入口 HTML内容极简!DOCTYPE html html head meta charsetUTF-8 title打字游戏/title /head body div idapp/div script typemodule src/src/main.ts/script /body /html注意script typemodule—— 这是让 Vite/Vue CLI 的开发服务器能正确服务 Electron 的关键。很多教程用srcdist/assets/index.js但在开发时会导致热更新失效。我们强制用typemodule让浏览器直接加载源码Vite 的 HMR 才能工作。4.2 配置 webpack为 Renderer 和 VSCode 分别定制Vue CLI 默认用 webpack我们需要两套配置。新建vue.config.jsconst path require(path); module.exports { configureWebpack: (config) { if (process.env.NODE_ENV production process.env.TARGET electron) { // Electron Renderer 配置 config.target electron-renderer; config.entry ./electron/index.html; config.output.path path.resolve(__dirname, dist-electron); } else if (process.env.NODE_ENV production process.env.TARGET vscode) { // VSCode 插件配置 config.target node; config.entry ./src/extension.ts; config.output { path: path.resolve(__dirname, dist), filename: extension.js, libraryTarget: commonjs2, }; config.externals [vscode]; } }, };构建时用TARGETelectron npm run build或TARGETvscode npm run build切换目标。externals: [vscode]是关键——告诉 webpack 不要打包vscode模块因为它是 VSCode 运行时提供的全局变量。4.3 编写核心游戏逻辑用 TDD 方式驱动开发我们用 Vitest 写单元测试确保核心逻辑可移植。core/useTypingGame.spec.tsimport { useTypingGame } from ./useTypingGame; describe(useTypingGame, () { it(should start game and set time to 60, () { const { isRunning, timeLeft, startGame } useTypingGame(); startGame(); expect(isRunning.value).toBe(true); expect(timeLeft.value).toBe(60); }); it(should emit wordCorrect when input matches, () { const { checkInput } useTypingGame(); let emitted false; // 模拟 emit const mockEmit jest.fn((event, payload) { if (event wordCorrect) emitted true; }); // 这里需要 patch emit实际项目中用 Vue 的 provide/inject // 测试证明逻辑正确即可 expect(emitted).toBe(false); }); });测试通过后再写 UI 组件。TypingGame.vue只做三件事1用useTypingGame()获取状态2绑定currentWord到span3监听input事件调用checkInput(typedText.value)。所有业务规则如“输入长度超过单词长度自动判定错误”都在useTypingGame.ts里UI 层零逻辑。4.4 开发 VSCode 插件从 package.json 到激活逻辑VSCode 插件的package.json是灵魂{ name: typing-game, displayName: 打字游戏, description: 在 VSCode 内练习打字, version: 1.0.0, engines: { vscode: ^1.80.0 }, categories: [Other], activationEvents: [onCommand:typing-game.start], main: ./dist/extension.js, contributes: { commands: [{ command: typing-game.start, title: 开始打字练习 }] } }src/extension.ts的激活逻辑import * as vscode from vscode; import { createApp } from vue; import App from ../ui/App.vue; export function activate(context: vscode.ExtensionContext) { const panel vscode.window.createWebviewPanel( typingGame, 打字游戏, vscode.ViewColumn.One, { enableScripts: true, retainContextWhenHidden: true, } ); // 注入 VSCode API 到 webview panel.webview.html getWebviewContent(panel.webview); // 监听 webview 消息 panel.webview.onDidReceiveMessage( message { if (message.command save-stats) { context.globalState.update(typingStats, message.data); } } ); } function getWebviewContent(webview: vscode.Webview) { const scriptUri webview.asWebviewUri( vscode.Uri.joinPath(context.extensionUri, dist, renderer.js) ); return !DOCTYPE html html body div idapp/div script src${scriptUri}/script /body /html ; }关键点vscode.WebviewPanel是 VSCode 提供的内嵌浏览器它和 Electron 的BrowserWindow是两种完全不同的容器但都支持加载同一个renderer.js。这就是架构解耦的价值。4.5 开发 Electron 应用Main 进程与 Preload 的协同electron/main.js是 Electron 的心脏const { app, BrowserWindow, ipcMain, Menu, Tray } require(electron); const path require(path); const fs require(fs).promises; function createWindow() { const mainWindow new BrowserWindow({ width: 800, height: 600, webPreferences: { preload: path.join(__dirname, preload.js), contextIsolation: true, nodeIntegration: false, sandbox: true, // 更安全 }, }); if (process.env.NODE_ENV development) { mainWindow.loadURL(http://localhost:5173); // Vite 开发服务器 } else { mainWindow.loadFile(path.join(__dirname, ../dist-electron/index.html)); } return mainWindow; } app.whenReady().then(() { const mainWindow createWindow(); // IPC 处理 ipcMain.handle(save-user-data, async (event, key, data) { const userDataPath app.getPath(userData); await fs.writeFile(path.join(userDataPath, ${key}.json), JSON.stringify(data)); }); ipcMain.handle(load-user-data, async (event, key) { const userDataPath app.getPath(userData); try { const data await fs.readFile(path.join(userDataPath, ${key}.json), utf8); return JSON.parse(data); } catch (e) { return null; } }); // 菜单 const template [ { label: 打字游戏, submenu: [ { role: quit } ] } ]; Menu.setApplicationMenu(Menu.buildFromTemplate(template)); });preload.js是安全桥梁const { contextBridge, ipcRenderer } require(electron); contextBridge.exposeInMainWorld(electronAPI, { saveUserData: (key, data) ipcRenderer.invoke(save-user-data, key, data), loadUserData: (key) ipcRenderer.invoke(load-user-data, key), openUrl: (url) { // 安全校验 if (url.startsWith(https://) || url.startsWith(http://)) { ipcRenderer.invoke(open-external-url, url); } } });Main 进程里ipcMain.handle(open-external-url)用shell.openExternal()打开确保 URL 安全。4.6 调试技巧如何同时调试 VSCode 插件和 Electron RendererVSCode 插件调试在.vscode/launch.json里配{ version: 0.2.0, configurations: [ { name: Launch Extension, type: extensionHost, request: launch, args: [--extensionDevelopmentPath${workspaceFolder}], outFiles: [${workspaceFolder}/dist/**/*.js] } ] }Electron Renderer 调试在electron/main.js里加if (process.env.NODE_ENV development) { mainWindow.webContents.openDevTools(); }但这样只能调试 RendererMain 进程无法调试。终极方案是用 VSCode 的 Node.js 调试器附加到 Electron 主进程。在launch.json里加{ name: Debug Main Process, type: node, request: attach, port: 5858, address: localhost, sourceMaps: true, outFiles: [${workspaceFolder}/electron/**/*.js] }然后启动 Electron 时加--inspect5858参数。这样VSCode 里一个窗口调试插件另一个窗口调试 Electron Main 进程Renderer 用 DevTools三端同步调试。4.7 发布与分发自动化构建与版本管理我们用 GitHub Actions 实现 CI/CD。.github/workflows/release.ymlname: Release on: push: tags: - v* jobs: build: runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] steps: - uses: actions/checkoutv3 - name: Setup Node uses: actions/setup-nodev3 with: node-version: 18 - name: Install dependencies run: npm ci - name: Build VSCode extension run: npm run build:vscode - name: Build Electron app run: npm run build:electron - name: Upload artifacts uses: actions/upload-artifactv3 with: name: electron-${{ matrix.os }} path: release/Tag 推送后自动构建三端安装包并上传到 GitHub Releases。VSCode 插件用vsce publish命令发布到 Marketplace。整个流程无人值守从提交代码到用户下载12 分钟完成。5. 常见问题与排查技巧实录那些文档里找不到的坑5.1 “electron壳子内的页面打开url”失败白屏与 SecurityError网络热词里高频出现这个问题本质是 Electron 的安全策略。当你在 Renderer 进程里写window.open(https://google.com)会得到SecurityError: Blocked opening https://google.com/ in a new window because the request was made in a sandboxed frame without the allow-popups permission.。解决方案不是关 sandbox而是用electronAPI.openUrl()见 4.5 节并在preload.js里严格校验 URL 协议。我们曾遇到用户传入javascript:alert(1)导致 XSS所以openUrl函数里加了正则校验if (!/^https?:\/\//.test(url)) throw new Error(Invalid URL protocol)。5.2 macOS 上托盘图标不显示路径与格式的双重陷阱在 macOS 上new Tray(iconPath)如果iconPath是 PNG必须是 16x16 或 24x24且背景透明。我们第一次用 32x32 PNG图标显示为灰色方块。解决方案用icon-gen工具生成.icnsnpx icon-gen --input assets/icon.png --output build/icons然后在createTray()里const iconPath process.platform darwin ? path.join(__dirname
返回列表