前语
在《屠龙少年终成恶龙,前端转产品的我给前端挖了个坑》这篇文章里,有讲到我是如何把咱们的前端带坑里去。一起谈论区有一条谈论 你那个demo.exe是用什么完结的?能够直接套壳现有的体系?
看起来如同是在坑外徜徉不定若有所思的姿态。因此我打算写本文把他踹坑里去,能踹一个是一个。
不想用 electron 和 tauri ?那咱们一起来写个像 electron 的垃圾玩意吧~ 咱们的方针是:前端程序员无需会三方言语就可独立完结桌面程序,创立托盘程序和服务、读写文件、处理进程、剪贴板这些都没有问题,估计体积1M内,最大不超过2M。
为什么做
我当时现已运用 nodejs 开发一个命令行程序,这个程序的东西办法是,从网络上获取动态的装备,然后读取这个装备进行发动。发动后就能去做其他额外的工作了,而不需求管这个程序。由于这个程序仅仅一个辅助东西。
可是现在有一些痛点:
每次发动的时候我都得先找到项目目录,然后运转 node xxx.js
,然后发动一个黑框框,然后我再最小化这个框。发动进程相当费事而且有一个不必管的窗口在任务栏,相当碍眼。
有许多办法能够处理发动问题,比方 pm2/快捷链接/大局装置/制作 PKG 装置包等。但各自有各自的问题,这儿不一一列举。
关于有一个黑框需求最小化到任务栏问题,我测验过运用 node child_process 的 detached=false, windowsHide=true
等参数合作 pm2 都是没有用的,黑框仍是会弹出。 假定有用,我要的也不仅如此。
我觉得这个东西不错,我想要把这个东西发给他人运用,尽管这个东西是 nodejs 写的,但我不希望他人还要去学习装置 nodejs 环境。尽管这个东西是命令行发动,并支撑参数装备,但我希望像惯例程序相同,他人点击一个图标就能发动,能够从界面上装备参数。能够在界面上看到程序的实时日志,最小化之后,变成一个小图标在任务栏,不占空间不碍眼。
那么问题来了,由于我经常用 html/css/js 画界面,对许多前端组件库比较了解,所以我打算用前端写界面。但 js 是跑在浏览器里的,读取不了保存在电脑里的装备文件,更完结不了托盘图标功用,也运转不了 node 程序。
据我所知,像这种想运用前端言语开发界面,又需求与操作体系进行交互的功用,有不少方案。下面是我对他们的调研成果:
名称 | 前端 | 后端 | 体积 MB | 内存 MB | 抛弃原因 | 补白 |
---|---|---|---|---|---|---|
nodegui | chromium | nodejs | 100 | 100 | 体积大 | |
miniblink49 | Chromium | nodejs | ? | ? | 体积大 | 仅支撑 window |
NW.js | Chromium | nodejs | 100 | 100 | 体积大 | |
electron | Chromium | nodejs | 100 | 100 | 体积大 | |
Wails | webview | go | 8M | ? | 需其他言语 | |
Tauri | webview | rust | 1 | ? | 需其他言语 | |
Qt | 可选 | C++ | 30 | ? | 需其他言语 | |
wpf | 可选 | C# | ? | ? | 需其他言语 | 仅支撑 window |
Muon | Chromium | go | 42 | 26 | 需其他言语 | |
Sciter | Sciter | QuickJS | 5 | ? | 与一般浏览器和 nodejs 可能有差异 | |
gluon | 浏览器 | nodejs | 1 | 80 | 生态小,例如没有找到托盘图标完结办法 | |
neutralino | 浏览器 | API | 2M | 60 | api 不多 |
当时咱们比较火有 electron 和 tauri。四年前我运用过 electron 做过一个桌面划词程序,由于涉及到体系操作,所以需求装置 node-gyp/pytohn/visual studio 等依靠来进行本地编译,能否操作成功与 electron/node/node-ffi 等版别兼容性有很大的联系,装置进程和 electron 的体积都给我留下了欠好的印象,另外 electron 里的主进程、烘托进程、通讯的一些运用上的差异,也让我觉得不那么便利,所以我抛弃 electron 。
接下来就是 tauri,它由于不打包 nodejs 和 chromium ,所以体积较小。但我看他官网上的 demo,就连发动都 rust 代码。
尽管代码没几行,但我也是相当回绝:说好的只运用前端言语就能写桌面程序呢?
所以我抛弃了 tauri 。原因是我真想找一个不运用三方言语就能做桌面程序的东西。我发现 neutralino 比较贴近我的需求,但它当时还很年轻,许多 api 和示例都没有。这相当于假如遇到了操作体系层面上的问题,只需他不供给 api 我就没法操作,由于我不会写原生代码,所以又抛弃了 neutralino 。
所以就自己做一个吧。
预备怎么做
预备运用当时了解的一个言语做一个基于 webview 的东西,咱们暂且叫 main。它加载好前端页面,并向前端页面注入 api 并连接上 websockets 。假如前端有什么对体系操作的诉求,告诉 main 即可,由 main 完结,关于前端而言,就像调用一个一般的 js 办法相同,传参、处理成果、完事。
言语名为 aardio ,由于“各自原因”这儿不做过多叙述。后边文档中统一称其为体系言语。
那么为什么都去搞一个言语了不搞 rust 这些?有几点考虑:
- 供给了 js/webview/nodejs 相互调用的例子
- 供给了一些常见的体系托盘、窗口操作示例等
- 我对作者保护这个言语这么多年心存敬畏
程序的全体架构是这样的:
- 装备层:常用的定制化需求,都能够经过一个 json 装备文件解决。js处理起来也简略。
- 依靠层:比方注入到 web 页的经过封装的 js 文件。
- 内核层:完结与 web 页面的通讯,满足 web 页面对体系进行操作的惯例诉求。
- 东西层:例如健壮性、安全性、主动晋级、调试、打包、发动等。
做成了什么样
下面这个图片演示了发动程序时,有一个绿色的进度条(不会遮挡鼠标),然后进入界面。
现在已过可行性验证阶段,给客户做了一个文件办理体系程序,相似一个网盘,页面由前端完结,然后文件的下载、预览、同步这些交给 main 供给的 api。
下面这个图片演示了在 web 中关闭程序。
关于自己的话,做了一个 ai 助手,对接的开源 ai-ui,已发给同事运用,也没有问题。做了一个文章开关说到的助手程序,自己运用。
再次演示一下通明窗口,上面的发动时的进度条也是运用通明窗口完结的。
演示自定义窗口标题和托盘。
程序发动时的进度条也是运用 html 完结的
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>loading...</title>
<style>
body,
html {
height: 100%;
overflow: hidden;
}
body {
display: flex;
align-items: center;
justify-content: center;
}
@property --progress {
syntax: '<percentage>';
inherits: false;
initial-value: 0%;
}
.g-progress {
margin: auto;
width: 240px;
height: 10px;
border-radius: 25px;
background: linear-gradient(90deg, #0f0, #0ff var(--progress), transparent 0);
border: 1px solid #eee;
transition: .3s --progress;
}
</style>
</head>
<body>
<div class="box">
<div id="progress" class="g-progress progress-bar-striped" style="--progress: 10%"></div>
</div>
</body>
</html>
遇到的问题及处理方案
官方示例中给到的 webview 交互示例经过 external 注入到页面的 window 上,经过此办法能让 js 中的数据和 main 进行类型转化(比方 js 里传一个 number,那么到 main 里也是 number),还供给了一些能够直接启用 main 里目标办法的操作。好用是好用,可是与 nodejs 交互的时候,没有这种主动转化的功用,而且示例中的 node 服务连接很慢。
为了让 main 支撑 webview 和 nodejs,而且运用办法统一,而且加快发动速度,查了一些材料,发生像这种跨言语通讯通常都是运用 rpc 协议完结的,有 json-rpc/http-rpc/rpc-ws 等,为了实时性更强,我挑选了 websockets 这种办法, 我 npm 社区中发现有 www.npmjs.com/package/rpc… 这个包可用,还兼容 node 和浏览器,测验往后挑选了它,这解决了跨言语通讯问题。
另一个问题是,mian 中有许多办法是现成的。比方以下代码在 main 中能够运用:
// 有一个 winform 目标
winform.hitMax() // 最大化
winform.show() // 显示窗口
winform.hwnd // 获取窗口句柄
winform.hitCaption() // 拖动窗口
winform.text = "title" // 设置窗口标题
// ... 上百个现在的办法和特点
假如咱们要为 js 供给 api, 咱们是每个特点和办法都得去写吗?这又费事,代码又还臃肿。
经过一波挣扎,我想起了运用署理这种办法去完结,仍是 js。
const obj = new Proxy(
{},
{
get: function (target, key, receiver) {
console.log(`getting ${key}!`);
return Reflect.get(target, key, receiver);
},
set: function (target, key, value, receiver) {
console.log(`setting ${key}!`);
return Reflect.set(target, key, value, receiver);
},
}
);
依据 proxy,咱们能够完结拦截到某个目标的办法调用和特点访问、设置等。再加上深层署理的话,像 winform.process.close()
这种有任何层办法特点都没有问题。
一起,在 main 中咱们有这样的代码,来处理 proxy 拦截到的每个 key path:
咱们把拦截到的 path ,比方在 js 里写 winform.process.close(true)
的时候,咱们把拦截到的 winform.process.close 和参数 true 经过 rpc-ws 供给的 call 办法传给 main,这时候 main 依据 path 去动态调用函数并把参数传进去。咱们把履行成果又丢给 call 办法回来给咱们的 js 即可。
那么问题又来了,既然都完结了在 js 里调用办法和访问特点都像在写 main 中的代码相同,那真的就能不能以 js 的方式去写 main 的代码呢?看了一天的教程,发现这水很深啊,约等于发明一门言语,怕了怕了,逃。
可是思路着要有吧?好的:
假如简略一些呢,咱们依然能够运用 proxy,完结操作符的拦截,从而完结一些简略的加减乘除的操作。然这没什么用啊,咱们要完结的是比方用 js 里对 winform 目标进行遍历之前,咱们就要做一个生成器之类的东西,在生成器的每一步里,去获取 main 里的遍历成果。感觉上如同能完结,实际我也不知道我在说什么。可是就算完结了,像这种遍历器,频频的言语交互应该会耗费很多时刻,感觉应该因小失大。
所以在 js 里获得 main 中言语的编写体会,就不完结啦。假如咱们真的要在 js 里写另一种言语,咱们开放一个相似 js 的 eval 的功用。它能够向 main 传原生代码和参数。
// 创立目录
const dir = `C:/my/`
await ws.call(`run`, [
`
fsys.createDir(arg)
`, dir])
例如上面这段代码,直接传送目录参数 C:/my/
到 arg,运用原生言语 fsys.createDir(arg)
去履行。
后期方案是什么
方案一:运用 main 去做更多的桌面 app,以此促进 main 的完善。
方案二:为某个当时老练的 ui 框架拟定一套 css 皮肤,例如 win7 皮肤 ,例如 element-ui 姿态很 web,但应用了这个皮肤之后,全体页面风格和控件都看起来就像原生 win7 桌面程序相同。
方案三:赶快完结 api 的封装和文档,让前端朋友只调用指定的 js api 即可完结托盘、进程、剪贴板、IO等体系操作。咱们封装的 api 尽量向 neutralino 接近,做到最小成本的迁移。等它老练在,能够迁入,没老练之前咱们也能自己用着。
需求什么协助
能够帮咱们封装 api,这需求你了解 main 的言语;能够用 main 来做些小东西测验一下,这就是最好的协助;能够做操作体系风格皮肤,等你做好了,electron 和 tauri 他们都能用,由于他仅仅 css;或者能够点个 star github.com/wll8/sys-sh… 。
好了,饼画了,牛吹了,坑挖了,我要去玩了。