ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Electron 打包网页为独立 exe:全屏遮任务栏与 Esc 退出实战

Electron 打包网页为独立 exe:全屏遮任务栏与 Esc 退出实战

简介:这份资源面向希望把本地HTML网页封装成Windows桌面程序的开发者,尤其适合已掌握HTML、CSS与JavaScript、想快速上手Electron打包流程的前端或全栈人员。内容围绕Electron框架展开,覆盖将网页资源打包为.exe可执行文件,并实现全屏显示、隐藏任务栏、Esc键退出程序,以及自定义exe图标与文件名等实用功能,同时涉及主进程与渲染进程间的IPC通信、electron-packager与electron-builder的打包参数配置。压缩包共约2000个文件,以js、ts、map、json、md、license等为主,另有pak、dll、exe等运行时与打包产物,整体约95.61MB,目录结构完整,便于对照查阅依赖与配置。目前已有1161人学习下载。读者可据此获得一套可直接参考的Electron工程结构、关键代码片段与打包命令示例,理解全屏窗口、任务栏遮挡、快捷键退出及图标替换的实现思路,并借助其中的依赖与配置信息排查版本兼容问题,减少从零搭建的试错成本。

1. 把网页塞进 exe:为什么我选 Electron 而不是直接打包浏览器

有个做内部工具的朋友找我,说他们车间有台老工控机,系统是 Windows 7,浏览器版本锁死在 Chrome 60 左右,但业务系统已经用上了 Vue 3 + Vite 构建的页面,一打开就白屏。他的诉求很具体:把网页变成一个双击就能跑的 exe,全屏显示、盖住任务栏、按 Esc 退出,顺便把图标和文件名都换成自己公司的。这不是什么高深需求,但真动手时坑不少——用 HTA 吧,内核太老;用 NW.js 吧,社区凉了半截;用 Tauri 吧,Win7 支持又是个玄学。绕了一圈,最后还是回到 Electron。它自带 Chromium 内核,版本可控,打包出来就是一个独立 exe,不依赖客户机装没装浏览器。这篇就按我实际落地的顺序,把从环境搭建到改图标、改文件名、全屏遮任务栏、Esc 退出的完整链路拆一遍,顺带把几个容易翻车的地方标出来。

2. 环境搭建与最小可运行 Electron 壳:从零到看见窗口

Electron 的本质是 Chromium + Node.js 的运行时,主进程管窗口和系统交互,渲染进程管页面。你要做的不是“编译网页”,而是让 Electron 加载你的 HTML,再把整个运行时打包成 exe。所以第一步不是写代码,而是把 Node 环境和项目骨架搭对。

2.1 Node 版本选择与项目初始化

Node 版本别追新,尤其你要兼容 Win7 的话。Electron 从 23 开始就不支持 Win7 了,所以如果目标机器有 Win7,锁死 Electron 22.x,对应 Node 16 左右。我一般用 nvm 切版本,避免全局污染。

# 查看当前 node 版本,建议 16.x 或 18.x node -v # 新建项目目录并初始化 package.json mkdir my-electron-app && cd my-electron-app npm init -y # 安装 electron,指定 22.x 以兼容 Win7 npm install electron@22.3.27 --save-dev

这里--save-dev是因为 Electron 只是开发依赖,最终打包时会被塞进 exe,不需要作为生产依赖发布。装完后node_modules/electron/dist下会有对应平台的二进制,国内网络如果卡在下载阶段,设置ELECTRON_MIRROR环境变量指向国内镜像即可,这个属于常规操作,不展开。

2.2 主进程与渲染进程的最小闭环

项目根目录建两个文件:main.js和index.html。main.js是主进程入口,负责创建窗口、设置全屏、监听 Esc。

// main.js const { app, BrowserWindow, globalShortcut } = require('electron'); const path = require('path'); let win = null; function createWindow() { win = new BrowserWindow({ width: 1920, height: 1080, fullscreen: true, // 全屏,但任务栏可能还在 frame: false, // 去掉标题栏和边框 autoHideMenuBar: true, // 隐藏菜单栏 webPreferences: { nodeIntegration: false, // 安全起见,关掉 contextIsolation: true } }); // 加载本地 HTML 或远程地址 win.loadFile(path.join(__dirname, 'index.html')); // 关键:设置 alwaysOnTop 并覆盖任务栏 win.setAlwaysOnTop(true, 'screen-saver'); win.setFullScreen(true); // 监听 Esc 退出 win.webContents.on('before-input-event', (event, input) => { if (input.key === 'Escape') { app.quit(); } }); } app.whenReady().then(() => { createWindow(); }); app.on('window-all-closed', () => { app.quit(); });

index.html随便写个页面验证:

<!DOCTYPE html> <html> <head><meta charset="utf-8"><title>Demo</title></head> <body style="background:#1e1e1e;color:#fff;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;"> <h1>Electron 全屏测试,按 Esc 退出</h1> </body> </html>

在package.json里加启动脚本:

"scripts": { "start": "electron ." }

跑npm start,你应该能看到一个无边框全屏窗口,按 Esc 直接退出。到这里最小闭环就通了。注意setAlwaysOnTop(true, 'screen-saver')这行,第二个参数是层级,screen-saver比普通alwaysOnTop更高,能盖住任务栏。但有些系统上任务栏会闪一下再被盖住,这是正常的,后面避坑章节会讲怎么处理。

3. 全屏遮任务栏与 Esc 退出的细节:别让任务栏弹出来

窗口能全屏了,但任务栏偶尔还会冒出来,尤其是用户按 Win 键或者鼠标移到屏幕底部时。这不是 Electron 的 bug,是 Windows 的焦点机制在作祟。要彻底遮住,得从窗口层级和事件拦截两方面下手。

3.1 窗口层级与 kiosk 模式

Electron 的BrowserWindow有个kiosk选项,设为true后就是真正的信息亭模式,任务栏、开始菜单全被屏蔽,用户只能操作你的页面。但 kiosk 模式下退出得靠代码,因为 Alt+F4 可能也被拦。我一般不用 kiosk,而是用fullscreen + alwaysOnTop + frame:false组合,灵活性更高。

win = new BrowserWindow({ fullscreen: true, frame: false, skipTaskbar: true, // 不在任务栏显示图标 alwaysOnTop: true, webPreferences: { ... } }); // 确保窗口获得焦点后再次置顶 win.on('focus', () => { win.setAlwaysOnTop(true, 'screen-saver'); }); // 防止窗口失去焦点时任务栏弹出 win.on('blur', () => { win.setAlwaysOnTop(true, 'screen-saver'); win.focus(); });

skipTaskbar: true让 exe 不在任务栏占位,blur事件里强制抢回焦点,这样即使用户点了任务栏,窗口也会立刻重新置顶。但注意,win.focus()在blur里调用可能造成死循环,所以加个延时或者判断条件更稳妥。我通常用setTimeout(() => win.focus(), 10)绕开。

3.2 Esc 退出的三种实现与取舍

Esc 退出有三种写法,各有适用场景:

第一种是上面用的before-input-event,在渲染进程加载前拦截键盘事件,优点是响应快,缺点是如果页面里有输入框,Esc 可能被页面逻辑吃掉。第二种是在渲染进程里用document.addEventListener('keydown'),然后通过 IPC 通知主进程退出。第三种是注册全局快捷键globalShortcut.register('Escape', ...),但全局快捷键会抢占系统其他程序的 Esc,不推荐。

我一般用第一种,并在页面里避免使用 Esc 作为功能键。如果页面确实需要 Esc,就改成组合键,比如Ctrl+Shift+Q。

// 组合键退出示例 win.webContents.on('before-input-event', (event, input) => { if (input.control && input.shift && input.key.toLowerCase() === 'q') { app.quit(); } });

参数说明:input.control和input.shift是布尔值,input.key是按键名称。注意input.key对字母键返回的是小写,所以用toLowerCase()统一处理。

3.3 多显示器下的全屏策略

如果客户机接了双屏,fullscreen: true默认只在主屏全屏,副屏会露出桌面。要覆盖所有屏幕,得用screen模块获取所有显示器,然后为每个显示器创建一个窗口,或者用win.setBounds()把窗口拉到所有屏幕的并集区域。

const { screen } = require('electron'); function createWindowOnAllDisplays() { const displays = screen.getAllDisplays(); const primary = displays[0]; // 计算所有屏幕的并集区域 let minX = 0, minY = 0, maxX = 0, maxY = 0; displays.forEach(d => { minX = Math.min(minX, d.bounds.x); minY = Math.min(minY, d.bounds.y); maxX = Math.max(maxX, d.bounds.x + d.bounds.width); maxY = Math.max(maxY, d.bounds.y + d.bounds.height); }); win.setBounds({ x: minX, y: minY, width: maxX - minX, height: maxY - minY }); }

这段代码在createWindow之后调用,能把窗口横跨所有显示器。但实际用的时候,如果副屏分辨率不同,页面布局可能错位,所以更稳妥的做法是只主屏全屏,副屏用win.setFullScreen(false)后手动定位。这个看具体场景,不是所有项目都需要。

4. 改图标、改文件名与打包成 exe:electron-builder 实战

开发时跑npm start没问题,但交付给客户得是一个独立的 exe,双击就能跑,图标和文件名都得换。这里用electron-builder,它比electron-packager更省心,支持 NSIS 安装包和绿色版。

4.1 安装 electron-builder 与基础配置

npm install electron-builder --save-dev

在package.json里加build字段:

"build": { "appId": "com.example.myapp", "productName": "MyApp", "directories": { "output": "dist" }, "win": { "target": "portable", "icon": "build/icon.ico" }, "portable": { "artifactName": "MyApp.exe" } }

target设为portable生成单文件绿色版,双击直接运行,不写注册表。artifactName控制最终 exe 的文件名,这里就是MyApp.exe。icon指向build/icon.ico,必须是 256x256 的 ico 文件,否则打包会报错。

4.2 图标制作的坑与验证

图标不是随便找个 png 改后缀就行。Windows 的 ico 格式要求包含多个尺寸(16x16、32x32、48x48、256x256),否则在某些分辨率下会显示模糊或者默认 Electron 图标。我一般用在线工具或者 ImageMagick 生成:

# 用 ImageMagick 把 png 转成多尺寸 ico convert icon.png -define icon:auto-resize=256,128,64,48,32,16 build/icon.ico

转换完用file build/icon.ico检查一下,确认是MS Windows icon resource且包含多个尺寸。如果打包后 exe 图标没变,先清空dist目录再重新打包,electron-builder 有缓存。

4.3 打包命令与产物验证

"scripts": { "start": "electron .", "dist": "electron-builder --win portable" }

跑npm run dist,等几分钟,dist目录下会出现MyApp.exe。双击运行,检查三件事:图标是不是你设置的、文件名是不是MyApp.exe、全屏和 Esc 退出是否正常。如果 exe 启动后白屏,大概率是loadFile的路径不对,打包后__dirname指向的是 asar 包内部,用path.join(__dirname, 'index.html')通常没问题,但如果用了process.cwd()就会翻车。

注意:portable 模式每次运行会解压到临时目录,启动比安装版慢几秒,这是正常的。如果客户机有杀毒软件,可能会误报,需要加白名单。

5. 避坑与常见问题排查:我踩过的五个坑

这一章全是血泪经验,每个都是实际交付时遇到的,按「现象 → 原因 → 解决」写,你对照着排查能省不少时间。

5.1 任务栏遮不住,鼠标一碰就弹出来

现象:全屏后任务栏隐藏了,但鼠标移到屏幕底部,任务栏又冒出来,甚至把窗口顶下去。

原因:Windows 的任务栏有自动弹出机制,当鼠标触碰到屏幕边缘时会激活。alwaysOnTop只能保证窗口在普通窗口之上,但任务栏的层级在某些情况下更高。

解决:用setAlwaysOnTop(true, 'screen-saver')把窗口层级提到最高,同时在blur事件里重新抢焦点。如果还不行,加一个定时器每 500ms 检查一次窗口是否被遮挡,是的话重新置顶。但定时器耗电,只建议在工控机这种常电场景用。

5.2 Esc 退出失效,按了没反应

现象:开发时 Esc 能退出,打包后按 Esc 没反应。

原因:打包后页面加载速度变慢,before-input-event注册时机可能晚于用户按键。或者页面里有 iframe,焦点在 iframe 里,主进程收不到键盘事件。

解决:把 Esc 监听到app.whenReady()之后立刻注册,不要等窗口加载完。如果是 iframe 问题,在渲染进程里也加一份监听,通过ipcRenderer.send('exit-app')通知主进程退出。

5.3 打包后 exe 体积巨大,超过 200MB

现象:一个简单页面打包出来 200MB+,客户嫌大。

原因:Electron 自带 Chromium 和 Node,基础体积就是 150MB 左右,这是正常的。如果超过 200MB,可能是把node_modules全打进去了,或者包含了开发依赖。

解决:在build配置里加files字段,只打包必要文件:

"files": [ "main.js", "index.html", "node_modules/**/*" ]

同时用electron-builder的asar压缩,能再小一点。但别指望降到几十 MB,那是 Tauri 的领域。

5.4 图标改了但 exe 还是默认图标

现象:build/icon.ico明明换了,打包出来还是 Electron 默认图标。

原因:electron-builder 有图标缓存,或者 ico 文件尺寸不对。

解决:删掉dist和node_modules/.cache,重新打包。确认 ico 包含 256x256 尺寸,用identify build/icon.ico查看。如果还不行,在win配置里显式指定icon的绝对路径。

5.5 Win7 上启动报错,提示缺少 api-ms-win-crt

现象:在 Win7 客户机上双击 exe,弹窗提示缺少api-ms-win-crt-runtime-l1-1-0.dll。

原因:Electron 22 依赖 Visual C++ 2015 运行时,Win7 默认没装。

解决:在打包时把 VC++ 运行库一起带上,或者让客户机装vcredist_x86.exe。electron-builder 有nsis配置可以自动安装依赖,但 portable 模式不行,得手动放一个vcredist在 exe 同目录,首次运行时静默安装。

6. 进阶技巧:用 asar 保护源码与自动更新预留

打包成 exe 后,源码其实还在 asar 包里,用asar extract就能解出来。如果不想让客户随便改页面,可以开asar并配合代码混淆,但这不是绝对安全,只是提高门槛。我一般会在build里加:

"asar": true, "compression": "maximum"

compression: maximum用 7z 压缩,体积能再小 10% 左右,但启动会慢一点。另外,如果以后要加自动更新,现在就把publish字段预留好,指向一个静态文件服务器,用electron-updater实现。不过内网工具通常不需要自动更新,手动替换 exe 更简单。

验证打包是否成功,我有个习惯:把 exe 拷到一台没装 Node、没装 Electron 的干净虚拟机上跑一遍,确认所有依赖都打进去了。这个步骤帮我拦过好几次“开发机能跑、客户机白屏”的问题。从那以后我每次交付前都强制走一遍干净环境验证,希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进