前言
在《屠龍少年終成惡龍,前端轉產品的我給前端挖了個坑》這篇文章裏,有講到我是如何把我們的前端帶坑裏去。同時評論區有一條評論 你那個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 社區中發現有 https://www.npmjs.com/package/rpc-websockets 這個包可用,還兼容 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 https://github.com/wll8/sys-shim 。
好了,餅畫了,牛吹了,坑挖了,我要去玩了。