将 Web 应用封装为桌面应用
Electron 简介
Electron 是基于 Node.js 的桌面应用框架,采用 Chromium / Node.js / Native API 架构,常用于封装 Vue / React 应用为桌面应用,也可直接对 url 进行封装。
本文采用 url 封装方式将 Streamlit Web 应用封装桌面应用。需要 Electron 中同时打包第三方 Web 应用,对 localhost:8501 进行封装,运行时首先拉起第三方应用,退出时一并结束。
安装
安装 Node.js / npm.
依赖库
axios 为主进程所依赖,不能放在 dev-dependcies 中,否则 Electron-Builder 不打包。
1 2
| npm install --save-dev electron electron-builder npm install --save axios
|
Electron-Builder 依赖
可能需要添加 winCode 签名文件。
运行
项目结构见 §1. 将 Streamlit 无头应用置于 resources/ 目录下。
初始化
采用 .bat 拉起第三方应用 + .js 拉起 .bat 的方式。这是由于没有找到很好的用 Electron 直接拉起第三方应用,并获取其 PID 的方式。(PID 用于桌面应用退出后杀死 Web 应用)
main.bat
1 2 3 4
| start resources/run_index.exe for /f "tokens=2 delims=," %%a in ('tasklist /nh /fi "imagename eq run_index.exe" /fo csv') do set pid=%%a echo %pid% :: pause :: 单独测试时可以加上 pause, 防止窗口一闪而过
|
main.js
main.js 是 Electron 的入口文件,笔者已经调通,可以直接套用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
| const { app, BrowserWindow, ipcMain } = require('electron') const { spawn, exec } = require('child_process') const axios = require('axios')
let webServerProcess = null; let intervalId; let processId;
async function killit() { return new Promise((resolve, reject) => { exec(`taskkill /pid ${processId} /f /t`, (error, stdout, stderr) => { if (error) { console.error('Error killing process:', error); reject(error); return; } if (stderr) { console.error('Error killing process:', stderr); reject(stderr); return; } resolve(); }); }); }
app.on('ready', () => { const script = spawn('cmd.exe', ['/c', 'main.bat']);
const win = new BrowserWindow({ width: 800, height: 600, autoHideMenuBar: true, alwaysOnTop: true, x: 0, y: 0 });
script.stdout.on('data', async (data) => { const pid = data.toString().trim(); processId = pid;
intervalId = setInterval(async () => { try { await axios.get('http://localhost:8531'); win.loadURL('http://localhost:8531'); clearInterval(intervalId); } catch (error) { console.error('Error accessing port 8531:', error); } }, 1000); });
script.stderr.on('data', (data) => { console.error(`Error: ${data}`); });
script.on('exit', (code) => { console.log(`Script exited with code ${code}`); }); });
app.on('window-all-closed', async function () { try { await killit(); } catch (error) { console.error('Error killing process:', error); } if (intervalId) { clearInterval(intervalId); } app.quit(); });
app.on('before-quit', () => { });
|
至此,运行 electron .
即可启动桌面应用。
Electron Builder
基于 Electron 封装桌面应用的方式很多,如 Packager, Builder 等。这里采用 Electron Builder.
主要关注 package.json 中的 build/extraResources 字段,在应用中一并封装第三方 Web 应用和 main.bat 脚本。在 extraResources 中配置数据文件路径,注意封装后数据文件的默认路径为 resources,所以想要 main.js 搜索文件的路径不变,则 ./*
封装后的路径应为 ../*
,类似地,resources/*
封装后的路径应为 ./*
。
区别于常见的 nsis 打包,设置 target=portable
打包为单个文件。
package.json 内容如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| { "name": "trainingtools-v0.6", "version": "1.0.0", "main": "main.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "start": "electron .", "build-win": "electron-builder --win" }, "build": { "appId": "com.trainingtools.desktop", "productName": "trainingtools-v0.6", "win": { "icon": "./static/icon/hisilicon-256-256.ico", "target": [ { "target": "portable" } ], "extraResources": [ { "from": "./main.bat", "to": "../main.bat" }, { "from": "./resources/run_index.exe", "to": "./run_index.exe" } ] } }, "author": "User@email", "license": "ISC", "description": "DataEyeTraining", "devDependencies": { "electron": "^32.1.2", "electron-builder": "^25.0.5" }, "dependencies": { "axios": "^1.7.7" } }
|
运行 npm run build-win
进行 Windows 桌面应用打包。
关于 Icon
Electron-Builder 要求 icon 至少提供 256×256 尺寸。此外,默认产物路径下偶发 icon 显示异常,可重命名或移到其他目录。
Q & A
cannot find module
axios 是运行时依赖,不可装在 dev 下
装包 response code 404
源问题,索引与实际包仓库不匹配
引用 axios 报错 TypeError cannot read properties…
由引用格式引起,axios 外面禁止加大括号
体积问题
Electron 打包时可能存在缓存积压,同一目录多次打包后体积暴涨。所以在正式打包前删除 node_modules 和 package-lock.json 重装所有依赖。
其他打包方案
Nativefier
封装了 Electron 打包 url 的能力,可以使用 Nativefier + Streamlit Share 的方式直接封装 Streamlit App.
踩坑
- Node.js 的
spawn
用于执行命令,其无法返回所拉起脚本的真正 PID