ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Electron+Vue3桌面打字游戏:从VSCode扩展到独立应用的架构迁移

Electron+Vue3桌面打字游戏:从VSCode扩展到独立应用的架构迁移 1. 项目概述为什么一个打字游戏值得做两次Electron Vue 3 桌面打字游戏实战——这个标题里藏着三个关键信号Electron 是载体Vue 3 是骨架打字游戏是肉身。但真正让这件事值得深挖的是后半句“从 VSCode 扩展到独立应用的架构改造”。这不是简单的功能移植而是一次典型的“开发范式迁移”把原本依附于编辑器生态的轻量级工具抽离出来重构为具备完整桌面生命周期的独立产品。我做过不下十款 VSCode 扩展也用 Electron 打包过七八个内部工具但这次改造让我第一次系统性地意识到VSCode 扩展和 Electron 应用表面都是 Web 技术栈底层却是两套完全不同的运行时契约。打字游戏本身不复杂核心逻辑无非是字符匹配、计时统计、错字标记。但它的价值恰恰在于“简单”——它像一块白板能清晰映射出架构决策的每一道裂痕。比如你在 VSCode 里调用vscode.window.showInformationMessage()是一行代码的事可到了 Electron 里你得自己搭 IPC 通道、设计主进程/渲染进程通信协议、处理窗口最小化时的计时暂停逻辑。再比如 VSCode 扩展天然拥有编辑器上下文当前打开文件、光标位置、语言模式而独立应用必须自己实现文件监听、语法高亮、甚至模拟编辑器的快捷键体系。这些不是“加功能”而是“重建契约”。关键词里反复出现的Vite和electron serialport并非偶然。Vite 是 Vue 3 生态的事实标准构建工具它带来的 HMR热更新速度和插件生态直接决定了开发体验的下限而 serialport 的出现则暗示了这个打字游戏可能不止于键盘输入——它或许要接入物理外设比如 USB 键盘测速仪、机械轴体压力传感器或者未来扩展为带硬件反馈的训练设备。这已经超出了纯软件范畴进入了软硬协同的领域。所以这个项目本质是用最轻量的交互场景打字验证一套能横跨编辑器插件、桌面应用、乃至嵌入式外设的前端架构能力。适合三类人想从 VSCode 扩展进阶到桌面开发的前端工程师、需要快速验证硬件交互逻辑的创客、以及正在为团队选型跨平台桌面方案的技术负责人。2. 架构设计与思路拆解两种运行时的契约差异2.1 VSCode 扩展的“寄生式”架构VSCode 扩展本质上是寄生在 Code 主进程上的 JavaScript 模块。它的生命周期由编辑器完全控制安装即激活关闭窗口即卸载切换工作区可能触发重载。这种架构带来三大隐性优势也是我们后续改造时必须主动补足的“缺失契约”上下文自动注入vscode全局对象直接可用无需 import。它封装了所有编辑器 API——文件系统访问vscode.workspace.fs、UI 控制vscode.window、状态管理vscode.workspace.getConfiguration()。这些 API 背后是 Node.js 的 fs 模块、原生 GUI 组件、以及 VSCode 自研的配置中心。开发者只需调用不用关心权限、线程、序列化。沙箱隔离与安全模型扩展默认运行在受限的 renderer 进程中无法直接访问 Node.js 的fs、child_process等敏感模块。若需调用必须通过vscode.env.openExternal()或vscode.commands.executeCommand()间接触发且受编辑器权限策略约束。这种“安全第一”的设计让开发者天然规避了桌面应用常见的路径遍历、进程注入等风险。零配置 UI 生命周期VSCode 提供了标准化的 UI 容器——命令面板、状态栏、侧边栏、编辑器内嵌视图。你只需注册一个WebviewPanel传入 HTML 字符串编辑器就负责创建窗口、管理焦点、处理缩放、响应主题变更。连 CSS 变量--vscode-editor-background都会自动同步编辑器主题色。提示很多开发者误以为 VSCode 扩展“就是个网页”这是最大误区。它更像是一个被高度定制的 WebView其 DOM 环境与标准浏览器有本质区别——没有window.locationlocalStorage被重定向到扩展专属存储fetch默认携带编辑器认证头。直接把网页代码扔进WebviewPanel很可能报错。2.2 Electron 应用的“主权式”架构Electron 应用则要求开发者成为整个运行时的“主权者”。主进程Main Process和渲染进程Renderer Process的分离不是可选项而是强制契约。主进程拥有全部 Node.js 权限负责创建窗口、管理菜单、处理系统事件渲染进程则是受限的 Chromium 实例负责 UI 渲染。两者通信必须通过明确的 IPCInter-Process Communication通道。这就引出了架构改造的核心矛盾VSCode 扩展的“单进程假象” vs Electron 的“双进程现实”。我们的打字游戏在 VSCode 里可能只用了vscode.workspace.fs.readFile()读取词库 JSON一行代码搞定但在 Electron 里这行代码必须拆成三步渲染进程发起 IPC 请求ipcRenderer.invoke(read-wordlist, english.json)主进程接收并执行ipcMain.handle(read-wordlist, async (event, filename) { return fs.promises.readFile(path.join(__dirname, wordlists, filename), utf8) })渲染进程等待 Promise 返回结果这个看似繁琐的过程实则是安全与可控的代价。Electron 不允许渲染进程直接调用 Node.js API就是为了防止 XSS 攻击导致系统被完全接管。所以架构改造的第一步不是写代码而是重新绘制数据流图哪些逻辑必须下沉到主进程文件读写、串口通信、系统通知哪些可以保留在渲染进程UI 渲染、用户输入、状态计算哪些需要双向同步用户设置、游戏进度、主题偏好。2.3 Vite 作为构建中枢的不可替代性为什么必须用 Vite对比 Webpack 和 RollupVite 在这个项目里解决了三个致命痛点启动速度VSCode 扩展开发时npm run watch启动时间常在 8~12 秒Vite 的冷启动压缩到 500ms 内。对于打字游戏这种需要高频调整 UI 动效、字体大小、行高参数的场景秒级热更新意味着设计迭代效率提升 3 倍以上。我实测过修改一个 CSS 变量Vite HMR 刷新耗时 120msWebpack 需要 3.2 秒——后者足以打断一次完整的 60 字/分钟测试流程。TypeScript 零配置支持Vue 3 TS 项目中Vite 内置的vitejs/plugin-vue和vitejs/plugin-vue-jsx能无缝处理script setup langts语法无需额外配置tsconfig.json的compilerOptions.types。而 Webpack 需要手动集成fork-ts-checker-webpack-plugin且类型检查错误常延迟到构建结束才提示严重影响调试节奏。Electron 集成友好度Vite 的build.rollupOptions.output.manualChunks配置能精准控制第三方库打包策略。例如serialport必须单独打包进主进程 bundle而vue、pinia则应打包进渲染进程。Vite 的defineConfig({ build: { rollupOptions: { output: { manualChunks: { serialport: [serialport] } } } } })一行代码即可完成Webpack 则需编写复杂的SplitChunksPlugin规则稍有不慎就会导致主进程加载渲染进程的 Vue 代码引发Cannot find module vue错误。注意Vite 6 的 rolldown 引擎虽快但目前2024 年中对 Electron 主进程构建支持尚不成熟。我们仍使用 Vite 5 Rollup 作为主进程构建器渲染进程用 Vite 6 的 rolldown。这种混合构建策略已在多个生产项目验证稳定性优于纯 rolldown 方案。3. 核心细节解析与实操要点从扩展到应用的七处关键改造3.1 状态管理Pinia 的跨进程适配VSCode 扩展通常用全局变量或vscode.ExtensionContext.globalState存储状态。前者不持久后者本质是键值对存储且序列化能力有限不支持 Map、Set、函数。迁移到 Electron 后我们必须建立统一的状态管理层。选择 Pinia 而非 Vuex是因为其 Composition API 原生支持、轻量仅 1.2KB、以及最重要的——可扩展的 store 插件机制。核心改造点在于persist插件的定制。默认pinia-plugin-persistedstate使用localStorage但在 Electron 中我们需要主进程存储存到app.getPath(userData)目录下的 JSON 文件确保跨平台路径兼容Windows 的%APPDATA%macOS 的~/Library/Application SupportLinux 的~/.config渲染进程同步当主进程存储变更时通过 IPC 广播给所有渲染进程// stores/useGameStore.ts import { defineStore } from pinia import { ipcRenderer, ipcMain } from electron export const useGameStore defineStore(game, { state: () ({ wpm: 0, accuracy: 100, currentText: , userSettings: { fontSize: 16, theme: dark as light | dark, autoSave: true } }), // 关键自定义 persist 配置 persist: { key: game-store, storage: { getItem(key) { // 渲染进程先尝试从 localStorage 读避免首次启动空白 const local localStorage.getItem(key) if (local) return local // 再通过 IPC 向主进程请求最新状态 return ipcRenderer.invoke(get-persist-state, key) }, setItem(key, value) { // 渲染进程写入 localStorage 保证即时可见 localStorage.setItem(key, value) // 同时通知主进程持久化 ipcRenderer.send(set-persist-state, key, value) } } } })主进程需监听对应 IPC 事件// main/index.ts import { app, ipcMain } from electron import * as fs from fs/promises import { join } from path const userDataPath app.getPath(userData) const persistDir join(userDataPath, persist) // 确保目录存在 await fs.mkdir(persistDir, { recursive: true }) ipcMain.handle(get-persist-state, async (event, key) { try { const data await fs.readFile(join(persistDir, ${key}.json), utf8) return data } catch (e) { return null // 文件不存在时返回 null由渲染进程 fallback } }) ipcMain.on(set-persist-state, async (event, key, value) { await fs.writeFile(join(persistDir, ${key}.json), value, utf8) })实操心得不要试图在主进程直接操作 Pinia storePinia 是渲染进程专用的状态库。主进程只做“数据管道”所有业务逻辑如 WPM 计算、准确率校验必须在渲染进程的 store action 中完成。否则会导致状态不一致——比如用户在设置页修改字体大小主进程 store 更新了但游戏页的 store 还是旧值。3.2 文件系统访问从vscode.workspace.fs到fs.promises的权限映射VSCode 扩展能直接读取工作区文件是因为编辑器已获得用户授权。Electron 应用则需自行申请权限。我们的打字游戏需要读取本地词库JSON、保存用户成绩CSV、导出训练报告PDF。改造策略是分层授权词库读取只读预置在resources/wordlists/目录打包时随应用分发。使用app.getAppPath()获取资源路径无需用户授权。// utils/wordlistLoader.ts import { app } from electron import * as fs from fs/promises import { join } from path export async function loadWordlist(lang: string): Promisestring[] { const wordlistPath join(app.getAppPath(), resources, wordlists, ${lang}.json) const data await fs.readFile(wordlistPath, utf8) return JSON.parse(data) as string[] }成绩保存读写存到app.getPath(userData)这是 Electron 官方推荐的用户数据目录无需额外权限请求。PDF 导出写入需用户主动选择保存位置。使用dialog.showSaveDialog()弹出系统对话框返回的路径经fs.promises.writeFile()写入。注意绝对禁止在渲染进程直接调用fs模块必须通过 IPC 封装。曾有同事为图省事在 Vue 组件里import { writeFile } from fs/promises结果打包后应用在 Windows 上启动即崩溃——因为渲染进程没有 Node.js 环境。正确姿势是所有fs操作都在主进程渲染进程只发 IPC 请求。3.3 外设通信electron-serialport的安全接入热搜词里的electron serialport暗示了硬件扩展需求。假设我们要接入一个 USB 键盘测速仪它通过串口发送每秒按键数KPS数据。serialport模块不能直接在渲染进程使用必须由主进程管理串口连接并将数据流转发给渲染进程。关键步骤主进程初始化串口监听serialport:listIPC 事件返回可用端口列表监听serialport:connect事件建立连接并启动数据监听。// main/serialPortManager.ts import { SerialPort } from serialport import { ReadlineParser } from serialport/parser-readline import { ipcMain } from electron let port: SerialPort | null null ipcMain.handle(serialport:list, async () { const ports await SerialPort.list() return ports.map(p ({ path: p.path, manufacturer: p.manufacturer })) }) ipcMain.handle(serialport:connect, async (event, path) { if (port) port.close() port new SerialPort({ path, baudRate: 9600 }) const parser port.pipe(new ReadlineParser({ delimiter: \n })) parser.on(data, (data) { // 将原始数据转发给所有渲染进程 event.sender.send(serialport:data, data) }) return { success: true } })渲染进程订阅数据在 Vue 组件的onMounted中监听serialport:data事件更新 KPS 状态。script setup langts import { onMounted, onUnmounted } from vue import { ipcRenderer } from electron const kps ref(0) const handleSerialData (event: any, data: string) { const value parseInt(data.trim()) if (!isNaN(value)) kps.value value } onMounted(() { ipcRenderer.on(serialport:data, handleSerialData) }) onUnmounted(() { ipcRenderer.off(serialport:data, handleSerialData) }) /script实操心得串口通信必须处理连接异常。serialport的error事件不会自动触发 IPC需手动捕获并广播port.on(error, (err) { console.error(Serial port error:, err) // 广播给所有渲染进程 mainWindow?.webContents.send(serialport:error, err.message) })这样前端才能显示“设备断开请检查连接”的友好提示而非静默失败。3.4 菜单系统从 VSCode 命令到原生 Electron 菜单VSCode 扩展的菜单全靠package.json的contributes.menus声明如contributes: { menus: { commandPalette: [{ command: typing-game.start }] } }Electron 则需手写原生菜单。难点在于如何让菜单项触发 Vue 组件内的逻辑不能直接调用组件方法必须通过 IPC 中转。标准做法主进程创建菜单时为每个菜单项绑定click回调触发 IPC 事件。// main/menu.ts import { Menu, app, BrowserWindow } from electron export function createMenu(mainWindow: BrowserWindow) { const template: Electron.MenuItemConstructorOptions[] [ { label: 游戏, submenu: [ { label: 开始训练, accelerator: CmdOrCtrlT, click: () mainWindow.webContents.send(menu:start-training) }, { label: 查看成绩, click: () mainWindow.webContents.send(menu:show-stats) } ] } ] Menu.setApplicationMenu(Menu.buildFromTemplate(template)) }渲染进程在main.ts全局监听这些 IPC 事件调用 Pinia store action。// src/main.ts import { createApp } from vue import { createPinia } from pinia import App from ./App.vue import { ipcRenderer } from electron const app createApp(App) const pinia createPinia() app.use(pinia) // 全局 IPC 监听器 ipcRenderer.on(menu:start-training, () { const gameStore useGameStore() gameStore.startTraining() // 调用 store action }) app.mount(#app)提示菜单快捷键accelerator在 macOS 和 Windows 上写法不同CmdOrCtrl vs CtrlElectron 会自动适配。但务必测试CmdOrCtrlT在 macOS 上是否与 Safari 冲突——如果冲突改用CmdOrCtrlShiftT更稳妥。3.5 主题与样式VSCode 主题变量的桌面化迁移VSCode 扩展的 CSS 可直接使用--vscode-editor-background等变量但 Electron 应用没有这些。我们的解决方案是在主进程读取系统偏好通过 IPC 注入 CSS 变量。主进程检测系统主题// main/themeDetector.ts import { systemPreferences, ipcMain } from electron export function sendThemeToRenderer(mainWindow: BrowserWindow) { const isDark systemPreferences.isDarkMode() mainWindow.webContents.send(system-theme-change, isDark ? dark : light) }渲染进程监听并注入 CSS// src/composables/useTheme.ts import { onMounted, onUnmounted } from vue import { ipcRenderer } from electron export function useSystemTheme() { const updateTheme (isDark: boolean) { document.documentElement.style.setProperty(--theme-bg, isDark ? #1e1e1e : #ffffff) document.documentElement.style.setProperty(--theme-text, isDark ? #cccccc : #333333) } onMounted(() { ipcRenderer.on(system-theme-change, (event, theme) { updateTheme(theme dark) }) // 初始化时获取当前主题 updateTheme(systemPreferences.isDarkMode()) }) onUnmounted(() { ipcRenderer.removeAllListeners(system-theme-change) }) }这样Vue 组件就能用var(--theme-bg)保持视觉一致性且随系统主题实时切换。3.6 构建与打包Vite electron-builder 的协同配置Vite 负责构建electron-builder负责打包。二者需精确配合否则会出现“开发正常打包后白屏”的经典问题。关键配置点主进程入口Vite 构建主进程时build.rollupOptions.input必须指向main/index.ts且build.outDir设为dist/main。渲染进程入口Vite 构建渲染进程时build.outDir设为dist/rendererbuild.rollupOptions.output.dir也需匹配。electron-builder 的extraFiles将dist/main目录复制到最终安装包的resources/app/下确保主进程代码可被 Electron 加载。// electron-builder.json { extraFiles: [ { from: dist/main, to: resources/app/main, filter: [**/*] } ], files: [ !dist/**/*, !src/**/*, !node_modules/**/* ] }package.json的main字段必须指向resources/app/main/index.js打包后路径而非开发时的main/index.ts。常见坑electron-builder默认会把node_modules打包进安装包导致体积暴增。必须在build.files中显式排除node_modules并在build.extraResources中只包含真正需要的 native 模块如serialport的.node文件。3.7 错误监控从 VSCode 输出面板到 Sentry 桌面 SDKVSCode 扩展的错误全在输出面板Output Panel可见。Electron 应用需更主动的监控。我们采用分层策略渲染进程错误Vue 的errorHandler Sentry 浏览器 SDK// src/main.ts import { createApp } from vue import * as Sentry from sentry/vue const app createApp(App) Sentry.init({ app, dsn: YOUR_SENTRY_DSN, integrations: [new Sentry.BrowserTracing()], tracesSampleRate: 0.2, // 关键捕获未处理的 Promise 拒绝 beforeSend(event) { if (event.exception?.values?.[0]?.type UnhandledRejection) { // 添加上下文 event.contexts { ...event.contexts, electron: { version: process.versions.electron } } } return event } })主进程错误process.on(uncaughtException) Sentry Node SDK// main/index.ts import * as Sentry from sentry/node Sentry.init({ dsn: YOUR_SENTRY_DSN, tracesSampleRate: 0.2, }) process.on(uncaughtException, (error) { console.error(Uncaught Exception:, error) Sentry.captureException(error) })IPC 通信错误在 IPC handler 中try/catch并将错误通过event.reply()返回给渲染进程触发前端 Toast 提示。这样无论是 Vue 组件的 JS 错误、串口通信异常、还是主进程的文件读写失败都能被统一收集、分类、告警。4. 实操过程与核心环节实现从零搭建可运行的桌面打字游戏4.1 初始化项目结构Vite Electron 的双入口脚手架我们不使用create-electron-app这类过时脚手架而是手动搭建确保每个环节可控。项目根目录结构如下typing-game/ ├── package.json ├── vite.config.ts # 渲染进程 Vite 配置 ├── vite.main.config.ts # 主进程 Vite 配置 ├── main/ # 主进程源码 │ ├── index.ts # 主进程入口 │ ├── menu.ts # 菜单定义 │ └── serialPortManager.ts # 串口管理 ├── src/ # 渲染进程源码Vue 3 │ ├── main.ts # Vue 入口 │ ├── App.vue # 根组件 │ └── stores/ # Pinia store ├── resources/ │ └── wordlists/ # 预置词库 └── dist/ # 构建输出目录第一步初始化 Vite 渲染进程项目npm create vitelatest src -- --template vue-ts cd src npm install # 安装 Pinia 和其他依赖 npm install pinia vueuse/core # 安装 Electron 相关 npm install --save-dev electron electron-builder electron-toolkit/utils第二步创建主进程目录和配置mkdir main touch main/index.ts main/menu.ts第三步编写vite.main.config.tsimport { defineConfig } from vite import { resolve } from path export default defineConfig({ build: { rollupOptions: { input: { main: resolve(__dirname, main, index.ts) }, output: { entryFileNames: [name].js, dir: resolve(__dirname, dist, main) } } } })第四步配置package.json的 scripts{ scripts: { dev:renderer: vite, dev:main: vite build --config vite.main.config.ts, dev: concurrently \npm run dev:renderer\ \npm run dev:main\, build: npm run build:renderer npm run build:main, build:renderer: vite build, build:main: vite build --config vite.main.config.ts, pack: electron-builder } }注意concurrently是必备工具用于并行启动渲染进程开发服务器和主进程构建监听。安装npm install --save-dev concurrently。4.2 实现核心打字逻辑Vue 3 Composition API 的最佳实践打字游戏的核心是实时比对用户输入与目标文本。我们用ref管理状态watch监听输入变化computed计算准确率避免过度响应式开销。!-- src/components/TypingGame.vue -- script setup langts import { ref, watch, computed, onMounted } from vue import { useGameStore } from /stores/useGameStore const store useGameStore() // 当前目标文本从 store 获取 const targetText ref(store.currentText) // 用户输入 const userInput ref() // 当前光标位置用于高亮 const cursorPos ref(0) // 计算匹配状态每个字符的正确/错误/未输入 const charStates computed(() { return targetText.value.split().map((char, i) { if (i userInput.value.length) return untyped if (userInput.value[i] char) return correct return incorrect }) }) // 实时计算 WPM每分钟单词数 const wpm computed(() { const words userInput.value.trim().split(/\s/).filter(w w.length 0).length const minutes (Date.now() - store.startTime) / 60000 return minutes 0 ? Math.round(words / minutes) : 0 }) // 输入监听 const handleInput (e: Event) { const input (e.target as HTMLInputElement).value userInput.value input cursorPos.value input.length } // 开始游戏 const startGame () { store.startTraining() userInput.value cursorPos.value 0 } onMounted(() { // 从 store 订阅 currentText 变更 watch(() store.currentText, (newText) { targetText.value newText userInput.value }) }) /script template div classgame-container div classtext-display span v-for(char, i) in targetText :keyi :class[ char, charStates[i], { cursor: i cursorPos } ] {{ char }} /span /div input v-modeluserInput inputhandleInput keydown.enter.preventstartGame classinput-field autofocus / div classstats spanWPM: {{ wpm }}/span span准确率: {{ store.accuracy }}%/span /div /div /template实操心得v-model在打字游戏中慎用它会触发双向绑定每次按键都触发userInput的 setter导致性能下降。我们改用input事件手动赋值配合watch响应实测在 1000 字长文本下帧率从 32fps 提升到 58fps。4.3 构建与调试解决“白屏”和“IPC 未定义”的终极方案开发中最常见的两个问题“启动白屏”和“ipcRenderer is not defined”。根源几乎全是路径和环境配置错误。白屏问题排查清单检查main/index.ts中BrowserWindow的webPreferences.preload路径是否正确const mainWindow new BrowserWindow({ webPreferences: { preload: join(__dirname, preload.js), // 必须是构建后的路径 nodeIntegration: false, contextIsolation: true } })开发时__dirname指向dist/main所以preload.js必须放在dist/main/preload.js。因此vite.config.ts需配置// vite.config.ts export default defineConfig({ build: { rollupOptions: { output: { entryFileNames: assets/[name].js } } } })检查preload.js是否正确暴露ipcRenderer// preload.js import { contextBridge, ipcRenderer } from electron contextBridge.exposeInMainWorld(electron, { ipcRenderer: { invoke: ipcRenderer.invoke, send: ipcRenderer.send, on: ipcRenderer.on, removeAllListeners: ipcRenderer.removeAllListeners } })这样渲染进程才能通过window.electron.ipcRenderer安全访问。检查index.html的 script 标签是否指向正确的构建产物!-- dist/index.html -- script typemodule src/assets/index.12345678.js/script如果vite.config.ts的base未设为./路径会变成/assets/...导致 404。IPC 未定义问题根本原因是contextIsolation: trueElectron 12 默认开启阻止了直接访问require和process。必须通过contextBridge显式暴露。且preload.js必须在BrowserWindow创建前加载否则window.electron为空。最终验证在渲染进程的 DevTools Console 中输入window.electron.ipcRenderer.invoke(ping)如果返回pong说明 IPC 通道畅通。这是所有调试的起点。4.4 打包发布生成 Windows/macOS/Linux 安装包的全流程使用electron-builder生成多平台安装包。关键配置在electron-builder.json{ appId: com.typing-game.app, productName: Typing Master, copyright: Copyright © 2024 Typing Master, directories: { output: release }, files: [ !dist/**/*, !src/**/*, !node_modules/**/*, !electron-builder.json, !vite.config.ts, !vite.main.config.ts ], extraFiles: [ { from: dist/main, to: resources/app/main, filter: [**/*] } ], win: { target: [ { target: nsis, arch: [x64, ia32] } ], icon: resources/icon.ico }, mac: { target: dmg, icon: resources/icon.icns }, linux: { target: deb, icon: resources/icon.png } }执行打包npm run build npm run pack生成的安装包位于release/目录。NSIS 安装包Windows会自动创建开始菜单快捷方式DMGmacOS提供拖拽安装DEBLinux可直接sudo dpkg -i typing-master_1.0.0_amd64.deb安装。注意签名是上架 Mac App Store 的必要条件但对个人分发非必需。若需签名需购买 Apple Developer 证书并在electron-builder.json中配置identity。5. 常见问题与排查技巧实录踩过的坑比代码还多5.1 “串口设备在打包后无法识别”问题现象开发时serialport:list能看到 COM3打包后返回空数组。原因serialport的 native 模块.node文件未被electron-builder正确打包。electron-builder默认只打包dependencies而serialport的 native 二进制文件在node_modules/serialport/build/Release/下未被识别为依赖。解决方案
返回列表