面试题手册

梳理高频技术问题,帮助你按主题复习和查漏补缺。

服务端阅读 06月5日 19:59

Electron 架构详解:Chromium + Node.js 如何协同工作

Electron 让你用 JavaScript、HTML、CSS 写桌面应用——VS Code、Discord、Slack 都是它做的。它不是什么黑科技,本质就是把 Chromium(浏览器内核)和 Node.js(服务端运行时)打包在一起,让 Web 页面能调用系统级 API。三层架构Electron 的架构可以拆成三层来看:┌──────────────────────────────────────────────┐│ 你的应用代码 │├──────────────────┬───────────────────────────┤│ Chromium │ Node.js ││ (渲染 UI) │ (系统 API、文件、网络) │├──────────────────┴───────────────────────────┤│ 操作系统 (Windows / macOS / Linux) │└──────────────────────────────────────────────┘Chromium:负责把 HTML/CSS/JS 渲染成用户看到的界面。Electron 内嵌了完整的 Chromium,所以你的应用就是一个独立的浏览器窗口Node.js:负责和操作系统打交道——读写文件、创建子进程、访问网络、操作剪贴板等。Electron 内嵌了完整的 Node.js 运行时你的代码:运行在上述两个环境之上,通过 Electron 提供的 API 把两者串联起来这就是 Electron 的核心卖点:一个代码库同时拥有浏览器的渲染能力和 Node.js 的系统能力,编译一次就能跑在 Windows、macOS、Linux 上。多进程模型Electron 继承了 Chromium 的多进程架构。这不是随意设计——Chromium 之所以用多进程,是因为单个网页崩溃不应该拖垮整个浏览器。Electron 同理:一个窗口崩了,其他窗口和主进程还能继续工作。主进程 (Node.js)├── 渲染进程 1 (Chromium) → 窗口 A├── 渲染进程 2 (Chromium) → 窗口 B├── 渲染进程 3 (Chromium) → 窗口 C└── GPU 进程 (共享)主进程应用入口,package.json 的 main 字段指向的文件运行在 Node.js 环境中,可以使用 fs、path、child_process 等所有 Node.js 模块负责创建 BrowserWindow、管理应用生命周期(app.whenReady()、app.quit())处理原生对话框、系统菜单、托盘图标渲染进程每个 BrowserWindow 对应一个独立的渲染进程运行在 Chromium 环境中,就是一个完整的网页运行环境默认不能直接使用 Node.js API(安全原因),需要通过 preload 脚本桥接GPU 进程Chromium 自动管理,负责图形渲染(硬件加速)一般不需要手动干预,但如果 GPU 加速导致问题,可以通过 app.disableHardwareAcceleration() 关闭两个运行时怎么协作Chromium 和 Node.js 各自有一套事件循环。Electron 在底层把它们整合到了一起:Node.js 的 libuv 事件循环 和 Chromium 的消息循环 被合并,两个环境的 I/O 和定时器可以互不阻塞地运行你可以在同一个 JavaScript 上下文中既用 setTimeout(浏览器 API)又用 fs.readFile(Node.js API)但在渲染进程中,出于安全考虑,这种融合是被限制的——contextIsolation 默认开启,Node.js API 被隔离。你只能通过 preload 脚本和 IPC 通信来间接使用系统能力。IPC 通信机制主进程和渲染进程之间通过 IPC(进程间通信)传递消息:// 主进程ipcMain.handle('read-config', async () => { const data = await fs.promises.readFile('config.json', 'utf-8') return JSON.parse(data)})// 渲染进程(通过 preload 暴露的 API)const config = await window.electronAPI.readConfig()通信是异步的,数据经过结构化克隆序列化——所以不能传函数、DOM 节点或循环引用的对象。Electron 的优势和代价优势跨平台:一套代码跑三个系统,开发成本远低于分别写原生应用前端友好:会写网页就能写桌面应用,团队不需要学 Swift / C# / Qt生态丰富:npm 上所有包都能用,Web 社区的 UI 库(React、Vue)直接拿来用自动更新:内置 autoUpdater,不需要自己实现增量更新代价包体积大:内嵌 Chromium + Node.js,最小打包也要 100MB+内存占用高:每个渲染进程就是一个 Chromium 标签页,开多窗口内存涨得很快性能不如原生:JS 的执行效率和内存管理比 C++/Rust 差,CPU 密集型任务会卡顿启动慢:冷启动需要加载 Chromium 和 Node.js,比原生应用慢这些代价不是"优化一下就好了"——它们是 Electron 架构的固有特征。如果你的应用对包体积、内存或启动速度有硬性要求(比如轻量工具类应用),Electron 可能不是最佳选择。Tauri(Rust + WebView)和 Wails(Go + WebView)是更轻量的替代方案。什么时候该用 Electron| 适合 | 不适合 ||------|--------|| 内容型应用(编辑器、笔记、聊天) | 轻量工具(计算器、截图) || 需要丰富 UI 的桌面端 | 对启动速度极敏感 || 团队是前端为主 | 需要极低内存占用 || 需要快速跨平台上线 | 大量原生系统集成 |Electron 最大的价值是降低桌面应用的开发门槛。如果团队已经会写 Web 应用,Electron 能让你们在几周内交付一个可用的桌面端——这个效率优势是原生开发无法比拟的。
服务端阅读 06月5日 19:58

Electron 主进程 vs 渲染进程:职责、API 和通信机制全对比

Electron 应用跑起来后,打开任务管理器你会看到好几个进程——一个主进程加上若干渲染进程。它们各司其职,也各有局限,搞不清谁负责什么,写出的代码就会出各种诡异 bug。一句话区分主进程:管窗口、管系统、管生命周期,是整个应用的"大管家"渲染进程:管页面、管 UI、管用户交互,是每个窗口的"画师"主进程只有一个,渲染进程可以有多个(每个 BrowserWindow 一个)。运行环境对比| | 主进程 | 渲染进程 ||---|---|---|| 运行环境 | Node.js | Chromium(浏览器) || 可用 API | 全部 Node.js API + Electron 主进程 API | Web API + 部分通过 preload 暴露的 Node.js API || 进程数量 | 1 个 | 每个窗口 1 个,相互隔离 || 职责 | 窗口管理、系统交互、原生菜单/对话框 | 渲染 UI、处理用户交互 || 能直接操作 DOM 吗 | 不能 | 能 || 能直接读文件吗 | 能 | 不能(需要通过 IPC 请求主进程) || 默认能访问 Node.js 吗 | 能 | 不能(contextIsolation 默认开启) |这个表基本说明了一切:主进程有系统权限但不会画界面,渲染进程会画界面但没系统权限。两者必须协作。主进程做什么主进程是 Electron 应用的入口,package.json 的 main 字段指向的那个文件就是主进程代码:// main.js - 主进程const { app, BrowserWindow, ipcMain, dialog } = require('electron')let mainWindowapp.whenReady().then(() => { mainWindow = new BrowserWindow({ webPreferences: { preload: path.join(__dirname, 'preload.js'), contextIsolation: true, nodeIntegration: false // 安全:不让渲染进程直接用 Node.js } }) mainWindow.loadFile('index.html')})// 处理系统级操作ipcMain.handle('open-file-dialog', async () => { const result = await dialog.showOpenDialog({ properties: ['openFile'] }) return result.filePaths[0]})主进程的核心职责:创建和管理窗口:new BrowserWindow() 只能在主进程调用控制应用生命周期:app.whenReady()、app.quit() 等事件系统级交互:文件对话框、系统菜单、托盘图标、通知IPC 中转:处理渲染进程发来的请求,或协调多个窗口间的通信渲染进程做什么每个 BrowserWindow 加载的网页就运行在一个独立的渲染进程里:// renderer.js - 渲染进程(通过 preload 暴露的 API 与主进程通信)const filePath = await window.electronAPI.openFileDialog()document.getElementById('file-path').textContent = filePath渲染进程的核心职责:渲染 UI:HTML + CSS + JavaScript,和写网页一模一样处理用户交互:按钮点击、表单提交、滚动等数据展示:从主进程获取数据后渲染到界面渲染进程之间是相互隔离的——窗口 A 的渲染进程不能直接访问窗口 B 的 DOM 或变量,必须通过主进程中转。preload 脚本:两个世界之间的桥梁渲染进程默认无法访问 Node.js API(这是安全设计),但很多场景又确实需要调用系统功能。preload 脚本就是解决这个问题的:// preload.js - 运行在渲染进程的上下文中,但有 Node.js 访问权限const { contextBridge, ipcRenderer } = require('electron')contextBridge.exposeInMainWorld('electronAPI', { openFileDialog: () => ipcRenderer.invoke('open-file-dialog'), readFile: (path) => ipcRenderer.invoke('read-file', path), onSave: (callback) => { ipcRenderer.on('trigger-save', (event, data) => callback(data)) }})preload 的执行时机很特殊:它在渲染进程的网页加载之前运行,既可以用 Node.js API,又可以访问渲染进程的 window 对象。contextBridge.exposeInMainWorld 把方法安全地挂到 window 上,让网页代码能调用。常见的坑在主进程里操作 DOM不行。主进程没有 DOM 环境。如果你想修改 UI,必须在渲染进程里操作,或者通过 IPC 通知渲染进程去改。在渲染进程里直接 require('fs')默认不行。nodeIntegration 默认关闭,contextIsolation 默认开启——这是 Electron 的安全最佳实践。需要读文件时,通过 preload 暴露方法,让主进程来读。多窗口间的全局状态渲染进程之间不共享内存。如果你需要一个全局状态(比如用户登录信息),有三种方案:主进程做状态中心:所有窗口通过 IPC 读写主进程中的变量LocalStorage/IndexDB:同一 origin 下共享,但只适合简单场景共享内存/文件:性能要求高时使用渲染进程崩溃不影响主进程这是一个设计优势——某个网页崩溃了,主进程可以检测到并重新创建窗口,整个应用不会挂掉:mainWindow.webContents.on('crashed', () => { mainWindow.destroy() mainWindow = new BrowserWindow({ ... }) mainWindow.loadFile('index.html')})进程架构图┌─────────────────────────────────┐│ 主进程 (1个) ││ app / BrowserWindow / ipcMain ││ 文件系统 / 系统对话框 / 原生菜单 │└──────────┬──────────────────────┘ │ IPC 通信 ┌──────┴──────┬──────────────┐ ▼ ▼ ▼┌────────┐ ┌────────┐ ┌────────┐│渲染进程1│ │渲染进程2│ │渲染进程3││窗口 A │ │窗口 B │ │窗口 C ││DOM/JS │ │DOM/JS │ │DOM/JS │└────────┘ └────────┘ └────────┘理解了这个架构,就明白了 Electron 开发的核心原则:系统操作在主进程,UI 操作在渲染进程,两者通过 IPC 通信。
服务端阅读 06月5日 19:54

Electron IPC 通信实战:send、invoke 和 contextBridge 用法对比

Electron 的主进程和渲染进程跑在不同的环境里——一个有 Node.js 权限,一个只有浏览器能力。它们要协作,就得靠 IPC(进程间通信)。理解 IPC 的几种通信模式和它们的取舍,是写好 Electron 应用的基本功。IPC 通信的三种模式1. send/on —— 单向通知,不等回复渲染进程向主进程"喊一声",不等回应:// 渲染进程const { ipcRenderer } = require('electron')ipcRenderer.send('log:message', { level: 'info', text: '用户点击了导出按钮' })// 主进程const { ipcMain } = require('electron')ipcMain.on('log:message', (event, data) => { logger.log(data.level, data.text)})主进程也可以主动向渲染进程推消息:// 主进程mainWindow.webContents.send('update:progress', { percent: 75 })// 渲染进程ipcRenderer.on('update:progress', (event, data) => { progressBar.style.width = data.percent + '%'})适用场景:日志记录、状态通知、进度更新——发送方不需要知道处理结果。2. invoke/handle —— 请求-响应,现代写法渲染进程发起请求,主进程处理后返回结果:// 渲染进程const filePath = await ipcRenderer.invoke('dialog:openFile')console.log('选择的文件:', filePath)// 主进程const { dialog } = require('electron')ipcMain.handle('dialog:openFile', async () => { const result = await dialog.showOpenDialog({ properties: ['openFile'] }) return result.filePaths[0] || null})invoke 返回 Promise,handle 里可以写异步逻辑。这比老式的 send + event.reply 干净太多,是 Electron 官方推荐的双向通信方式。为什么比 send/on 好:代码更简洁——不用手动配对 send 和 on天然支持 async/await错误能通过 catch 捕获,不会被静默吞掉适用场景:需要主进程执行操作并返回结果——打开文件对话框、读写本地文件、调用系统 API。3. send + event.reply —— 老式双向通信Electron 7 之前的双向通信方式:// 渲染进程ipcRenderer.send('get-file-content', { path: '/data/config.json' })ipcRenderer.on('file-content-response', (event, data) => { console.log(data)})// 主进程ipcMain.on('get-file-content', (event, data) => { const content = fs.readFileSync(data.path, 'utf-8') event.reply('file-content-response', content)})现在不建议用了——invoke/handle 是更好的替代。event.reply 的主要问题是:渲染进程需要额外监听一个回调通道,代码更散乱,也不支持 async/await。安全:contextBridge 是必须的从 Electron 12 开始,contextIsolation 默认开启。这意味着渲染进程的 JavaScript 不能直接 require('electron')——必须通过 preload 脚本和 contextBridge 安全地暴露 API。// preload.jsconst { contextBridge, ipcRenderer } = require('electron')contextBridge.exposeInMainWorld('electronAPI', { // 只暴露方法,不暴露 ipcRenderer 对象本身 openFile: () => ipcRenderer.invoke('dialog:openFile'), readFile: (path) => ipcRenderer.invoke('file:read', path), onProgress: (callback) => { const handler = (event, data) => callback(data) ipcRenderer.on('update:progress', handler) return () => ipcRenderer.removeListener('update:progress', handler) }})// 渲染进程(普通网页环境)const filePath = await window.electronAPI.openFile()const content = await window.electronAPI.readFile(filePath)// 监听进度const cleanup = window.electronAPI.onProgress((data) => { updateUI(data.percent)})// 组件卸载时清理cleanup()安全原则永远不要把整个 ipcRenderer 暴露给渲染进程——只暴露具体方法通道名用命名空间:dialog:openFile 比 openFile 更不容易冲突主进程必须验证输入——渲染进程的代码可能被 XSS 篡改,不能信任传来的数据窗口间通信多个 BrowserWindow 之间不能直接通信,需要主进程中转:// 主进程作为消息中转ipcMain.on('message:forward', (event, data) => { // 转发给所有其他窗口 BrowserWindow.getAllWindows().forEach(win => { if (win.webContents !== event.sender) { win.webContents.send('message:broadcast', data) } })})或者用 MessagePort 建立两个渲染进程之间的直接通道:// 主进程:给两个窗口搭桥const { MessageChannelMain } = require('electron')ipcMain.on('setup-channel', (event) => { const [port1, port2] = new MessageChannelMain() // port1 给请求方 event.sender.postMessage('channel-established', null, [port1]) // port2 给目标窗口 targetWindow.webContents.postMessage('channel-established', null, [port2])})// 渲染进程 AipcRenderer.on('channel-established', (event) => { const port = event.ports[0] port.postMessage({ type: 'hello', from: 'window-a' }) port.onmessage = (e) => console.log('Received:', e.data)})MessagePort 的优势是两个窗口直接通信,不经过主进程中转,延迟更低。适合窗口间需要频繁交互的场景(比如编辑器的主窗口和面板窗口)。IPC 性能优化批量发送,别逐条发// 差:1000 次 IPC 调用items.forEach(item => ipcRenderer.send('process', item))// 好:1 次 IPC 调用ipcRenderer.invoke('process-batch', items)每次 IPC 调用都有序列化和跨进程传输的开销。批量处理能减少调用次数,显著提升性能。大数据走文件系统或共享内存IPC 传输大量数据(比如图片、大文件内容)会很慢,因为要经过结构化克隆序列化。正确的做法是:// 渲染进程请求文件路径,自己读取const filePath = await ipcRenderer.invoke('file:getPath')// 用 Node.js(preload 暴露的方法)读取文件const content = await window.electronAPI.readFile(filePath)// 或者用共享内存const sharedBuffer = await ipcRenderer.invoke('buffer:getShared')耗时任务用 Worker Threads不要在主进程里跑 CPU 密集任务——会阻塞所有窗口的 IPC 处理:// 主进程const { Worker } = require('worker_threads')ipcMain.handle('heavy-task', async (event, data) => { return new Promise((resolve, reject) => { const worker = new Worker('./heavy-worker.js', { workerData: data }) worker.on('message', resolve) worker.on('error', reject) })})TypeScript 类型安全给 IPC 通道加上类型定义,能在编译时捕获参数错误:// ipc-types.tsexport interface IpcChannels { 'dialog:openFile': { args: void; result: string | null } 'file:read': { args: { path: string }; result: string } 'log:message': { args: { level: string; text: string }; result: void }}// preload.tscontextBridge.exposeInMainWorld('electronAPI', { invoke: <K extends keyof IpcChannels>( channel: K, ...args: IpcChannels[K]['args'] extends void ? [] : [IpcChannels[K]['args']] ): Promise<IpcChannels[K]['result']> => ipcRenderer.invoke(channel, ...args)})这样渲染进程调用 window.electronAPI.invoke('file:read', { path: 123 }) 时,TypeScript 会报错——path 应该是 string 不是 number。IPC 通道设计清单| 检查项 | 说明 ||--------|------|| 通道名有命名空间 | file:read 而不是 readFile || 用 invoke/handle 不用 send/reply | 更安全、更简洁 || contextBridge 只暴露方法 | 不暴露 ipcRenderer 对象 || 主进程验证输入 | 不信任渲染进程的数据 || 大数据不走 IPC | 走文件系统或共享内存 || 耗时任务用 Worker | 不阻塞主进程 || 监听器记得清理 | 组件卸载时 removeListener |
服务端阅读 06月4日 14:01

Electron打包分发:签名、公证、自动更新和体积优化

Electron 应用写完了不算完——打包、签名、分发、自动更新,每一步都有坑。Windows 上没签名的安装包会被 SmartScreen 拦截,macOS 上没公证的应用直接打不开,安装包体积动辄 150MB+ 用户嫌大。这篇文章把打包到分发的完整流程走一遍。打包工具选择| 工具 | 特点 | 适合谁 ||------|------|--------|| electron-builder | 功能最全,签名+更新+多格式一步到位 | 生产环境首选 || electron-forge | 官方推荐,集成开发+打包+发布流程 | 新项目开箱即用 || electron-packager | 只打包不安装包,功能简单 | 只需要可执行文件 |大部分项目选 electron-builder 就对了。electron-builder 配置npm install --save-dev electron-builder在 package.json 里配置:{ "build": { "appId": "com.yourcompany.yourapp", "productName": "YourApp", "directories": { "output": "dist" }, "files": [ "build/**/*", "node_modules/**/*", "package.json" ], "win": { "target": [{ "target": "nsis", "arch": ["x64"] }], "icon": "build/icon.ico" }, "mac": { "target": [{ "target": "dmg", "arch": ["x64", "arm64"] }], "icon": "build/icon.icns", "category": "public.app-category.productivity", "hardenedRuntime": true, "gatekeeperAssess": false, "entitlements": "build/entitlements.mac.plist", "entitlementsInherit": "build/entitlements.mac.plist" }, "linux": { "target": ["AppImage", "deb"], "icon": "build/icon.png", "category": "Utility" } }}打包命令:npx electron-builder --win # Windowsnpx electron-builder --mac # macOSnpx electron-builder --linux # Linuxnpx electron-builder -mwl # 全平台(需要在对应系统上跑)NSIS 安装程序(Windows)NSIS 是 Windows 上最常用的安装包格式:{ "build": { "nsis": { "oneClick": false, "allowToChangeInstallationDirectory": true, "createDesktopShortcut": true, "createStartMenuShortcut": true, "shortcutName": "YourApp", "uninstallDisplayName": "YourApp", "license": "LICENSE.txt" } }}oneClick: false 让用户选择安装目录,而不是一闪而过安装完。createDesktopShortcut 看似方便,但很多用户反感桌面图标——建议设为 alwaysCreate: false 让用户自己勾选。代码签名不签名的应用会被操作系统拦截:Windows 的 SmartScreen 弹蓝框,macOS 的 Gatekeeper 直接说"无法验证开发者"。Windows 签名需要购买代码签名证书(EV 或 Standard)。EV 证书签名后 SmartScreen 立即信任,Standard 证书需要积累信誉。# 环境变量方式(CI/CD 推荐)export CSC_LINK=path/to/certificate.pfxexport CSC_KEY_PASSWORD=your-passwordnpx electron-builder --winelectron-builder 检测到 CSC_LINK 环境变量后自动签名,不用额外配置。macOS 签名和公证macOS 要求应用同时签名和公证(Notarization),否则用户打开时会弹"无法验证开发者"。签名需要 Apple Developer 证书,公证需要 Apple ID:export CSC_LINK=path/to/developer-id.p12export CSC_KEY_PASSWORD=your-passwordexport APPLE_ID=your@email.comexport APPLE_APP_SPECIFIC_PASSWORD=xxxx-xxxx-xxxx-xxxxexport APPLE_TEAM_ID=XXXXXXXXXXnpx electron-builder --macelectron-builder 在 mac.hardenedRuntime: true 的情况下会自动签名并提交公证。公证过程需要 1-5 分钟,期间应用无法分发。entitlements.mac.plist 文件(声明权限):<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"><plist version="1.0"><dict> <key>com.apple.security.cs.allow-jit</key> <true/> <key>com.apple.security.cs.allow-unsigned-executable-memory</key> <true/> <key>com.apple.security.cs.allow-dyld-environment-variables</key> <true/></dict></plist>Electron 需要这三个权限:JIT(V8 引擎)、unsigned memory(渲染进程)、dyld variables(native 模块加载)。不声明的话签名后应用会崩溃。自动更新electron-updater 是 electron-builder 配套的自动更新方案,支持差分更新(只下载变化部分):npm install electron-updaterconst { autoUpdater } = require('electron-updater')const log = require('electron-log')autoUpdater.logger = logautoUpdater.autoDownload = false // 不自动下载,先提示用户app.whenReady().then(() => { autoUpdater.checkForUpdates() autoUpdater.on('update-available', (info) => { // 通知用户有新版本 dialog.showMessageBox({ type: 'info', title: '发现新版本', message: `新版本 ${info.version} 可用,是否现在下载?`, buttons: ['下载', '稍后'] }).then(({ response }) => { if (response === 0) autoUpdater.downloadUpdate() }) }) autoUpdater.on('update-downloaded', () => { dialog.showMessageBox({ type: 'info', title: '更新就绪', message: '新版本已下载,重启应用以完成安装。', buttons: ['立即重启', '稍后'] }).then(({ response }) => { if (response === 0) autoUpdater.quitAndInstall() }) })})更新源配置在 package.json:{ "build": { "publish": { "provider": "github", "owner": "your-username", "repo": "your-repo" } }}支持 GitHub Releases、S3、通用 HTTP 服务器。发布新版本时,electron-builder 自动把安装包上传到 GitHub Releases,autoUpdater 检查 latest.yml 判断是否有更新。体积优化Electron 应用默认 150MB+,因为包含了完整的 Chromium。可以压缩:排除不需要的文件{ "build": { "files": [ "build/**/*", "!build/samples/**/*", "node_modules/**/*", "!node_modules/*/test/**/*", "!node_modules/*/docs/**/*", "!node_modules/*.md" ] }}! 开头表示排除。test、docs、README 等文件打包后不需要。asar 归档asar 把源码打包成只读归档,减少文件数量和体积:{ "build": { "asar": true, "asarUnpack": [ "node_modules/native-module/**/*" // native 模块不能放进 asar ] }}native 模块(better-sqlite3、keytar 等)必须 unpack,因为它们需要加载 .node 动态库,asar 里的文件不能直接 dlopen。双架构 vs 通用二进制macOS 支持 Universal 二进制(同时包含 x64 和 arm64),但体积翻倍。如果不需要 Rosetta 兼容,单独打 arm64 体积小一半:{ "build": { "mac": { "target": [{ "target": "dmg", "arch": ["arm64"] }] } }}M1/M2/M3 用户占 macOS 大多数,只打 arm64 够用。需要兼容 Intel 的场景再打 Universal。CI/CD 自动化用 GitHub Actions 自动打包:name: Buildon: pushjobs: build: strategy: matrix: os: [windows-latest, macos-latest, ubuntu-latest] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: { node-version: 20 } - run: npm ci - run: npx electron-builder --publish always env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} CSC_LINK: ${{ secrets.CSC_LINK }} CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}关键点:每个平台必须在对应 OS 上打包。Windows 安装包不能在 macOS 上交叉编译(签名工具不兼容)。macOS 公证也必须在 macOS 上跑。
服务端阅读 06月4日 14:00

Electron多窗口管理:IPC通信、MessagePort和窗口状态恢复

Electron 应用超过一个窗口就会遇到两个问题:怎么管(创建、销毁、引用回收)、怎么通(主窗口改了设置,设置窗口怎么知道)。管理不好就内存泄漏,通信不好就数据不一致。这篇文章把多窗口管理和 IPC 通信的常用模式讲清楚。窗口管理创建不同类型的窗口const { BrowserWindow } = require('electron')// 主窗口function createMainWindow() { const win = new BrowserWindow({ width: 1200, height: 800, webPreferences: { nodeIntegration: false, contextIsolation: true, preload: path.join(__dirname, 'preload.js') } }) win.loadFile('index.html') return win}// 设置窗口(模态,附属于主窗口)function createSettingsWindow(parent) { const win = new BrowserWindow({ width: 600, height: 400, parent: parent, modal: true, // 模态:打开时主窗口不可操作 show: false, // 先不显示,等 ready-to-show webPreferences: { nodeIntegration: false, contextIsolation: true, preload: path.join(__dirname, 'preload.js') } }) win.loadFile('settings.html') win.once('ready-to-show', () => win.show()) // 避免白屏闪烁 return win}// 工具窗口(无边框、置顶、透明)function createToolWindow() { const win = new BrowserWindow({ width: 300, height: 200, frame: false, // 无标题栏 alwaysOnTop: true, // 始终置顶 transparent: true, // 透明背景 resizable: false }) win.loadFile('tool.html') return win}窗口引用管理用一个 Map 统一管理所有窗口,防止引用泄漏:const windows = new Map()function createWindow(id, options) { // 如果已存在,聚焦而不是再创建 if (windows.has(id) && !windows.get(id).isDestroyed()) { windows.get(id).focus() return windows.get(id) } const win = new BrowserWindow(options) windows.set(id, win) win.on('closed', () => { windows.delete(id) // 窗口关闭时移除引用,允许 GC }) return win}// 使用createWindow('main', { width: 1200, height: 800 })createWindow('settings', { width: 600, height: 400, parent: windows.get('main'), modal: true })最常见的内存泄漏:窗口关闭了但引用还在——win.on('closed') 里必须把引用清除,否则 BrowserWindow 对象不会被回收。单例窗口某些窗口只能有一个(如设置窗口),重复点击应该聚焦已有窗口而不是新开:let settingsWindow = nullfunction openSettings() { if (settingsWindow && !settingsWindow.isDestroyed()) { settingsWindow.focus() return } settingsWindow = new BrowserWindow({ /* ... */ }) settingsWindow.on('closed', () => { settingsWindow = null })}isDestroyed() 检查很关键——窗口可能已经被 destroy() 了但变量还没清空。IPC 通信Electron 的 IPC 分两种方向:渲染→主(invoke/send)、主→渲染(send/webContents)。现代 IPC 模式(contextIsolation + preload)Electron 12+ 默认开启 contextIsolation,渲染进程不能直接用 require('electron')。正确做法是通过 preload 暴露安全 API:// preload.jsconst { contextBridge, ipcRenderer } = require('electron')contextBridge.exposeInMainWorld('electronAPI', { // invoke: 等待主进程返回结果(Promise) getSettings: () => ipcRenderer.invoke('get-settings'), saveSettings: (settings) => ipcRenderer.invoke('save-settings', settings), // send/on: 单向通知,不等返回 onSettingsChanged: (callback) => { ipcRenderer.on('settings-changed', (event, settings) => callback(settings)) }})// main.jsconst { ipcMain } = require('electron')// invoke 对应 handleipcMain.handle('get-settings', () => { return store.get('settings', { theme: 'light', fontSize: 14 })})ipcMain.handle('save-settings', (event, settings) => { store.set('settings', settings) // 通知所有窗口设置变了 BrowserWindow.getAllWindows().forEach(win => { win.webContents.send('settings-changed', settings) }) return true})invoke/handle 返回 Promise,适合需要返回值的场景。send/on 单向,适合通知类消息。窗口间通信两个渲染窗口之间不能直接 IPC,必须经过主进程中转:渲染进程A → ipcRenderer.invoke() → 主进程 ipcMain.handle() → win.webContents.send() → 渲染进程B主进程充当消息总线:// 主进程:转发消息ipcMain.on('relay-message', (event, targetWindowId, channel, data) => { const targetWin = windows.get(targetWindowId) if (targetWin && !targetWin.isDestroyed()) { targetWin.webContents.send(channel, data) }})MessagePort:双向通信通道Electron 14+ 支持 MessagePort,可以建立渲染进程间的双向通信通道,不需要每次都过主进程:// 主进程:为两个窗口创建通道ipcMain.handle('create-channel', (event, targetId) => { const targetWin = windows.get(targetId) if (!targetWin) return null const { port1, port2 } = new MessageChannelMain() // 给发起方 port1 event.sender.postMessage('channel-created', { port: port1 }, [port1]) // 给目标方 port2 targetWin.webContents.postMessage('channel-created', { port: port2 }, [port2])})渲染进程收到 port 后就可以直接通信:// 渲染进程window.electronAPI.onChannelCreated((port) => { port.onmessage = (event) => { console.log('收到消息:', event.data) } port.postMessage('hello from the other side')})MessagePort 适合实时数据流(如编辑器里主窗口和预览窗口的同步),比每次 invoke 少一次主进程中转。窗口状态持久化记住窗口位置和大小,下次打开时恢复:function createMainWindow() { const bounds = store.get('windowBounds', { width: 1200, height: 800, x: undefined, y: undefined }) const win = new BrowserWindow({ ...bounds, webPreferences: { /* ... */ } }) // 窗口移动或缩放时保存 win.on('resize', () => saveBounds(win)) win.on('move', () => saveBounds(win)) return win}function saveBounds(win) { clearTimeout(saveBounds.timer) saveBounds.timer = setTimeout(() => { store.set('windowBounds', win.getBounds()) }, 500) // 防抖,避免频繁写入}要注意:如果用户外接显示器拔掉了,保存的坐标可能在屏幕外,窗口"消失"了。恢复时检查坐标是否在可见区域内:const { screen } = require('electron')function ensureVisible(win) { const bounds = win.getBounds() const displays = screen.getAllDisplays() const visible = displays.some(d => { const { x, y, width, height } = d.workArea return bounds.x >= x && bounds.x < x + width && bounds.y >= y && bounds.y < y + height }) if (!visible) win.center()}
服务端阅读 06月4日 13:58

Electron菜单和托盘:跨平台差异、右键菜单和托盘图标坑

菜单和托盘是桌面应用的"门面"——用户通过菜单找到功能,通过托盘保持应用在后台运行。Electron 提供了 Menu 和 Tray API,但跨平台差异和坑不少:macOS 的菜单栏和 Windows 完全不同,托盘图标格式要求也不一样。这篇文章把菜单和托盘的常见实现都过一遍。应用菜单创建基础菜单macOS 应用的菜单栏是系统级的,不创建菜单连快捷键都不好使。Windows/Linux 的菜单可以藏在窗口里。const { app, Menu, BrowserWindow } = require('electron')app.whenReady().then(() => { const mainWindow = new BrowserWindow({ /* ... */ }) const template = [ { label: '文件', submenu: [ { label: '新建', accelerator: 'CmdOrCtrl+N', click: () => createNewFile() }, { label: '打开', accelerator: 'CmdOrCtrl+O', click: () => openFile() }, { type: 'separator' }, { label: '保存', accelerator: 'CmdOrCtrl+S', click: () => saveFile() }, { type: 'separator' }, { label: '退出', accelerator: 'CmdOrCtrl+Q', click: () => app.quit() } ] }, { label: '编辑', submenu: [ { role: 'undo', label: '撤销' }, { role: 'redo', label: '重做' }, { type: 'separator' }, { role: 'cut', label: '剪切' }, { role: 'copy', label: '复制' }, { role: 'paste', label: '粘贴' }, { role: 'selectAll', label: '全选' } ] }, { label: '视图', submenu: [ { role: 'reload', label: '刷新' }, { role: 'toggleDevTools', label: '开发者工具' }, { type: 'separator' }, { role: 'resetZoom', label: '重置缩放' }, { role: 'zoomIn', label: '放大' }, { role: 'zoomOut', label: '缩小' }, { type: 'separator' }, { role: 'togglefullscreen', label: '全屏' } ] } ] const menu = Menu.buildFromTemplate(template) Menu.setApplicationMenu(menu)})macOS 的特殊处理macOS 第一项菜单名必须是应用名,系统会自动加"关于""隐藏""退出"等菜单项:const isMac = process.platform === 'darwin'const template = [ ...(isMac ? [{ label: app.name, submenu: [ { role: 'about', label: `关于 ${app.name}` }, { type: 'separator' }, { role: 'services' }, { type: 'separator' }, { role: 'hide', label: '隐藏' }, { role: 'hideOthers' }, { role: 'unhide' }, { type: 'separator' }, { role: 'quit', label: '退出' } ] }] : []), // ... 其他菜单]如果不加这个,macOS 上应用菜单的行为会很奇怪——没有"关于"和"偏好设置"入口,也不支持 Cmd+H 隐藏窗口。动态菜单菜单项可以运行时修改——打勾、禁用、改文字:const menu = Menu.buildFromTemplate([ { label: '视图', submenu: [ { label: '深色模式', type: 'checkbox', checked: store.get('darkMode', false), click: (menuItem) => { store.set('darkMode', menuItem.checked) applyTheme(menuItem.checked) } }, { label: '导出', enabled: false, // 初始禁用,打开文件后启用 id: 'exportMenu' } ] }])// 打开文件后启用导出const exportItem = menu.getMenuItemById('exportMenu')exportItem.enabled = true隐藏默认菜单如果你不需要菜单栏(如工具类应用),可以设为 null:Menu.setApplicationMenu(null)注意:设为 null 后,复制粘贴等默认快捷键也会失效。如果你只是想隐藏菜单栏但保留快捷键,用 autoHideMenuBar: true 创建窗口(Windows/Linux 上按 Alt 显示菜单)。右键菜单(Context Menu)右键菜单是最常用的交互——在列表上右键编辑删除,在输入框里右键复制粘贴:const { Menu, ipcMain } = require('electron')// 渲染进程通过 IPC 请求弹出右键菜单ipcMain.on('show-context-menu', (event, type) => { const template = getContextMenuTemplate(type) const menu = Menu.buildFromTemplate(template) // 在当前窗口弹出 const win = BrowserWindow.fromWebContents(event.sender) menu.popup({ window: win })})function getContextMenuTemplate(type) { if (type === 'file') { return [ { label: '打开', click: () => openFile() }, { label: '重命名', click: () => renameFile() }, { type: 'separator' }, { label: '删除', click: () => deleteFile() } ] } if (type === 'text') { return [ { role: 'copy' }, { role: 'cut' }, { role: 'paste' } ] } return [{ role: 'copy' }, { role: 'paste' }]}渲染进程触发:// preload.jscontextBridge.exposeInMainWorld('electronAPI', { showContextMenu: (type) => ipcRenderer.send('show-context-menu', type)})// renderer.jswindow.addEventListener('contextmenu', (e) => { e.preventDefault() const type = e.target.closest('.file-item') ? 'file' : 'text' window.electronAPI.showContextMenu(type)})也可以用 electron-context-menu 这个库,自动给输入框加复制粘贴菜单、给图片加保存菜单。系统托盘(Tray)托盘让应用最小化到系统托盘区,不占任务栏位置——后台工具、音乐播放器、下载器几乎都要托盘。基础实现const { Tray, Menu, nativeImage } = require('electron')let tray = nullapp.whenReady().then(() => { const iconPath = path.join(__dirname, 'assets', 'tray-icon.png') const icon = nativeImage.createFromPath(iconPath) tray = new Tray(icon.resize({ width: 16, height: 16 })) tray.setToolTip('我的应用') const contextMenu = Menu.buildFromTemplate([ { label: '显示窗口', click: () => mainWindow.show() }, { label: '暂停', type: 'checkbox', checked: false, click: (item) => togglePause(item.checked) }, { type: 'separator' }, { label: '退出', click: () => app.quit() } ]) tray.setContextMenu(contextMenu)})点击托盘图标显示窗口Windows/Linux 上点击托盘图标通常应该显示/聚焦窗口,macOS 上则弹出菜单(系统规范):tray.on('click', () => { if (process.platform === 'darwin') return // macOS 用菜单 if (mainWindow.isVisible()) { mainWindow.hide() } else { mainWindow.show() mainWindow.focus() }})托盘图标格式不同平台对图标的要求不同:| 平台 | 推荐格式 | 尺寸 | 注意 ||------|----------|------|------|| Windows | .ico 或 .png | 16x16 | 支持 ICO 多尺寸 || macOS | .png 或 Template | 16x16 | 深色模式用 Template 图标 || Linux | .png | 16x16 | 部分桌面环境要求 22x22 |macOS 深色模式适配:图标文件名以 Template 结尾(如 tray-iconTemplate.png),系统会自动根据明暗主题反色。使用方式:const iconPath = path.join(__dirname, 'assets', process.platform === 'darwin' ? 'tray-iconTemplate.png' : 'tray-icon.png')最小化到托盘而非关闭点击关闭按钮时隐藏到托盘,而不是退出应用:mainWindow.on('close', (event) => { if (!app.isQuitting) { event.preventDefault() mainWindow.hide() }})app.on('before-quit', () => { app.isQuitting = true})app.isQuitting 是自定义标志——只有通过托盘的"退出"或 Cmd+Q 触发的退出才会真正关闭窗口。直接点关闭按钮只是隐藏。动态托盘图标下载进度、新消息通知等场景需要动态更新托盘图标:// 用 Canvas 生成带数字的图标const { nativeImage } = require('electron')function createBadgeIcon(count) { const { createCanvas } = require('canvas') // 需要 npm install canvas const size = 16 const canvas = createCanvas(size * 2, size * 2) // 2x for retina const ctx = canvas.getContext('2d') ctx.drawImage(baseIcon, 0, 0, size * 2, size * 2) if (count > 0) { ctx.fillStyle = '#FF3B30' ctx.beginPath() ctx.arc(size * 1.5, size * 0.5, 8, 0, Math.PI * 2) ctx.fill() ctx.fillStyle = 'white' ctx.font = 'bold 10px sans-serif' ctx.textAlign = 'center' ctx.fillText(count > 9 ? '9+' : String(count), size * 1.5, size * 0.5 + 4) } return nativeImage.createFromBuffer(canvas.toBuffer())}tray.setImage(createBadgeIcon(unreadCount))macOS 上更简单——系统原生支持 Dock 徽标:app.dock.setBadge(unreadCount > 0 ? String(unreadCount) : '')
服务端阅读 06月4日 13:57

Electron数据持久化:electron-store、IndexedDB和SQLite怎么选?

Electron 应用要存数据,选择比 Web 前端多——除了浏览器自带的 localStorage 和 IndexedDB,还能直接写文件系统、用 SQLite、或者用专门为 Electron 设计的 electron-store。选错了方案,后期迁移成本很高。这篇文章按场景分类,帮你选最合适的存储方案。方案选择速查| 方案 | 数据量 | 查询能力 | 适用场景 ||------|--------|----------|----------|| electron-store | < 1MB | 无(JSON 读写) | 用户设置、应用配置 || localStorage | < 5MB | 无(KV) | 简单状态、主题偏好 || IndexedDB | < 100MB | 索引查询 | 离线数据、缓存 || SQLite | 无上限 | 完整 SQL | 结构化数据、历史记录、搜索 || 文件系统 | 无上限 | 无 | 日志、导出文件、大文件 |electron-store:最简单的配置存储electron-store 是 Electron 生态里用得最多的轻量存储——本质就是把 JSON 文件读写封装了一层,加了 schema 校验、默认值、加密支持。npm install electron-storeconst Store = require('electron-store')const store = new Store({ defaults: { windowBounds: { width: 1200, height: 800 }, theme: 'system', recentFiles: [], }})// 读写store.set('theme', 'dark')store.get('theme') // 'dark'store.get('windowBounds.width') // 1200(支持点号路径)// 删除store.delete('theme')store.has('theme') // false// 监听变化store.onDidChange('theme', (newValue, oldValue) => { console.log(`主题从 ${oldValue} 变为 ${newValue}`)})数据存在 app.getPath('userData')/config.json,Windows 上是 %AppData%/你的应用名/config.json,macOS 上是 ~/Library/Application Support/你的应用名/config.json。适合存:用户偏好、窗口位置、最近打开的文件列表。不适合存:聊天记录、操作日志、任何需要条件查询的数据——JSON 文件每次读写都是全量操作,数据多了就慢。localStorage 和 sessionStorageElectron 的渲染进程里可以用浏览器的 localStorage,但有坑:// 渲染进程localStorage.setItem('key', 'value')localStorage.getItem('key')坑一:容量限制 5-10MB,存不了多少东西。坑二:数据绑定在 origin 上。如果你的应用用了自定义协议(app.setAsDefaultProtocol),localStorage 的 origin 可能变化,之前存的数据就找不到了。坑三:同步 API,数据量大时阻塞渲染线程。坑四:只能在渲染进程用,主进程访问不了。建议:只在渲染进程存一些临时状态(如表单草稿),重要数据不要依赖 localStorage。IndexedDB:浏览器里的结构化存储IndexedDB 是浏览器原生的 NoSQL 数据库,支持索引和事务,容量比 localStorage 大得多。// 打开数据库const request = indexedDB.open('MyAppDB', 1)request.onupgradeneeded = (event) => { const db = event.target.result const store = db.createObjectStore('notes', { keyPath: 'id', autoIncrement: true }) store.createIndex('updatedAt', 'updatedAt', { unique: false })}request.onsuccess = (event) => { const db = event.target.result // 写入 const tx = db.transaction('notes', 'readwrite') tx.objectStore('notes').add({ title: 'Hello', content: 'World', updatedAt: Date.now() }) // 按索引查询 const idxTx = db.transaction('notes', 'readonly') const index = idxTx.objectStore('notes').index('updatedAt') const range = IDBKeyRange.lowerBound(Date.now() - 86400000) // 最近一天 index.openCursor(range).onsuccess = (e) => { const cursor = e.target.result if (cursor) { console.log(cursor.value) cursor.continue() } }}IndexedDB 的 API 是回调式的,非常难用。推荐用 idb 这个库封装成 Promise:npm install idbconst { openDB } = require('idb')const db = await openDB('MyAppDB', 1, { upgrade(db) { db.createObjectStore('notes', { keyPath: 'id', autoIncrement: true }) }})await db.add('notes', { title: 'Hello', content: 'World' })const allNotes = await db.getAll('notes')IndexedDB 适合缓存数据(如离线文章列表),但复杂查询能力有限——没有 JOIN,没有聚合函数。数据量超过几百 MB 性能也会下降。SQLite:需要 SQL 时的选择Electron 主进程是 Node.js,可以直接用 SQLite。这是存储大量结构化数据的最优方案——完整 SQL 支持、事务、索引、百 GB 级数据都没问题。npm install better-sqlite3为什么用 better-sqlite3 而不是 sqlite3?因为 better-sqlite3 是同步 API,不用处理异步回调,性能也更好(C 绑定更高效)。在 Electron 主进程里同步不是问题——主进程本就不应该被数据库操作阻塞。const Database = require('better-sqlite3')const path = require('path')const dbPath = path.join(app.getPath('userData'), 'app.db')const db = new Database(dbPath)// 建表db.exec(` CREATE TABLE IF NOT EXISTS notes ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, content TEXT, updated_at INTEGER NOT NULL ); CREATE INDEX IF NOT EXISTS idx_notes_updated ON notes(updated_at);`)// 插入(prepared statement,防 SQL 注入)const insert = db.prepare('INSERT INTO notes (title, content, updated_at) VALUES (?, ?, ?)')insert.run('My Note', 'Hello world', Date.now())// 批量插入(事务)const batchInsert = db.transaction((items) => { for (const item of items) { insert.run(item.title, item.content, Date.now()) }})batchInsert([ { title: 'Note 1', content: 'A' }, { title: 'Note 2', content: 'B' },])// 查询const notes = db.prepare('SELECT * FROM notes WHERE updated_at > ? ORDER BY updated_at DESC LIMIT 20').all(Date.now() - 86400000)// 搜索const results = db.prepare("SELECT * FROM notes WHERE title LIKE ?").all('%keyword%')better-sqlite3 的 native 模块需要针对 Electron 重新编译:npm install --save-dev electron-rebuildnpx electron-rebuild或者在 package.json 里配置 postinstall:"postinstall": "electron-rebuild"文件系统:大文件和日志直接操作文件适合日志、导出数据、用户文档等场景:const fs = require('fs')const path = require('path')// 日志写入(append)const logPath = path.join(app.getPath('userData'), 'app.log')fs.appendFileSync(logPath, `[${new Date().toISOString()}] Event occurred\n`)// 用户文档目录const docsPath = app.getPath('documents')fs.writeFileSync(path.join(docsPath, 'export.json'), JSON.stringify(data))大量日志建议用 electron-log,它自动处理文件轮转和大小限制。数据迁移策略应用版本升级时,数据结构可能变化。每种方案的迁移方式不同:electron-store:在 defaults 里加新字段,旧数据自动补默认值。删字段要手动处理:const store = new Store({ defaults: { /* 新 schema */ } })// 迁移:删除废弃字段if (store.has('oldKey')) { store.delete('oldKey')}SQLite:用版本号控制迁移:const currentVersion = db.pragma('user_version', { simple: true })if (currentVersion < 1) { db.exec('ALTER TABLE notes ADD COLUMN tags TEXT') db.pragma('user_version = 1')}IndexedDB:onupgradeneeded 在版本号变化时触发,在这里加新 store 和索引。安全注意事项加密敏感数据:electron-store 支持 encryptionKey 选项,密码等敏感数据不要明文存 JSON不要存到代码目录:用 app.getPath('userData') 获取系统标准路径,不要写进 resources/渲染进程不要直接访问文件系统:通过 IPC 让主进程操作,避免开启 nodeIntegration
服务端阅读 06月4日 13:55

Electron调试指南:主进程、渲染进程和生产环境排查

Electron 有两个进程,调试方法完全不同:渲染进程用 Chrome DevTools,和调试网页一样;主进程是 Node.js 环境,需要用 VS Code 或远程调试协议。很多人只会开 DevTools,主进程出了问题只能 console.log 猜——这篇文章把两个进程的调试方法都讲清楚,以及生产环境下的排查手段。渲染进程调试:DevTools渲染进程就是 Chromium 的网页进程,调试体验和 Chrome 一样。自动打开 DevTools开发模式下自动打开,生产模式关闭:mainWindow = new BrowserWindow({ /* ... */ })mainWindow.loadFile('index.html')if (process.env.NODE_ENV === 'development') { mainWindow.webContents.openDevTools()}打包后的应用可以通过快捷键 Ctrl+Shift+I(Windows/Linux)或 Cmd+Option+I(macOS)手动打开。如果你不想让用户打开 DevTools,在创建窗口时设置 devTools: false。自定义快捷键切换const { globalShortcut } = require('electron')app.whenReady().then(() => { globalShortcut.register('CommandOrControl+Shift+D', () => { const win = BrowserWindow.getFocusedWindow() if (!win) return win.webContents.isDevToolsOpened() ? win.webContents.closeDevTools() : win.webContents.openDevTools() })})app.on('will-quit', () => globalShortcut.unregisterAll())安装框架 DevTools 扩展React DevTools 和 Vue DevTools 可以直接集成到 Electron 里:npm install --save-dev electron-devtools-installerconst { default: installExtension, REACT_DEVELOPER_TOOLS, VUEJS_DEVTOOLS } = require('electron-devtools-installer')app.whenReady().then(() => { installExtension(REACT_DEVELOPER_TOOLS) .then(name => console.log(`已安装: ${name}`)) .catch(err => console.error('安装失败:', err))})主进程调试:VS Code主进程跑在 Node.js 里,DevTools 管不到。用 VS Code 断点调试是最方便的方式。launch.json 配置{ "version": "0.2.0", "configurations": [ { "name": "Debug Main Process", "type": "node", "request": "launch", "cwd": "${workspaceFolder}", "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron", "windows": { "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd" }, "args": ["."], "outputCapture": "std" } ]}F5 启动调试,在 main.js 里打断点,主进程代码可以逐行跟踪。注意:这只能调试主进程,渲染进程的断点要在 DevTools 里设。同时调试两个进程先启动主进程调试,然后在 DevTools 里调试渲染进程。两个调试器互不干扰。但要注意:主进程调试器会拦截 console.log,输出在 VS Code 的 Debug Console 而不是终端。主进程调试:Chrome 远程调试不想用 VS Code 的时候,可以用 Chrome 的 DevTools 连接主进程:app.commandLine.appendSwitch('remote-debugging-port', '9222')启动应用后,在 Chrome 里打开 chrome://inspect,点击 "Configure…" 添加 localhost:9222,就能看到 Electron 主进程的 Node.js 上下文,可以直接断点调试。也可以命令行启动:npx electron --remote-debugging-port=9222 .远程调试的好处是不依赖 VS Code,任何能开 Chrome 的机器都能调试——适合远程排查 CI 环境或同事电脑上的问题。IPC 调试主进程和渲染进程通过 IPC 通信,消息传递出了问题最难排查——两边都能跑,但就是数据传不过去。监听所有 IPC 消息// 主进程:监听所有来自渲染进程的消息ipcMain.on('*', (event, ...args) => { console.log('[IPC Main ← Renderer]', event.channel, args)})// 渲染进程(通过 preload):监听所有来自主进程的消息contextBridge.exposeInMainWorld('electronAPI', { onMainMessage: (callback) => { ipcRenderer.on('*', (event, ...args) => { callback(event.channel, args) }) }})实际上 Electron 的 ipcMain 不支持通配符监听,变通方案是用 webContents.on('ipc-message') 在主进程拦截所有消息:mainWindow.webContents.on('ipc-message', (event, channel, ...args) => { console.log(`[IPC] ${channel}`, args)})这样所有 ipcRenderer.send 的消息都能在主进程看到,不用在每个 handler 里加 log。常见调试场景白屏/加载失败渲染进程白屏,先看 DevTools Console 有没有报错。如果连 DevTools 都打不开,可能是 HTML 文件路径错误或协议问题:// 错误:相对路径在打包后可能找不到文件mainWindow.loadFile('index.html')// 正确:用 file:// 协议的绝对路径mainWindow.loadFile(path.join(__dirname, 'index.html'))内存泄漏Electron 的内存泄漏通常来自两处:渲染进程的 JS 对象没释放(用 DevTools Memory 面板拍快照对比),或者主进程的 BrowserWindow 没 destroy()(检查窗口引用是否置 null)。CPU 占用高用 DevTools Performance 面板录制,看火焰图里的长任务。常见原因:渲染进程里的定时器没清理、大列表没虚拟化、主进程的同步文件操作阻塞了事件循环。生产环境排查生产环境没有 DevTools,需要靠日志:日志收集const { dialog } = require('electron')const fs = require('fs')const log = require('electron-log')log.transports.file.resolvePath = () => path.join(app.getPath('userData'), 'logs/main.log')log.transports.file.maxSize = 5 * 1024 * 1024 // 5MB 轮转// 捕获未处理的异常process.on('uncaughtException', (error) => { log.error('Uncaught Exception:', error) dialog.showErrorBox('意外错误', error.message)})electron-log 同时写文件和控制台,打包后日志在 userData/logs/ 目录下。用户反馈问题时让他们发这个文件。远程错误上报Sentry 提供 Electron SDK,自动捕获主进程和渲染进程的崩溃:const Sentry = require('@sentry/electron')Sentry.init({ dsn: 'your-dsn', attachStacktrace: true,})MiniDump 崩溃也能捕获——这是 DevTools 做不到的。
服务端阅读 06月4日 12:51

Electron 怎么集成 React/Vue?安全配置和 IPC 通信详解

Electron 集成 Web 技术的本质就是:渲染进程跑 Web 应用,主进程提供 Node.js 能力,两者通过 IPC 通信。前端框架、UI 库、CSS 框架在渲染进程里照常使用——Electron 对它们来说就是一个浏览器窗口。前端框架:照常用,注意路由模式React、Vue、Angular 在 Electron 里和浏览器里写法完全一样。唯一需要注意的是路由模式:React Router / Vue Router 默认用 history 模式,但 Electron 加载本地文件时 URL 是 file:// 协议,history 模式会 404。解决方案:用 HashRouter(React)或 createWebHashHistory(Vue),或确保生产环境用 file:// 加载时配置 fallback。更省心的方式是用 electron-vite 或 electron-forge 这些脚手架,它们把主进程、预加载脚本、渲染进程的构建都配好了——不用手动拼 Webpack/Vite 配置。安全配置:三条铁律所有集成的前提是安全配置正确。从 Electron 12 开始默认启用上下文隔离:// main.js — 必须这么配new BrowserWindow({ webPreferences: { nodeIntegration: false, // 禁止渲染进程直接用 Node contextIsolation: true, // 隔离预加载脚本和渲染进程 preload: path.join(__dirname, 'preload.js') // 只通过 preload 暴露 API }})// preload.js — 用 contextBridge 暴露安全 APIconst { contextBridge, ipcRenderer } = require('electron')contextBridge.exposeInMainWorld('electronAPI', { readFile: (path) => ipcRenderer.invoke('fs:readFile', path), writeFile: (path, data) => ipcRenderer.invoke('fs:writeFile', path, data), onMenuAction: (callback) => ipcRenderer.on('menu:action', (_, data) => callback(data))})// renderer.js — 像用普通 API 一样调用const content = await window.electronAPI.readFile('/path/to/file')永远不要开 nodeIntegration——渲染进程加载的第三方脚本(广告、分析 SDK)会拿到完整的 Node.js 权限,等于把电脑控制权交出去。IPC 通信:主进程和渲染进程的桥梁渲染进程不能直接调 Node.js API,必须通过 IPC 中转:渲染->主进程:ipcRenderer.invoke('channel', data) -> ipcMain.handle('channel', handler) — 请求-响应模式主进程->渲染进程:mainWindow.webContents.send('channel', data) -> ipcRenderer.on('channel', callback) — 推送模式常见场景:渲染进程需要读写文件(调 fs)、调系统对话框(调 dialog)、访问数据库——都走 IPC 让主进程执行,结果通过 Promise 返回。UI 库和 CSS 框架:无脑用Ant Design、Element Plus、Tailwind CSS、Material UI 在 Electron 里和浏览器里完全一样。Tailwind 特别适合 Electron 桌面应用——原子类让样式迭代快,打包时 tree-shake 掉没用到的类,体积可控。一个常见坑:某些 UI 库的弹窗/抽屉用 document.body.appendChild 挂载,如果渲染进程的 DOM 结构被 Electron 的安全策略限制,可能出现弹窗定位异常。解法是在弹窗组件上指定 getPopupContainer 回当前容器而非 body。构建工具:Vite 比 Webpack 快 10 倍Vite 的 HMR 在 Electron 开发体验远超 Webpack——渲染进程改一行 CSS 几乎秒刷。主进程改代码需要重启 Electron,但 Vite 对渲染进程的加速足够弥补。// vite.config.js — Electron 兼容配置export default defineConfig({ base: './', // 相对路径,file:// 协议必须 build: { outDir: 'dist', emptyOutDir: true }, server: { port: 5173, strictPort: true // 端口被占直接报错,不会自动换端口 }})开发时主进程 mainWindow.loadURL('http://localhost:5173'),生产环境 mainWindow.loadFile('dist/index.html')。追问Electron 能用 Service Worker 吗?技术上能,但没意义。Electron 应用本身就是"离线"的,不需要 SW 做缓存。而且 SW 在 file:// 协议下有限制。如果你需要离线数据缓存,用 IndexedDB 或 SQLite 直接存本地文件。怎么在渲染进程里用 Node 模块?不应该直接用。正确做法是:在 preload.js 里通过 contextBridge 暴露封装好的 API,主进程里用 Node 模块实现。如果非要绕过(不推荐),开 nodeIntegration: true——这等于放弃了安全隔离,只适合内部工具。怎么同时调试主进程和渲染进程?VS Code 的 launch.json 配两个 configuration:一个用 type: "node" 调试主进程,另一个用 Chrome DevTools 调试渲染进程。或者用 --remote-debugging-port=9222 启动 Electron,Chrome 访问 chrome://inspect 同时看两个进程。electron-devtools-installer 可以自动加载 React/Vue DevTools。
服务端阅读 06月2日 01:21

Electron 应用怎么防 XSS 和代码注入?安全最佳实践

Electron 的安全核心原则:不要信任渲染进程中的任何代码。渲染进程加载的是 Web 内容,可能被 XSS 攻击。如果渲染进程有 Node.js 访问权限(nodeIntegration: true),XSS 就等于远程代码执行——攻击者可以直接读写文件系统。第一条规则:关闭 nodeIntegrationnew BrowserWindow({ webPreferences: { nodeIntegration: false, contextIsolation: true }});nodeIntegration: false:渲染进程的 JS 不能直接调用 require('fs') 等 Node.js APIcontextIsolation: true:预加载脚本和渲染页面的 JS 运行在不同的 V8 上下文中,渲染页面无法修改预加载脚本的全局变量这是 Electron 最基本的安全配置。不关 nodeIntegration 的应用,一个 XSS 漏洞就能让攻击者完全控制用户的电脑。第二条规则:enableRemoteModule 关掉@electron/remote 模块让渲染进程间接调用主进程的 API。它本质上是把主进程的能力暴露给了渲染进程,和 nodeIntegration: true 一样危险。webPreferences: { nodeIntegration: false, contextIsolation: true, sandbox: true}sandbox: true 把渲染进程放在 Chromium 沙箱里,即使有漏洞也无法访问系统资源。这是最严格的安全模式。preload 脚本安全模式需要渲染进程和主进程通信时,用 preload 脚本暴露有限的 API:// preload.jsconst { contextBridge, ipcRenderer } = require('electron');contextBridge.exposeInMainWorld('electronAPI', { readFile: (path) => ipcRenderer.invoke('read-file', path), saveFile: (path, content) => ipcRenderer.invoke('save-file', path, content)});// 渲染进程const content = await window.electronAPI.readFile('/path/to/file');contextBridge.exposeInMainWorld 只暴露你明确声明的函数,渲染进程无法访问其他任何 Node.js API。攻击者即使注入了 JS,也只能调用 readFile 和 saveFile,不能执行任意系统命令。不要加载不信任的远程内容// 危险!加载远程 HTMLwin.loadURL('https://untrusted-site.com');// 安全:只加载本地文件win.loadFile('index.html');如果必须加载远程内容,用 webSecurity: true(默认开启)确保同源策略生效,并用 allowRunningInsecureContent: false 阻止加载 HTTP 资源。限制导航和弹窗win.webContents.on('will-navigate', (event, url) => { event.preventDefault(); // 阻止页面跳转});win.webContents.setWindowOpenHandler(() => { return { action: 'deny' }; // 阻止弹窗});XSS 攻击常用的手法是让页面跳转到恶意域名。阻止导航和弹窗可以切断这个路径。内容安全策略(CSP)在 HTML 的 meta 标签或 HTTP 头里设置 CSP,限制资源加载来源:<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'">script-src 'self' 只允许加载同源脚本,阻止攻击者注入外部 JS。unsafe-inline 对 CSS 可以接受(样式注入危害小),对 JS 绝对不能用。代码签名和公证未签名的应用会被操作系统拦截(macOS Gatekeeper、Windows SmartScreen),用户看到警告后大概率不敢安装。macOS:需要 Apple Developer 证书签名 + 公证(notarization)。electron-builder 配合 electron-notarize 工具自动完成。Windows:需要代码签名证书(EV 证书最可靠,立即获得 SmartScreen 信誉)。Linux:无签名要求,但 AppImage/Flatpak 有各自的签名机制。自动更新的安全确保更新包通过 HTTPS 下载,且验证签名。electron-updater 默认在 macOS 上验证代码签名,Windows 上验证 SHA256。不要关闭签名验证。检查清单[ ] nodeIntegration: false[ ] contextIsolation: true[ ] sandbox: true(如果不需要 preload 调 Node API)[ ] 不使用 @electron/remote[ ] preload 只暴露最小 API[ ] CSP 禁止 unsafe-inline 脚本[ ] 阻止渲染进程导航到外部 URL[ ] 应用签名 + 公证
服务端阅读 06月2日 01:20

Electron 太卡太占内存怎么办?8 个性能优化实战技巧

Electron 应用最常见的抱怨:内存 300MB 起步、启动慢、CPU 空转。这些问题大部分可以通过架构调整和代码优化解决,但有些是 Chromium 内核的硬限制,只能缓解不能根除。1. 减少渲染进程数量每个 BrowserWindow 是一个独立的 Chromium 渲染进程,至少占 50-80MB 内存。开了 5 个窗口就是 250-400MB。优化方案:能用一个窗口解决就不要开多个。需要多视图用 BrowserView(Electron 28+ 用 WebContentsView)代替多窗口——多个 View 共享一个渲染进程,内存开销小得多。const { WebContentsView } = require('electron');const view = new WebContentsView();view.webContents.loadURL('https://example.com');mainWindow.contentView.addChildView(view);2. 懒加载窗口不要在应用启动时创建所有窗口。只在用户打开时才创建,关闭时销毁:let settingsWindow = null;function openSettings() { if (settingsWindow) { settingsWindow.focus(); return; } settingsWindow = new BrowserWindow({ /* ... */ }); settingsWindow.on('closed', () => { settingsWindow = null; });}设为 null 而不是 hide——hide 保留渲染进程在内存里,设为 null 才真正释放。3. 主进程和渲染进程分离不要在渲染进程里做 CPU 密集型操作(解析大文件、图像处理),会阻塞 UI 导致卡顿。把这些任务放到主进程或 Worker 线程:// 渲染进程const { ipcRenderer } = require('electron');const result = await ipcRenderer.invoke('heavy-task', data);// 主进程ipcMain.handle('heavy-task', (event, data) => { return doHeavyWork(data);});更重的任务用 Node.js 的 worker_threads,避免阻塞主进程的事件循环。4. 开启 V8 内存优化const app = require('electron').app;app.commandLine.appendSwitch('js-flags', '--max-old-space-size=512');限制 V8 堆内存上限,迫使垃圾回收更积极。默认 V8 在 1.5GB 左右才触发 GC,限制到 512MB 让 GC 更早介入,减少内存峰值。5. 精简依赖Electron 应用打包后体积大的一个原因是 node_modules 太臃肿。用 electron-builder 的 files 配置只包含必要的文件:{ "build": { "files": [ "dist/**/*", "package.json" ], "extraResources": [] }}检查包体积:electron-builder 打包后看 *.blockmap 文件大小,或者用 source-map-explorer 分析 JS bundle 大小。6. 优化启动速度冷启动慢的常见原因:加载太多 JS、创建了不需要的窗口、初始化了用不到的模块。用 webpack 或 esbuild 打包,减少文件 I/O 次数延迟加载非核心模块(import() 动态导入)show: false 创建窗口,加载完成后再 show()const win = new BrowserWindow({ show: false });win.loadURL('app://index.html');win.once('ready-to-show', () => { win.show(); });7. 关闭不需要的 Chromium 特性app.commandLine.appendSwitch('disable-features', 'MediaRouter');app.commandLine.appendSwitch('disable-background-timer-throttling');MediaRouter 是 Chromecast 投屏功能,大部分桌面应用不需要。关掉能省一点内存和 CPU。8. 渲染进程性能分析用 Chrome DevTools 分析渲染进程性能——和优化网页一样。Ctrl+Shift+I 打开 DevTools,Performance 面板录制操作,看哪些函数耗时最长。主进程性能分析:启动时加 --inspect 参数,用 Chrome DevTools 远程连接。底线Electron 应用的内存下限约 150-200MB(Chromium 内核开销),这是无法突破的。如果你的应用必须控制在 50MB 以内,Electron 不是正确选择——看 Tauri 或纯原生。
服务端阅读 06月2日 01:19

Electron 和原生应用怎么选?性能、开发效率和跨平台对比

Electron 用 Web 技术写桌面应用,原生用 Swift/Kotlin/C++ 写。选哪个取决于你的团队技术栈、性能要求和分发场景。一句话:Web 团队选 Electron,性能敏感选原生。核心差异| 维度 | Electron | 原生(Swift/Kotlin/C++) ||------|----------|--------------------------|| 技术栈 | HTML/CSS/JS | 平台特定语言 || 跨平台 | 一套代码三端运行 | 每个平台单独写 || 开发速度 | 快(Web 生态成熟) | 慢(三倍工作量) || 安装包 | 大(100MB+,含 Chromium) | 小(10-30MB) || 内存占用 | 高(300MB+) | 低(50-100MB) || 启动速度 | 慢(2-5 秒) | 快(<1 秒) || 原生 API | 间接调用,部分受限 | 直接调用,完全访问 || UI 一致性 | 三端一致 | 各平台原生风格 |Electron 的优势开发效率高:前端工程师直接上手,不用学 Swift 和 Kotlin。一个团队维护一套代码,而不是三个。npm 生态几十万包,几乎所有功能都有现成库。UI 跨平台一致:设计稿做一套就行。原生开发每个平台 UI 规范不同,同样的功能要做三遍。热更新能力:Electron 可以通过远程加载 JS 实现热更新,绕过应用商店的审核周期。原生应用必须通过商店审核。知名 Electron 应用:VSCode、Slack、Discord、Notion、Figma Desktop——这些应用证明 Electron 能做到产品级质量。原生的优势性能:原生应用的 CPU/内存占用是 Electron 的 1/3 到 1/5。Chromium 渲染引擎本身就要占 200-300MB 内存,这是架构决定的,再怎么优化也下不来。系统集成:原生应用可以直接调用系统 API——Touch Bar、Widgets、App Shortcuts、系统通知的深度集成。Electron 只能通过有限的桥接接口访问。用户体验:原生应用遵循平台 UI 规范,用户不需要学习新的交互方式。Electron 应用做得很"Web",在 macOS 上感觉不像 Mac 应用。中间方案Tauri:用 Rust 写后端 + Web 前端,打包体积只有 3-10MB(不含 Chromium,用系统 WebView)。内存占用比 Electron 低 50-70%。适合工具类应用,但生态不如 Electron 成熟,原生模块支持有限。Flutter Desktop:Dart 语言,自绘引擎。跨端一致性好(移动端+桌面端),但桌面端生态还在发展中。React Native for Desktop:微软的 react-native-windows 和 react-native-macos。用 React 写原生 UI,比 Electron 性能好但社区较小。决策框架选 Electron 的情况:团队全是前端工程师应用是内容展示/工具类,不追求极致性能需要快速上线,三端同步发布需要热更新能力选原生的情况:应用对性能/内存敏感(视频编辑、3D 渲染、大型游戏)需要深度系统集成只需要支持一个平台用户体验优先级高于开发效率选 Tauri 的情况:想要 Electron 的开发体验但受不了包体积和内存后端逻辑不复杂(Tauri 的 Rust 后端学习曲线较陡)目标用户对安装包大小敏感
服务端阅读 06月2日 01:19

Electron 怎么实现自动更新?electron-updater 配置和完整流程

Electron 应用的自动更新用 electron-updater 实现,配合 electron-builder 打包。原理很简单:应用启动时检查远程服务器有没有新版本,有就下载并替换,下次启动生效。最简配置// package.json{ "build": { "publish": { "provider": "github", "owner": "your-username", "repo": "your-repo" } }}// main.tsimport { autoUpdater } from 'electron-updater';autoUpdater.autoDownload = false;autoUpdater.checkForUpdates();autoUpdater.on('update-available', () => { // 通知用户有新版本 autoUpdater.downloadUpdate();});autoUpdater.on('update-downloaded', () => { // 下载完成,提示重启 autoUpdater.quitAndInstall();});三步:检查更新 → 下载 → 安装。autoDownload: false 让你控制何时下载(可以选择在用户确认后再下载,避免浪费流量)。发布更新到 GitHub每次发布新版本:# 1. 改 package.json 版本号npm version patch # 或 minor / major# 2. 打包并发布到 GitHub Releaseselectron-builder --publish always--publish always 会自动把安装包上传到 GitHub Releases,并生成 latest.yml(Windows)/ latest-mac.yml(macOS)文件,electron-updater 靠这个文件判断是否有新版本。不想自动发布,用 --publish never,手动上传到 GitHub Releases。自建更新服务器不想用 GitHub?任何静态文件服务器都行。electron-updater 只需要访问两个文件:安装包(.exe / .dmg / .AppImage)版本描述文件(latest.yml / latest-mac.yml)// package.json{ "build": { "publish": { "provider": "generic", "url": "https://your-server.com/updates/" } }}服务器目录结构:/updates/├── latest.yml├── myapp-1.2.0.exe├── latest-mac.yml└── myapp-1.2.0.dmg每次发版把安装包和 yml 文件放上去就行。增量更新(Windows)全量更新每次下载完整安装包(几十到上百 MB),对大应用不友好。Windows 支持增量更新(blockmap):只下载变化的部分,减少 80-90% 下载量。electron-builder 打包时默认生成 .blockmap 文件,electron-updater 自动使用增量更新,不需要额外配置。macOS 和 Linux 不支持 blockmap 增量更新。更新进度通知下载大文件时应该显示进度:autoUpdater.on('download-progress', (progress) => { const percent = progress.percent.toFixed(1); const speed = (progress.bytesPerSecond / 1024 / 1024).toFixed(1); // 发送到渲染进程显示 mainWindow.webContents.send('update-progress', { percent, speed: `${speed} MB/s` });});更新签名macOS 必须签名才能自动更新,否则系统会拦截。Windows 建议签名,否则 SmartScreen 会弹警告。macOS 签名配置:{ "build": { "mac": { "identity": "Developer ID Application: Your Name (TEAMID)", "hardenedRuntime": true, "entitlements": "build/entitlements.mac.plist" } }}常见问题更新检查不触发:开发模式(electron .)下 autoUpdater 不工作,必须打包成安装版才能测试更新流程。可以用 electron-builder build --dir 生成未安装的包本地测试。macOS 更新后打不开:通常是签名或公证(notarization)问题。macOS 10.15+ 要求应用必须经过 Apple 公证,否则用户需要手动允许。在 electron-builder 里配 "afterSign": "electron-builder-notarize"。版本号没变但提示更新:确保每次发布都改了 package.json 的 version。electron-updater 通过比较版本号判断是否需要更新。
服务端阅读 06月2日 01:18

Electron 原生模块怎么用?N-API 和 node-gyp 编译实战

Electron 原生模块是用 C/C++ 写的 Node.js 模块,通过 N-API 或 NAN 桥接到 JavaScript。常见于需要调用系统 API(文件系统、硬件、加密)或追求极致性能的场景。难点不在写 C++ 代码,而在编译——Electron 用的 Node.js 版本和系统 Node 可能不同,导致原生模块编译失败。为什么需要原生模块JavaScript 做不了的事:调用操作系统的原生 API(注册表、系统通知、硬件接口)、极高性能计算(图像处理、加密)、复用已有的 C/C++ 库。常见的原生模块:node-serialport(串口通信)、node-gyp(编译工具链)、sharp(图像处理,底层 libvips)、keytar(系统密钥管理)。原生模块的两种方式1. N-API(推荐):Node.js 官方提供的稳定 ABI。用 N-API 编译的模块不依赖特定 Node.js 版本,Electron 升级时不需要重新编译。2. NAN(旧方案):直接用 V8 的 C++ API,不同 Node 版本间 API 会变,每次 Electron 升级可能需要重编译。新项目不要用 NAN。编译原生模块Electron 的 Node.js 版本和系统安装的 Node 版本不同,所以原生模块必须针对 Electron 重新编译。方法一:electron-rebuild(最简单)npm install electron-rebuild --save-devnpx electron-rebuild自动检测 Electron 的 Node 版本和 ABI,重新编译所有原生模块。方法二:prebuild(推荐发布流程)很多原生模块提供预编译二进制(prebuild),不需要本地编译。但 Electron 需要指定下载对应版本:npm install --runtime=electron --target=28.0.0 --disturl=https://electronjs.org/headers--target 是 Electron 版本号,--disturl 指向 Electron 的头文件下载地址。自己写一个原生模块用 N-API C++ 写模块,用 node-gyp 编译:// src/addon.cpp#include <node_api.h>napi_value Hello(napi_env env, napi_callback_info info) { napi_value result; napi_create_string_utf8(env, "Hello from C++!", NAPI_AUTO_LENGTH, &result); return result;}napi_value Init(napi_env env, napi_value exports) { napi_value fn; napi_create_function(env, "hello", NAPI_AUTO_LENGTH, Hello, NULL, &fn); napi_set_named_property(env, exports, "hello", fn); return exports;}NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)# binding.gyp{ "targets": [{ "target_name": "addon", "sources": ["src/addon.cpp"] }]}npm install --build-from-source在 Electron 里使用:const addon = require('./build/Release/addon.node');console.log(addon.hello()); // "Hello from C++!"常见编译问题MODULE_NOT_FOUND 或 Unsatisfied dependency:原生模块没有针对 Electron 重编译。跑 npx electron-rebuild 解决。node-gyp 编译报错:缺少构建工具链。macOS 需要 Xcode Command Line Tools(xcode-select --install),Windows 需要 npm install -g windows-build-tools,Linux 需要 build-essential 和 python3。Mac M1/M2 芯片兼容:arm64 架构的原生模块需要用 arm64 版的 Electron 编译。如果用 Rosetta 跑 x64 版 Electron,原生模块也要编译成 x64。electron-rebuild 会自动匹配架构。打包时处理原生模块用 electron-builder 打包时,原生模块会被自动包含。但如果用了 asar 打包,原生模块不能放在 asar 里面(.node 文件不能从 asar 中加载)。electron-builder 默认会把原生模块解压到 app.asar.unpacked 目录,不需要手动处理。
前端阅读 1142024年7月10日 00:18

如何在系统上安装 Electron?

Electron 是一个使用 JavaScript, HTML 和 CSS 构建跨平台桌面应用的框架。它基于 Node.js 和 Chromium,因此提供了一个富有表现力和高效的开发环境。下面是在系统上安装 Electron 的步骤: 步骤 1: 安装 Node.jsElectron 基于 Node.js,因此首先需要确保你的开发环境中安装了 Node.js。可以通过访问 Node.js 官网 并下载适合你操作系统的安装包来进行安装。安装完成后,可以在命令行中运行以下命令来验证安装是否成功:node -vnpm -v这些命令将显示 Node.js 和 npm(Node.js 的包管理器)的版本,从而确认它们已经正确安装。步骤 2: 使用 NPM 初始化新项目创建一个新的目录作为你的项目文件夹,并在该目录中打开命令行,运行以下命令来初始化一个新的 Node.js 项目:mkdir my-electron-appcd my-electron-appnpm init -y这将创建一个 package.json 文件,这是管理项目依赖和配置的核心文件。步骤 3: 安装 Electron通过 npm 安装 Electron 到你的项目中:npm install --save-dev electron这个命令将 Electron 添加到你的项目的开发依赖中,并且会修改 package.json 文件来反映这一变化。步骤 4: 创建你的第一个 Electron 应用在项目目录中创建一个 main.js 文件,这将是你的 Electron 应用的入口点。可以从 Electron 的官方文档或 GitHub 仓库中复制一个基础的“Hello World”示例到这个文件中。同时,需要修改 package.json 文件中的 scripts 部分,添加一个启动脚本:"scripts": { "start": "electron ."}步骤 5: 运行你的 Electron 应用一切设置完成后,可以通过以下命令来启动你的 Electron 应用:npm start这条命令将运行 Electron 并加载你的 main.js 文件,你应该能看到一个窗口打开,显示你的应用。总结通过上述步骤,你可以在你的开发环境中成功安装并运行一个基本的 Electron 应用。随着你对 Electron 的熟悉,可以继续探索更复杂的功能和优化你的应用。示例假设我曾经参与开发了一个基于 Electron 的文本编辑器项目。在项目初期,我负责搭建基础的项目结构,包括设置 Electron。通过上述步骤,我们快速地搭建了项目框架,并成功运行了第一个版本。这个过程中,确保每个开发者都能顺利在各自的机器上运行和测试应用是非常关键的。
前端阅读 2712024年7月10日 00:17

Electron使用安全吗?

Electron 是一个使用 JavaScript, HTML 和 CSS 构建桌面应用程序的框架。它让开发者可以使用前端技能来开发桌面应用,这在一定程度上提高了开发效率和跨平台兼容性。然而,讨论到 Electron 的安全性,这里有几个关键点需要考虑:1. Web 技术的安全风险由于 Electron 应用基于 Chromium 和 Node.js,它继承了 web 技术的一些安全挑战。例如,跨站脚本(XSS)攻击、远程代码执行等风险在 Electron 应用中同样存在。开发者需要像开发 web 应用一样对 Electron 应用进行安全控制和防护。2. Node.js 的集成Electron 允许在渲染进程中直接使用 Node.js API,这大大增加了应用程序的功能性,但同时也带来了安全风险。如果应用存在漏洞,攻击者可能通过这些 API 执行恶意代码或访问系统资源。因此,推荐的做法是在主进程中处理所有的 Node.js 调用,并严格限制渲染进程中的 Node.js 功能。3. 安全实践的缺乏Electron 开发需要开发者具备一定的安全意识和专业知识。如果开发团队缺乏对 Electron 安全模型的理解,可能会导致安全漏洞。例如,不正确的配置、权限过度放宽等问题都可能成为安全漏洞。4. 安全更新和维护和所有软件平台一样,保持 Electron 及其内部 Chromium 和 Node.js 组件的更新是非常重要的。这有助于及时修复已知的安全漏洞。但是,在一些实例中,开发者可能未能及时更新其应用中的 Electron 版本,这会留下潜在的风险。例子举个例子,2017年的一个研究指出,大约 37% 的 Electron 应用存在安全漏洞,这些漏洞主要来自过时的 Electron 版本。像 Slack、Discord 这样的流行应用都曾使用 Electron,这表明即使是大型项目也需要严格的安全审核和及时更新。结论总的来说,Electron 在提供了高效和便捷的同时,确实引入了一些安全风险。它的使用可以是安全的,但前提是开发者必须采取恰当的安全措施,包括但不限于使用安全的编码实践、定期更新 Electron 及其依赖、以及限制敏感操作只在可信的进程中执行。通过这些方法,可以显著降低安全风险。
前端阅读 2612024年7月10日 00:17

Electron 代码调试方式有哪些?

在Electron中进行代码调试涉及不同层面和技巧,主要包括以下几个方面:1. 主进程调试主进程(Main Process)负责管理Web页面和与操作系统的交互。调试主进程可以使用以下方法:使用electron --inspect启动Electron这允许你通过Chrome DevTools进行调试。你可以在命令行中运行如下命令:electron --inspect=9222 your-app-main.js这会在9222端口上启动一个WebSocket服务器,你可以通过Chrome浏览器访问 chrome://inspect 并点击 "Open dedicated DevTools for Node" 连接到这个端口进行调试。VS Code集成调试在VS Code中,你可以添加一个配置到.vscode/launch.json文件来调试主进程:{ "version": "0.2.0", "configurations": [ { "type": "node", "request": "launch", "name": "Debug Main Process", "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron", "program": "${workspaceFolder}/main.js", "stopOnEntry": false, "console": "integratedTerminal" } ]}这样设置后,你可以直接从VS Code启动并调试Electron的主进程。2. 渲染进程调试渲染进程(Renderer Process)承载了Web页面,调试方式类似于普通的Web页面调试:使用开发者工具在Electron的窗口中,你可以直接使用Ctrl+Shift+I(或者Cmd+Opt+I在Mac上)打开Chrome开发者工具进行调试。远程调试也可以通过启动Electron时添加--remote-debugging-port参数来启用远程调试:electron --remote-debugging-port=9223 your-app.js然后通过浏览器访问 http://localhost:9223 来连接远程调试工具。3. 使用专用工具对于更复杂的情况,你还可以使用如 Spectron (用于自动化测试Electron应用)或 Devtron (Electron专用的DevTools扩展)这样的工具来辅助调试。实际案例在我之前的项目中,我们开发了一个基于Electron的桌面应用。在调试主进程内存泄漏问题时,我使用了--inspect参数并通过Chrome开发者工具的内存快照工具定位到泄漏源。这样的工具和方法极大地帮助我们提高了调试效率。调试是确保Electron应用性能和稳定性的关键步骤,使用合适的工具和方法能有效提升开发和维护效率。
前端阅读 2862024年7月9日 23:43

Electron 应用程序的性能优化有哪些技术手段?

Electron 应用程序的性能优化技术手段在 Electron 应用程序的开发中,性能优化是一个重要的考虑因素,尤其是因为 Electron 应用倾向于消耗较多的系统资源。以下是一些主要的性能优化技术手段:1. 优化 JavaScript 和 CSS减少资源的体积:使用工具如 Webpack 或 Rollup 来压缩和合并 JavaScript 文件和 CSS 文件,减少文件的大小和请求数量。移除未使用的代码:利用 Tree Shaking 技术移除未使用的代码,减少最终包的体积。例子:在一个项目中,使用 Webpack 的 UglifyJS 插件压缩 JavaScript,减少了约 30% 的文件大小。2. 渲染进程的优化使用分离的渲染器进程:为不同的窗口或功能使用独立的渲染进程,可以避免单一进程过载,提高响应性和效率。延迟加载和懒加载:对于非首屏需要展示的资源,可以延迟加载或懒加载,减少初始化加载的时间。例子:在开发一个多标签应用时,每个标签使用独立的渲染进程,这样一个标签的崩溃不会影响到其他标签。3. 有效管理内存主动释放未使用的资源:及时释放不再需要的对象和变量,避免内存泄漏。使用原生模块代替 JavaScript 对象:在可能的情况下,使用更接近系统底层的原生模块,这些通常比 JavaScript 实现更加高效。例子:通过使用 global.gc() 在适当的时候强制垃圾回收,帮助释放内存。4. 优化 Electron 的配置预加载脚本:使用预加载脚本来加载最小必需的 JavaScript,避免在渲染进程中加载过多的脚本。禁用不必要的 Electron 功能:例如,如果不需要使用 Node.js 集成,可以在渲染进程中禁用它,减少资源占用。例子:在一个 Electron 应用中禁用了 Node.js 集成,并通过预加载仅加载了必要的脚本,这样减少了内存的占用并提高了加载速度。5. 使用硬件加速开启硬件加速:默认情况下 Electron 支持硬件加速,但在某些情况下如果遇到问题可以尝试关闭它来查看性能变化。优化图形渲染:对于图形密集型应用,合理使用 WebGL 或其他 Web 技术可以利用 GPU 加速渲染。例子:在开发一款视频处理软件时,通过使用 WebGL 来处理图像渲染,显著提高了渲染效率。通过这些技术手段,可以有效地提升 Electron 应用程序的性能。每种方法都有其适用的场景,因此在应用到具体项目时,需要根据实际情况灵活选择和调整。
前端阅读 1212024年7月9日 23:42

Electron 的菜单有哪些不同类型?

在 Electron 中,菜单主要分为以下几种类型:应用程序菜单(Application Menu):应用程序菜单是位于应用窗口顶部的主菜单,通常包括文件、编辑、视图、窗口和帮助等常见的菜单项。例如,在 macOS 上,应用程序菜单还包括应用名称的菜单,这个菜单通常包含关于、服务、隐藏、退出等选项。上下文菜单(Context Menu):上下文菜单是右键点击时出现的菜单,这种菜单通常与特定的上下文或界面元素相关联,如文本编辑区右键可能出现剪切、复制、粘贴等选项。上下文菜单可以根据应用中当前状态或元素类型提供不同的选项。托盘菜单(Tray Menu):托盘菜单是指在系统托盘图标(或系统通知区域图标)上右键点击或单击时显示的菜单。这类菜单常用于背景运行的应用,允许用户快速访问应用功能,如打开主窗口、退出应用等。通过 Electron 的 Menu 模块,开发者可以灵活地构建和修改这些菜单。例如,你可以使用以下代码段来创建一个简单的应用程序菜单:const { Menu, MenuItem } = require('electron')const template = [ { label: '编辑', submenu: [ { label: '撤销', role: 'undo' }, { label: '重做', role: 'redo' }, { type: 'separator' }, { label: '剪切', role: 'cut' }, { label: '复制', role: 'copy' }, { label: '粘贴', role: 'paste' } ] }, { label: '视图', submenu: [ { label: '重载', role: 'reload' }, { label: '全屏切换', role: 'togglefullscreen' } ] }]const menu = Menu.buildFromTemplate(template)Menu.setApplicationMenu(menu)这段代码定义了一个具有“编辑”和“视图”两个主菜单项的应用程序菜单,每个菜单项下有具体的操作选项。通过使用角色(role)属性,Electron 能够提供一些常用的行为,如撤销、剪切、复制等,简化开发过程。
前端阅读 1292024年7月9日 23:42

如何在 Electron 中创建通知提醒?

在 Electron 中创建通知提醒主要有两种方式:使用 HTML5 的 Notification API 或者 Electron 的 Notification 模块。以下是详细的步骤和示例代码:使用 HTML5 Notification APIHTML5 的 Notification API 较为通用,适用于在 web 页面中创建通知。在 Electron 中使用时,它会调用系统的通知功能。步骤:检查权限:在发送通知前,需要先检查或请求用户允许显示通知的权限。创建并显示通知:一旦获得权限,就可以创建并显示通知。示例代码:function showNotification() { // 检查权限 if (Notification.permission !== "granted" && Notification.permission !== "denied") { Notification.requestPermission().then(permission => { if (permission === "granted") { createNotification(); } }); } else if (Notification.permission === "granted") { createNotification(); }}function createNotification() { const notification = new Notification("新通知", { body: "这是一个通知内容示例。", icon: "path/to/icon.png" }); notification.onclick = () => { console.log('通知被点击了!'); };}showNotification();使用 Electron 的 Notification 模块Electron 提供了一个自定义的 Notification 模块,它完全支持在应用程序中发送系统通知。步骤:检查是否支持:在某些操作系统上,Electron 的通知可能不被支持,因此首先要检查是否支持。创建并显示通知。示例代码:const { Notification } = require('electron');function showElectronNotification() { if (Notification.isSupported()) { const notification = new Notification({ title: "新通知", body: "这是一个通知内容示例。", icon: "path/to/icon.png" }); notification.show(); notification.on('click', () => { console.log('通知被点击了!'); }); } else { console.log("通知功能不支持!"); }}showElectronNotification();结论根据您的需求,如果您需要更多的原生系统集成,推荐使用 Electron 的 Notification 模块。如果您的应用更依赖于 web 技术,或者需要与传统的 web 应用保持一致性,HTML5 Notification API 可能是更好的选择。在实际开发中,您可以根据具体情况选择合适的方法。