ARTICLE DETAIL

资讯详情

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

从electron-quick-start到跨平台桌面应用:5分钟跑通第一个Electron项目

从electron-quick-start到跨平台桌面应用:5分钟跑通第一个Electron项目 写这篇的时候我先说说我自己的经历。几年前我第一次接触桌面应用开发看到 Electron 的第一反应是“这玩意儿不就是一个套了壳的浏览器吗”心里多少有点不屑。但实际用了几个基于 Electron 做的工具之后我改变看法了——能用前端技术栈快速交付一款 Mac、Windows、Linux 都能跑的桌面应用对个人开发者和小团队来说性价比实在太高。如果你已经会写 HTML、CSS 和 JavaScript那么从 0 到跑通第一个 Electron 应用真的只需要 5 分钟。这篇文章我就带你把 electron-quick-start 这个官方脚手架吃透再顺手把第一个跨平台桌面应用改造出来。1. 为什么我推荐从 electron-quick-start 起步而不是自己搭工程新手接触 Electron 时往往有两个极端要么直接去看英文文档被主进程、渲染进程、IPC、preload 一堆概念劝退要么打开 Vite 或者 Webpack 想自己从头搭一套工程结果光配置就折腾一整天。两条路我都走过最后发现 electron-quick-start 才是最优解。1.1 官方脚手架的设计哲学给你最小可运行集合electron-quick-start 是 Electron 官方维护的最小示例仓库整个项目只有三个核心文件main.js、index.html、package.json另外附带一个preload.js在较新版本中已经加入。它不包含任何打包工具、不需要编译步骤、不强迫你使用任何前端框架。这个设计的精妙之处在于它把 Electron 应用最本质的部分剥离了出来一个负责创建窗口的主进程脚本一个展示内容的 HTML 页面一个描述应用元信息和启动入口的 package.json。你把这三个文件理解了整个 Electron 的架构骨架也就理解了八成。从这个地基往上盖楼即便后面换成 React、Vue或者接上 Vite、electron-builder底层逻辑依然是这三件套。1.2 环境准备只需要 Node.js 和 npm在动手之前你得先确认机器上有 Node.js。Electron 本身是跑在 Node.js 生态里的npm install和npm start这些命令都依赖 Node 运行时。我建议装 Node.js 16 以上的版本太老的版本会出现一些奇奇怪怪的兼容问题。这里有个小坑要提醒Electron 的二进制文件默认从 GitHub 下载国内网络环境下经常卡在安装这一步。解决办法是设置 Electron 的镜像源在命令行里先执行下面这句npm config set electron_mirror https://npmmirror.com/mirrors/electron/如果你已经配置过 npm 镜像也可以直接把ELECTRON_MIRROR环境变量指到上面的地址。这一步不提前做的话后面npm install可能会等很久甚至直接失败别问我怎么知道的我当年就卡了整整一个下午。环境检查完毕之后可以用两个命令验证一下node -v npm -v能正常输出版本号就说明基础环境没问题。至于用 nvm 管理 Node 版本、用 pnpm 还是 yarn 这类选择那是项目复杂到一定程度之后才需要考虑的事现在可以先不管。1.3 绕过“从零搭建”的四个理由自己从零搭建工程并不是不可以但对于入门来说有四个很现实的障碍。第一你要手动创建 .gitignore、配置文件、入口文件任何一个路径写错都会导致应用启动失败这对新手排查起来很痛苦。第二你需要理解 CommonJS 和 ES Module 的差异选错了模块系统可能连require都报错。第三你还要处理开发模式下的热更新这一套联动配置调试成本极高。第四官方脚手架的代码是经过社区反复验证的不会有版本不匹配的问题。electron-quick-start 把这四个障碍全部移除了你只需要克隆仓库、安装依赖、启动应用。从学习路径的角度看官方模板就是帮你把“环境陷阱”全部踩平的那条路。2. 五分钟上手克隆、安装、启动全流程实录接下来就是重头戏——把这个应用真正跑起来。我会把每一步都拆开揉碎包括你看到的输出结果长什么样出现了报错该怎么处理。2.1 获取模板仓库的两种方式获取 electron-quick-start 的方式有两种第一种是用 git 克隆git clone https://github.com/electron/electron-quick-start cd electron-quick-start如果你没有安装 Git或者不想用命令行克隆可以直接去 GitHub 仓库页面下载 ZIP 压缩包解压后得到相同的目录结构。我个人更推荐用 git clone因为后面你想看更新日志或者切换分支拉取不同版本来对比学习都会方便很多。克隆下来之后你看到的目录应该是这样的electron-quick-start/ ├── .gitignore ├── index.html ├── LICENSE.md ├── main.js ├── package.json ├── preload.js └── renderer.js如果你的版本里没有renderer.js也不用慌新版本把渲染进程的逻辑单独拆到renderer.js里了核心文件依然是 main.js 和 index.html。2.2 npm install卡住和成功进入项目目录后执行npm install这一步会读取package.json里的依赖声明把electron这个包下载到node_modules目录。默认情况下 Electron 的安装包体积在 80MB 到 100MB 左右下载速度取决于你的网络环境和第一节里说的镜像配置。安装过程如果顺利你会在node_modules/electron/dist/目录下看到一个解压好的 Electron 可执行文件同时node_modules/.bin/electron这个符号链接会被创建它就是后续启动应用用的程序入口。如果安装卡在Downloading electron这个输出上超过五分钟大概率就是网络问题。你可以按 CtrlC 中断重新执行上面那句镜像配置然后删掉node_modules目录再重新npm install。2.3 npm start第一个窗口出现依赖装好之后启动应用npm start这句命令实际上执行的是package.json里scripts字段定义的electron .它的含义是让 Electron 运行当前目录下的main.js。如果一切正常一个宽度 800、高度 600 的窗口就会弹出来窗口标题是 “Hello World”页面上会显示一句 “Hello World! This is Electron, a JavaScript framework for creating desktop applications with web technology. We are using Node.js [版本号] and Electron [版本号].” 旁边还有一个 “Node.js Integration” 测试按钮点击后会触发一个带 Node 版本信息的原生弹窗。看到这个窗口出现你的第一个 Electron 应用就跑通了。这时候你已经在 Windows、macOS 或者 Linux 上成功用 Web 技术创建了一个原生桌面窗口跨平台桌面应用这扇门已经推开了一半。2.4 第一次启动的常见报错速查第一次启动时最常遇见的报错信息我整理在这张表里如果不幸碰到了直接对照处理就行。报错信息原因解决办法Electron failed to install correctlynode_modules 里的 electron 包不完整删掉 node_modules 后重新 npm installCannot find module main.js启动命令没找到入口文件确认当前目录在项目根目录或检查 package.json 的 main 字段Error: ENOENT: no such file or directory路径中包含中文或空格把项目移动到纯英文路径下再试GPU process launch failed显卡驱动或系统图形环境异常临时用electron . --disable-gpu启动验证是否与此相关3. 摸清模板里每个文件的职责从 Hello World 到看懂架构窗口弹出来只是第一步真正提升段位的是搞清楚这个窗口是怎么“被创建”的。这一节带你把 electron-quick-start 里的每个文件过一遍弄清楚每一行代码都在干什么。3.1 main.js主进程的“上帝视角”打开main.js你会看到这样一段代码略做过精简适配const { app, BrowserWindow } require(electron); const path require(node:path); function createWindow() { const mainWindow new BrowserWindow({ width: 800, height: 600, webPreferences: { preload: path.join(__dirname, preload.js), }, }); mainWindow.loadFile(index.html); } app.whenReady().then(() { createWindow(); app.on(activate, () { if (BrowserWindow.getAllWindows().length 0) createWindow(); }); }); app.on(window-all-closed, () { if (process.platform ! darwin) app.quit(); });这段代码是整个应用的地基。我先说一个关键概念Electron 应用跑起来之后至少有两个进程——主进程和渲染进程。主进程对应这里的 main.js它运行在 Node.js 环境里负责创建窗口、管理应用生命周期、和操作系统底层交互渲染进程对应你的浏览器窗口负责渲染 HTML 页面、运行前端 JavaScript。app.whenReady()是一个异步方法它会等 Electron 完成初始化后触发回调在这个回调里调用createWindow()创建窗口。为什么要等待这个事件因为 Electron 内部有一堆原生初始化工作比如加载系统资源、注册进程单例窗口不能在这之前创建。BrowserWindow是主进程里最重要的类它负责创建一个原生窗口。注意webPreferences.preload里指定了一个预加载脚本路径用path.join(__dirname, preload.js)拼出来。__dirname是当前脚本所在目录的绝对路径这样的写法保证了无论在哪个平台、哪个目录启动Electron 都能正确定位 preload.js。app.on(activate, ...)这段针对 macOS 做了特殊处理用户点击 Dock 图标时如果窗口都关了会重新创建一个窗口。macOS 的惯例是应用窗口关闭后应用本身还在运行所以需要有这个“重建窗口”的逻辑。window-all-closed则对应 Windows 和 Linux 的习惯所有窗口关闭后应用就退出。但 macOS 会保留应用常驻所以用process.platform ! darwin判断来过滤。3.2 index.html 与 renderer.js渲染进程的“前台体验”index.html就是一个普通的 HTML 文件里面用h1展示了 “Hello World!” 文案下方写了一行说明还放了一个按钮。这个文件在main.js里通过mainWindow.loadFile(index.html)被加载它运行在渲染进程里。渲染进程和浏览器里的页面非常像但它有一点本质不同它可以通过 preload 脚本获得一部分 Node.js 能力。在较新版本的模板中index.html里会引入renderer.js用来绑定按钮的点击事件script src./renderer.js/script而renderer.js里会这样给按钮绑定事件const button document.getElementById(test-button); button.addEventListener(click, () { const message This is a message from Node.js: ${process.versions.node}; document.getElementById(node-version).innerText message; alert(message); });这种结构是 Electron 官方推荐的写法HTML 负责结构JavaScript 负责交互两者分离页面逻辑清爽。3.3 preload.js沟通两个世界的桥梁preload.js是很多人容易忽视但极重要的文件。在 Electron 的安全模型里渲染进程默认不能直接访问 Node.js 的 API也不能直接用require(electron)。但很多业务场景确实需要渲染进程和主进程通信比如点击“选择文件”按钮弹出一个原生文件对话框这类操作必须由主进程完成。preload.js 就是那个“中间人”。默认模板的 preload.js 里其实内容不多但它的意义在于从 Electron 12 开始contextIsolation默认开启渲染进程和 Node.js 环境是隔离的preload 脚本是唯一被允许在这个隔离边界上运行的代码它可以通过contextBridge安全地向渲染进程暴露 APIconst { contextBridge, ipcRenderer } require(electron); contextBridge.exposeInMainWorld(api, { getNodeVersion: () process.versions.node, sendMessage: (msg) ipcRenderer.send(message-from-renderer, msg), });这样你在renderer.js里就能用window.api.getNodeVersion()来获取 Node 版本而不用直接暴露整个 Node.js 给页面。从安全角度看这是“最小权限原则”的体现——我只给你我需要你用的能力其他的门都关起来。3.4 package.json应用的“身份证”package.json决定了 Electron 怎么跑起来{ name: electron-quick-start, version: 1.0.0, main: main.js, scripts: { start: electron . }, devDependencies: { electron: ^30.0.0 } }main字段是整个应用的入口它告诉 Electron 启动时该加载哪个文件。scripts.start里的electron .表示用 Electron 打开当前项目目录。devDependencies里的electron版本号是开发依赖因为在生产环境中你不需要安装 Electron——它已经随打包产物一起发布了。到这里你就明白了main.js是大脑index.html是脸面preload.js是桥梁package.json是说明书。四者的协作关系大致如下Electron 读 package.json 找到 main.jsmain.js 创建 BrowserWindow 并加载 index.htmlpreload.js 在加载 HTML 之前先注入安全 APIrenderer.js 在页面里用这些 API 做交互。4. 从模板到自己的应用第一个真正可玩的桌面工具模板跑通了接下来就动手改造。我给你设计一个完整的实操路径把这个项目改造成一个“系统信息查看器”——可以显示当前系统的平台、架构、内存信息和 Node 版本。这个工具不大但需要用到主进程 API、IPC 通信、页面渲染三条链路练一遍之后你对 Electron 的理解会再上一个台阶。4.1 改造 BrowserWindow自定义窗口的尺寸、标题和外观首先在main.js里修改BrowserWindow的配置加入标题、图标和更合适的尺寸const { app, BrowserWindow } require(electron); const path require(node:path); function createWindow() { const mainWindow new BrowserWindow({ width: 640, height: 480, title: 系统信息查看器, autoHideMenuBar: true, webPreferences: { preload: path.join(__dirname, preload.js), contextIsolation: true, nodeIntegration: false, }, }); mainWindow.loadFile(index.html); // 打开开发者工具时会比较方便正式发布前记得注释掉 mainWindow.webContents.openDevTools({ mode: detach }); } app.whenReady().then(() { createWindow(); app.on(activate, () { if (BrowserWindow.getAllWindows().length 0) createWindow(); }); }); app.on(window-all-closed, () { if (process.platform ! darwin) app.quit(); });几个关键点说明一下。title可以指定窗口标题栏上显示的文本但如果 HTML 里有title标签那个优先级更高——所以想改窗口标题最干净的做法是两边都改。autoHideMenuBar: true会把默认菜单栏File、Edit、View 那套自动隐藏Windows 和 Linux 上按 Alt 键可以临时显示这能让你的应用看起来更简洁。nodeIntegration: false必须显式声明这是安全底线防止页面里的脚本直接访问 Node.js API。contextIsolation: true保持默认开启配合 preload 脚本使用能有效隔离两个世界。openDevTools在开发阶段很实用但我建议你写个注释提醒自己发布前一定要删掉不然用户打开应用会弹出一个开发者工具窗口很掉价。4.2 用 IPC 让渲染进程“指挥”主进程拿系统数据获取系统信息需要用到主进程的os模块渲染进程不能直接访问。我们用 IPC进程间通信来实现流程如下渲染进程用户点击“获取系统信息”按钮preload 脚本把请求通过ipcRenderer.invoke发给主进程主进程收到消息后调用 Node.jsos模块拿到数据返回给渲染进程渲染进程把数据显示在页面上。先在 preload.js 中暴露一个统一的数据获取方法const { contextBridge, ipcRenderer } require(electron); contextBridge.exposeInMainWorld(systemInfo, { getInfo: () ipcRenderer.invoke(get-system-info), });然后在 main.js 中用ipcMain.handle监听这个请求const os require(node:os); ipcMain.handle(get-system-info, async (event) { return { platform: os.platform(), // 比如 win32 / darwin / linux architecture: os.arch(), // 比如 x64 / arm64 cpuCount: os.cpus().length, // CPU 物理核心数逻辑核心 totalMemory: (os.totalmem() / 1024 / 1024 / 1024).toFixed(2), // GB freeMemory: (os.freemem() / 1024 / 1024 / 1024).toFixed(2), // GB nodeVersion: process.versions.node, electronVersion: process.versions.electron, }; });ipcMain.handle和ipcRenderer.invoke是一对配套 API它们实现了“请求-响应”式的通信模式渲染进程调用invoke主进程 handle 处理完返回值Promise 就会 resolve。如果是单向发消息不需要返回值可以用ipcRenderer.sendipcMain.on如果要持续推送数据比如显示下载进度可以考虑webContents.send。现在这个场景用 invoke/handle 是最简洁的。4.3 改造 index.html 页面与按钮逻辑接下来把 HTML 页面改成展示系统信息的布局!DOCTYPE html html langzh-CN head meta charsetUTF-8 / title系统信息查看器/title style body { font-family: system-ui, sans-serif; padding: 40px; background: #f6f7f9; color: #222; } .card { background: white; border-radius: 12px; padding: 24px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); } .info-row { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #eee; } button { margin-top: 16px; padding: 10px 18px; font-size: 14px; cursor: pointer; } /style /head body div classcard h1系统信息查看器/h1 div idinfo-list div classinfo-rowspan平台/spanspan idplatform-/span/div div classinfo-rowspan架构/spanspan idarchitecture-/span/div div classinfo-rowspanCPU 数量/spanspan idcpuCount-/span/div div classinfo-rowspan总内存/spanspan idtotalMemory-/span/div div classinfo-rowspan可用内存/spanspan idfreeMemory-/span/div div classinfo-rowspanNode.js 版本/spanspan idnodeVersion-/span/div div classinfo-rowspanElectron 版本/spanspan idelectronVersion-/span/div /div button idget-info-btn获取系统信息/button /div script src./renderer.js/script /body /html对应地修改renderer.jsconst btn document.getElementById(get-info-btn); btn.addEventListener(click, async () { const info await window.systemInfo.getInfo(); document.getElementById(platform).innerText info.platform; document.getElementById(architecture).innerText info.architecture; document.getElementById(cpuCount).innerText info.cpuCount; document.getElementById(totalMemory).innerText info.totalMemory GB; document.getElementById(freeMemory).innerText info.freeMemory GB; document.getElementById(nodeVersion).innerText info.nodeVersion; document.getElementById(electronVersion).innerText info.electronVersion; });然后运行npm start点击按钮页面就会显示出当前系统的真实信息。这一步做完你已经实际走通了“渲染进程触发 → preload 桥接 → 主进程响应 → 渲染进程渲染”的完整链路这是 Electron 开发最核心的套路后面再复杂的业务也跳不出这个框架。4.4 开发环境与生产环境不能忽略的细节在主进程里判断开发还是生产环境通常会看环境变量或 Electron 自带的一个快捷字段const isDev !app.isPackaged;app.isPackaged在应用被打包后比如用 electron-builder 或者 Electron Forge 打包会返回true开发模式下返回false。用这个字段可以灵活控制行为比如开发环境加载本地文件、生产环境加载远程地址如果你后续要做自动更新的话if (isDev) { mainWindow.loadFile(index.html); } else { mainWindow.loadURL(https://your-website.com); }这里有个需要注意的地方加载远程 URL 时页面会去请求网络资源如果应用要离线工作你就得把静态资源打包进应用里用本地路径加载。早期我写过一个桌面工具为了图方便直接 loadURL 指向我们的官网结果用户断网之后应用就白屏了。桌面应用的第一原则就是尽量不依赖远程资源所有静态资源随包分发。5. 事情还没完打包、跨平台发布和安全加固一个都不能少标题里既然提到“跨平台桌面应用”那么只在你自己的电脑上跑通那一半。另一半是把你的应用打包成 Windows 上的.exe、macOS 上的.dmg、Linux 上的.AppImage并处理好不同平台的差异。5.1 不同平台的差异与适配思路Electron 的跨平台并不是零成本三个主流桌面平台在几个关键点上有明显差异我根据自己的实践经验整理如下。窗口行为差异。macOS 应用窗口关闭后应用仍然常驻 Dock点击 Dock 图标后需要重新创建窗口这就是模板里activate回调用意的由来Windows 和 Linux 则是最后窗口关闭即退出整个进程。这个不是你改配置能避开的必须在代码里针对process.platform做分支处理。文件路径差异。Windows 路径用反斜杠\macOS 和 Linux 用正斜杠/。Node.js 的path模块能自动帮你处理但如果你直接手写路径字符串拼接打包到别的平台上就很容易爆炸。一个原则永远用path.join或者path.resolve来拼路径不要用 /这种方式。快捷键差异。开发菜单快捷键时macOS 用Command对应meta修饰键Windows 和 Linux 用Ctrl。Electron 的accelerator配置里可以写成CmdOrCtrlShiftI它会根据当前平台自动映射成对应的键省去手动判断。5.2 打包工具的实际选择先看一个现状Electron 官方推荐的是Electron Forge社区里也很流行electron-builder另外一个叫electron-packager的工具则比较轻量。这三个工具定位不同我用一个表格给你对比一下。工具核心能力最适合的场景我用下来的感受Electron Forge官方一体化脚手架、开发、打包全家桶想用官方的东西一路走到底集成度高但有时候封装太厉害出问题时难排查electron-builder打包、制作安装包、自动更新配置一站式需要产出多平台安装包、对打包配置有较多自定义要求配置略复杂但文档全、社区案例多electron-packager只把应用打成一个可执行文件目录快速产出免安装的可执行程序简单直接但不生成安装包如果你现在只是个人工具、学习项目或者内部分发那 electron-packager 最合适。如果你要把应用发布给普通用户还要做图标、安装包、自动更新那 electron-builder 是绕不开的。我个人现在的主力工具是 electron-builder因为它对多平台打包支持最成熟下面就以它为例。安装 electron-builder 后在package.json里增加build配置{ build: { appId: com.example.systeminfo, productName: SystemInfo, directories: { output: dist }, win: { target: nsis }, mac: { target: dmg }, linux: { target: AppImage } }, scripts: { build:win: electron-builder --win, build:mac: electron-builder --mac, build:linux: electron-builder --linux } }然后运行npm run build:win打包之后dist/目录下会生成一个系统信息查看器 Setup 1.0.0.exe安装包双击安装后桌面会出现应用图标。到这里“跨平台桌面应用”这件事才算真正闭环。5.3 继续往里走的几个进阶话题你的第一个 Electron 应用已经可以跑了、可以打包了但如果你要继续深入下面这几个话题是接下来的必经之路。菜单栏定制。在很多工具型应用里默认菜单栏是完全可以隐藏甚至重构成自定义菜单的。Electron 通过Menu.buildFromTemplate可以构建自定义菜单还能给菜单项挂快捷键、勾选状态。如果你想做一个“无菜单栏、纯内容”、类似浏览器壳的应用autoHideMenuBar加上快捷键监听基本就够了。主进程与渲染进程通信的进阶模式。前面用的invoke/handle适合一次性请求。如果主进程想要主动推送数据比如文件下载完成、后台任务进度更新可以用webContents.send主动向页面发消息。再往下还有MessageChannelMain这种更底层、适合构建复杂双向通信的机制。自动更新。桌面应用和网页应用最大的差别之一就是没有“刷新一下就拿到新版本”的机制。electron-builder 可以配合electron-updater实现安装包自动更新这套机制在 Windows 和 macOS 上都能工作。自动更新是一个单独的大话题能聊的比今天的系统信息查看器多得多。安全加固。渲染进程里不该开nodeIntegrationpreload 里只暴露出你真正需要的方法用contextIsolation守住边界。如果你将来要加载远程内容还需要配置 CSP内容安全策略来限制页面能加载的资源范围。桌面应用跑在用户本机上安全级别要求其实比网页更高——网页攻击最多拿你的账号桌面应用如果防线太松攻击者直接就能碰你的本地文件系统。写在最后我的实操体会我从第一次跑通 electron-quick-start到写完一个能正常打包发布的小工具中间大概花了两天。回头看卡住我的从来不是 Electron 本身而是各种环境问题和新旧版本之间的 API 差异。所以这次写这篇我把能提前踩的坑都给你标了出来——镜像配置、nodeIntegration 默认关闭、app.isPackaged判断开发生产、跨平台路径不能写死这些每一个都是真实项目里会遇到的坑。最后再分享一个小技巧把 electron-quick-start 的仓库保留一份在你自己的代码托管账号里平时做实验、复现问题、临时做个一次性小工具都从它起步。Electron 版本更新之后官方模板会同步调整 API 用法你把它当成一个会呼吸的活文档比单纯看文档能学到更多。下一次遇到“这个功能怎么做”的时候先看看 quick-start 的基础链路能不能跑通再想怎么加东西就会轻松很多。
返回列表