简介:这份资源面向希望把本地HTML页面封装成Windows桌面程序的开发者,尤其适合已掌握HTML、CSS与JavaScript、想快速上手Electron打包流程的前端或全栈人员。内容围绕Electron框架展开,覆盖将网页资源打包为.exe可执行文件的核心流程,并重点实现全屏显示、隐藏任务栏、按Esc键退出程序,以及自定义exe图标与文件名等实用功能,同时涉及主进程与渲染进程间的IPC通信、electron-packager与electron-builder的打包参数配置。压缩包共约2000个文件,以js、ts、map、json、md、license等为主,另有cmd、yml、pak、dll、exe等运行时与配置相关文件,整体约95.61MB,目录结构完整,便于对照查阅依赖与构建产物。目前已有1161人学习下载,可作为从网页到桌面应用落地的参考工程,帮助读者理解窗口创建、全屏控制、键盘事件监听与打包定制的实现思路。
1. 从网页到桌面程序:为什么我选 Electron 做全屏 kiosk 封装
去年接手一个展厅项目,客户给了一个已经做好的 HTML 页面,要求是:双击打开就全屏,任务栏不能露出来,按 Esc 才能退出,exe 图标和文件名都得换成他们自己的。听起来简单,但真动手才发现坑不少——浏览器 F11 全屏会留白边,任务栏偶尔还会弹出来抢焦点,打包出来的 exe 默认叫 electron.exe,图标是那个原子模型,客户一看就皱眉。
这类需求其实很常见:把现成的 HTML 页面(不管是单文件还是带资源的目录)封装成一个独立的 Windows 可执行文件,运行起来像原生程序一样全屏铺满,没有浏览器地址栏、没有标签页、没有右键菜单,退出方式由你说了算。Electron 就是干这个的——它把 Chromium 和 Node.js 打包在一起,让你用前端技术写桌面应用,同时能直接调用系统级 API 控制窗口行为。
适合谁看?手里有 HTML 页面想转成 exe 的前端,做数字标牌、展厅互动、工控面板、考试系统客户端的开发者,或者单纯想给自己写的网页工具套个壳的人。下面我从项目结构开始,把全屏、遮任务栏、Esc 退出、图标和文件名修改这几个核心点逐个拆开讲。
2. 项目骨架与主进程:BrowserWindow 的创建与全屏参数
2.1 目录结构与依赖安装
Electron 项目的结构很扁平,核心就三个东西:主进程入口、渲染进程页面、package.json。我一般会这样组织:
my-kiosk/ ├── main.js # 主进程入口 ├── preload.js # 预加载脚本(可选) ├── index.html # 你的 HTML 页面 ├── assets/ # 页面资源 ├── build/ │ └── icon.ico # 打包用的图标 └── package.json初始化依赖只需要两步。先建 package.json,然后装 electron 和打包工具:
npm init -y npm install electron --save-dev npm install electron-builder --save-dev这里选 electron-builder 而不是 electron-packager,原因是它对图标替换、安装包生成、文件名自定义的支持更完整,配置写在 package.json 里就行,不用记一堆命令行参数。装完之后 package.json 里会有 devDependencies,版本号按你实际装的来,不锁死。
2.2 主进程创建全屏窗口
主进程是整个应用的入口,负责创建窗口、控制生命周期。全屏和遮任务栏的关键都在 BrowserWindow 的构造参数里:
// main.js const { app, BrowserWindow, globalShortcut } = require('electron'); const path = require('path'); let mainWindow = null; function createWindow() { mainWindow = new BrowserWindow({ fullscreen: true, // 直接全屏,铺满屏幕 frame: false, // 去掉标题栏和边框 kiosk: true, // kiosk 模式,强制独占全屏 alwaysOnTop: true, // 置顶,防止被其他窗口盖住 skipTaskbar: true, // 不在任务栏显示图标 autoHideMenuBar: true, // 隐藏菜单栏 webPreferences: { preload: path.join(__dirname, 'preload.js'), contextIsolation: true, nodeIntegration: false } }); mainWindow.loadFile('index.html'); // 注册 Esc 退出 globalShortcut.register('Escape', () => { mainWindow.close(); }); mainWindow.on('closed', () => { mainWindow = null; }); } app.whenReady().then(createWindow); app.on('window-all-closed', () => { app.quit(); });逐段说下参数。fullscreen: true让窗口直接以全屏启动,但光有这个还不够——Windows 上任务栏有时会盖在上面。kiosk: true才是真正的独占模式,它会强制窗口占据整个屏幕并且阻止用户通过常规方式退出,这是遮住任务栏最可靠的一招。frame: false去掉系统标题栏,skipTaskbar: true让程序不在任务栏留图标,alwaysOnTop: true防止被其他窗口抢焦点。
globalShortcut.register('Escape', ...)注册的是全局快捷键,即使焦点不在窗口上也能响应。注意要在窗口关闭时注销,否则多次启动会冲突。contextIsolation: true和nodeIntegration: false是安全默认值,除非你的 HTML 页面需要直接调 Node API,否则别开。
2.3 加载页面与资源路径处理
loadFile加载本地 HTML 文件,路径相对于主进程文件所在目录。如果你的 HTML 引用了 CSS、JS、图片,用相对路径就行,Electron 会按文件系统解析。但有个坑:打包成 asar 之后,__dirname指向的是 asar 包内部,loadFile依然能正常工作,但如果你在 HTML 里用file://绝对路径就会挂。所以统一用相对路径,别写死盘符。
如果页面需要读取本地文件或者调用系统能力,通过 preload 脚本暴露有限接口,而不是直接开 nodeIntegration。这是安全底线,也是 electron-builder 打包时不会报警告的前提。
3. 全屏与退出机制:kiosk 模式、Esc 监听与防误触
3.1 kiosk 模式与 fullscreen 的区别
很多人以为fullscreen: true就够了,实际在 Windows 上它只是把窗口最大化到屏幕尺寸,任务栏仍然可能因为某些触发条件(比如鼠标移到屏幕底部)弹出来。kiosk: true是 Chromium 提供的独占全屏模式,它会接管整个显示区域,任务栏被完全遮挡,用户也无法通过 Alt+Tab 切出去(取决于系统设置)。
我一般两个都开:fullscreen: true保证启动即全屏,kiosk: true保证任务栏不露头。如果只开 kiosk 不开 fullscreen,某些 Electron 版本启动时会先闪一下窗口模式再进全屏,体验不好。
3.2 Esc 退出的三种实现方式
Esc 退出有三种做法,各有适用场景:
第一种是全局快捷键,就是上面代码里的globalShortcut.register('Escape', ...)。优点是即使页面失焦也能响应,缺点是会占用系统级快捷键,如果同时开了其他程序可能冲突。
第二种是在渲染进程里监听键盘事件:
// 在 index.html 的脚本里 document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { window.close(); // 需要 preload 暴露 close 方法 } });这种方式需要 preload 暴露window.close,因为默认情况下渲染进程不能直接关窗口。优点是只在页面聚焦时生效,不会干扰系统。
第三种是主进程监听before-input-event:
mainWindow.webContents.on('before-input-event', (event, input) => { if (input.key === 'Escape') { mainWindow.close(); } });这种方式介于两者之间,不需要全局快捷键,也不依赖页面脚本。我一般用第一种做展厅场景(保证一定能退出),用第三种做普通桌面工具(避免全局冲突)。
3.3 防止用户误触退出的加固
如果这是给客户用的展示程序,你可能不希望用户随便按 Esc 就退出。常见做法是加一个隐藏的退出组合键,比如 Ctrl+Shift+Q,或者连续按三次 Esc 才退出:
let escCount = 0; let escTimer = null; globalShortcut.register('Escape', () => { escCount++; if (escCount === 1) { escTimer = setTimeout(() => { escCount = 0; }, 1000); } if (escCount >= 3) { clearTimeout(escTimer); mainWindow.close(); } });这段逻辑是:1 秒内按满 3 次 Esc 才退出,否则计数清零。这样普通用户乱按不会关掉程序,维护人员知道暗号就能退。参数1000是毫秒,按需调整。
注意:globalShortcut 注册的快捷键在应用退出前要调用
globalShortcut.unregisterAll(),否则下次启动可能注册失败。
4. 打包与定制:图标替换、文件名修改与 electron-builder 配置
4.1 package.json 里的 build 配置
electron-builder 的配置全部写在 package.json 的build字段里。下面是一个针对 Windows exe 的完整配置:
{ "name": "my-kiosk", "version": "1.0.0", "main": "main.js", "scripts": { "start": "electron .", "dist": "electron-builder --win" }, "build": { "appId": "com.example.mykiosk", "productName": "展厅展示程序", "directories": { "output": "dist" }, "win": { "target": "nsis", "icon": "build/icon.ico", "artifactName": "${productName}.${ext}" }, "nsis": { "oneClick": false, "allowToChangeInstallationDirectory": true, "createDesktopShortcut": true, "shortcutName": "展厅展示程序" } } }关键字段解释:productName决定 exe 的文件名和安装后的程序名,这里写「展厅展示程序」,打包出来就是「展厅展示程序.exe」。win.icon指向图标文件,必须是.ico格式,尺寸建议包含 256x256。artifactName控制输出文件名,${productName}.${ext}表示用产品名加扩展名。nsis.oneClick设为 false 会生成带安装向导的安装包,如果只想生成绿色版 exe,把 target 改成portable。
4.2 图标制作的坑
Windows 的 ico 文件不是随便一张 png 改后缀就行。它需要包含多个尺寸(16x16、32x32、48x48、256x256),否则在某些场景下会显示模糊或者不显示。我一般用在线工具或者 ImageMagick 生成:
magick convert icon.png -define icon:auto-resize=256,128,64,48,32,16 icon.ico这条命令把一张 png 转成包含 6 个尺寸的 ico。如果没有 ImageMagick,用在线 ico 生成器也行,但注意别用那些带水印的。
4.3 打包命令与输出验证
配置写好后,运行:
npm run distelectron-builder 会自动下载对应平台的 Electron 二进制包,然后打包。输出在dist目录下,你会看到「展厅展示程序 Setup 1.0.0.exe」或者「展厅展示程序.exe」(portable 模式)。双击安装或直接运行,检查三件事:图标是不是你设置的、文件名对不对、全屏和 Esc 退出是否正常。
如果图标没换成功,八成是 ico 文件不合规或者路径写错了。如果文件名不对,检查productName和artifactName的拼写。如果全屏失效,确认kiosk和fullscreen都开了。
5. 避坑与常见问题:从白屏到任务栏弹出的排查记录
5.1 打包后白屏,开发时正常
现象:npm start能正常显示页面,打包成 exe 后打开是白屏。
原因:HTML 里用了绝对路径或者__dirname在 asar 包内解析异常。常见的是<script src="/js/app.js">这种以斜杠开头的路径,在文件系统里会解析到盘符根目录。
解决:把所有资源引用改成相对路径,<script src="./js/app.js">或者<script src="js/app.js">。如果用了 Vue/React 的 history 路由,改成 hash 路由,因为 file:// 协议不支持 history API。
5.2 任务栏仍然会弹出来
现象:已经设了fullscreen: true,但鼠标移到屏幕底部时任务栏还是冒出来。
原因:fullscreen只是窗口最大化,不是独占模式。Windows 的任务栏有自动弹出机制。
解决:加上kiosk: true。如果 kiosk 模式下仍有问题,检查是否被其他置顶窗口干扰,可以再加alwaysOnTop: true和setAlwaysOnTop(true, 'screen-saver')。
5.3 Esc 注册失败或冲突
现象:按 Esc 没反应,或者偶尔能退偶尔不能。
原因:globalShortcut.register返回 false 表示注册失败,通常是被其他程序占用了 Esc 键。另外如果在app.whenReady之前注册也会失败。
解决:在whenReady之后注册,并检查返回值:
const ret = globalShortcut.register('Escape', () => { mainWindow.close(); }); if (!ret) { console.log('Esc 注册失败,可能被占用'); }如果确实被占用,换用before-input-event方式。
5.4 打包体积过大
现象:一个简单的 HTML 页面,打包出来 200MB+。
原因:Electron 本身包含 Chromium 和 Node.js,基础体积就在 150MB 左右。这是正常现象,不是配置问题。
解决:如果对体积敏感,可以考虑用 Tauri 或 Neutralino 等更轻量的方案,但它们对系统 API 的支持不如 Electron 全面。对于展厅场景,200MB 完全可以接受。
5.5 安装后桌面快捷方式名称不对
现象:exe 文件名对了,但桌面快捷方式还是显示英文名。
原因:nsis.shortcutName没配置,或者productName和shortcutName不一致。
解决:在 build.nsis 里显式设置shortcutName为中文名,和productName保持一致。
6. 二次开发与改造:从固定页面到可配置 kiosk 框架
如果你只是封装一个固定页面,上面的代码已经够用了。但实际项目里往往需要更灵活:页面地址可配置、退出快捷键可改、窗口尺寸自适应不同分辨率。我一般会把配置抽出来放到一个config.json里,主进程启动时读取:
// config.json { "url": "index.html", "exitKey": "Escape", "exitCount": 3, "fullscreen": true, "kiosk": true }主进程里读取并应用:
const fs = require('fs'); const config = JSON.parse(fs.readFileSync(path.join(__dirname, 'config.json'), 'utf-8')); function createWindow() { mainWindow = new BrowserWindow({ fullscreen: config.fullscreen, kiosk: config.kiosk, frame: false, skipTaskbar: true, webPreferences: { preload: path.join(__dirname, 'preload.js') } }); mainWindow.loadFile(config.url); let count = 0; globalShortcut.register(config.exitKey, () => { count++; if (count >= config.exitCount) mainWindow.close(); }); }这样改页面地址不用动代码,改退出键也不用重新打包逻辑。更进一步,可以把 config.json 放在 exe 同级目录,打包时排除它,这样部署后还能改。
另一个常见改造是加一个本地 HTTP 服务,让页面通过http://localhost:port加载而不是file://。这样做的好处是可以用 fetch 请求本地接口、可以用 Service Worker、可以避免 file 协议的各种限制。用 Node 的 http 模块起一个静态服务就行:
const http = require('http'); const serveStatic = require('serve-static'); // 需要 npm install serve-static const serve = serveStatic(path.join(__dirname, 'www')); const server = http.createServer((req, res) => { serve(req, res, () => { res.statusCode = 404; res.end('Not found'); }); }); server.listen(0, '127.0.0.1', () => { const port = server.address().port; mainWindow.loadURL(`http://127.0.0.1:${port}/index.html`); });listen(0)表示随机分配一个空闲端口,避免和本机其他服务冲突。serve-static指向你的页面目录。这样页面里就可以正常用 fetch、localStorage、IndexedDB 等浏览器 API,不受 file 协议限制。
验证改造是否成功,我一般走三步:先npm start看开发模式是否正常,再npm run dist打包看 exe 是否正常,最后把 exe 拷到另一台没装开发环境的机器上跑一遍。第三步最关键,很多路径问题和依赖缺失只有在干净环境里才会暴露。
从那以后我每次做 Electron 封装,都会在打包后把 exe 拷到一台没装 Node 的机器上跑一遍,确认图标、全屏、退出、资源加载全部正常才交付。这个习惯帮我拦住了至少三次「开发机正常、客户机白屏」的事故。希望帮到你。
本文还有配套的精品资源,点击获取