對於網頁開發者來說,開發桌面小工具寫成網頁再轉桌面應用程式是最省時省力的選擇,而 Github 開發的 Electron 則是最流行的網頁轉桌面應用程式框架,大家日常使用的軟體中有許多就是用 Electron 開發的,例如:Discord、Microsoft Teams、Skype、Slack、Whatsapp,而這幾年橫掃開發界的 Visual Studio Code 更是其中經典。

對 ASP.NET MVC/ASP.NET Core 開發者來說,則有 Eletron.NET 能快速整合 Electron 與 ASP.NET Core,輕鬆將 ASP.NET Core 包成 Electron 應用程式。關於 Electron.NET,在去年有寫過文章介紹,有興趣的同學可參考:用 ASP.NET Core 寫桌面 GUI 應用程式 - Electron.NET

Electron 雖然威力強大,對我來說卻過於繁瑣笨重,開發需下載安裝 Electron CLI、額外設定,而發佈檔案因包含 Chromium,容量往往達到數百 MB,這個大小對 Teams、Slack、VSCode 等中大型應用程式仍算合理,若拿來寫批次改檔名、加解密之類的「小」工具就顯荒謬,也違背我追求的極簡風格。

見識過 ASP.NET Core Minimal API 的輕巧,我心生一念,何不用 ASP.NET Core 專案內嵌 HTML、.js 跑介面、呼叫 Minimal API MapPost("...") 寫 WebAPI,再呼叫客戶端一定有的瀏覽器開啟我們的網站,靠一個小小 EXE 搞定所有事情,這才符合極簡主義。

這個構想要實作不難,這篇先談如何動態決定 HTTP Port 並啟動瀏覽器。

ASP.NET Core 編譯發行的 exe 檔,執行時 Kestrel 預設會聽 http://localhost:5000https://localhost:5001,而我們也可透過 ASPNETCORE_URLS 環境變數、--urls 啟動參數、appsettings.json urls 設定以及 UseUrls() 方法指定 Port 參考,但小工具在客戶環境不需安裝,執行時無法確保 Port 未佔用故難以預先指定,甚至可能同一程式跑兩份,若預先指定則執行者的便會因 Port 被佔用無法繫結出錯:

因此,較理想做法是隨機挑選當時可用的 TCP Port。很幸運地,Ketrel 也有內建支援動態決定 Port,做法很簡單,只需將 Port 指定為 0 即可

小改程式,加上 app.Urls.Add("http://127.0.0.1:0"),Kestrel 便會在啟動時自動找到可用 Port,同一支程式跑兩次也不會打架:

由於 Port 隨機決定,啟動瀏覽器時需用點技巧查詢該次使用的 Port 以決定 URL。要啟動預設瀏覽器,若只考慮 Windows,用 Process.Start 呼叫 cmd.exe /c start url 即可。Kestrel URL 需在 app.Run() 啟動後透過 IServer.Features.Get<IServerAddressesFeature>() 取得,所以我們改寫成非同步 RunAsync(),取得 URL 啟動瀏覽器,再等待 RunAsync() 傳回的 Task 結束。另外,由於 VSCode 或 Visual Studio F5 偵錯時本來就會啟動瀏覽器,為避免重複,我加上 Debugger.IsAttached 檢查,只在獨立執行時啟動瀏覽器。

完整範例如下:

using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Hosting.Server.Features;
using System.Diagnostics;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.Urls.Add("http://127.0.0.1:0");
app.MapGet("/", () => "Hello World!");

var task = app.RunAsync();

if (!Debugger.IsAttached)
{
    var url = app.Services.GetRequiredService<IServer>()
        .Features.Get<IServerAddressesFeature>()!
            .Addresses.First();
    Process.Start(new ProcessStartInfo("cmd", $"/c start {url}")
    { CreateNoWindow = true });
}

task.Wait();

測試成功。

下一篇,來談談怎麼在瀏覽器關閉時結束 ASP.NET Core 程式。

Tips of how to binding available portin ASP.NET Core and lauch browser to open the default url of our web.


Comments

# by C.C.

除了 Electron, 其實微軟也有出了一個用edge engine的新webview (https://developer.microsoft.com/en-us/microsoft-edge/webview2/), 雖然本質都一樣是自帶一個browser, 不過它卻有一個運作模式是可以呼叫個別安裝在windows上的共用版本, 那就不用自帶著數百MB的browser了 而win11似乎預設已有安裝這個共用的webview 有興趣的話不妨看看

# by Jeffrey

to C.C.,感謝分享,WebView2 也是不錯的思考方向,我想到的小缺點是需引用 WinForm、WPF 等 GUI 技術,增加複雜度與相依性,但操作介面密切整合是一大優勢,能滿足某些情境的需求。

# by Off-Duty Sorcerer

黑大你好,我也是先學網頁但有時也會有開發桌面小APP的需求,現在通常用winform或WPF亂寫一通。 最近在玩AI繪圖時,發現它整個應用程式的樣子就很像你描述的那樣,以web 當成ui,實際程式跑在背後一個小黑視窗。 沒想到.NET CORE也可以做到一樣的事!? 小弟目前還不太熟悉.NET CORE,也不知道怎麼把網站編譯成一個執行檔,不知道是否有完整範例可供參考試玩? 感謝

# by Jeffrey

to Off-Duty Sorcerer, 我有將這套做法包成 NuGet 套件,https://blog.darkthread.net/blog/electron-net-alternative/ 你目前的重點可放在學會怎麼寫 ASP.NET Core,之後用引用套件加幾行程式便可將 ASP.NET Core 網站轉成桌面程式。

# by Off-Duty Sorcerer

看起來好像ASP.NET Core Minimal API可以發佈成.exe檔的樣子,我一直搞不清楚這跟一般的Web API有什麼不同...感謝指引!

# by Off-Duty Sorcerer

動態Port那裏不知道為什麼設定起來怪怪的,但其他部分都可以用了!發佈時用--self contained在沒有權限安裝.NET的機器上也可以用得很開心!再搭配SQLite和ef core用起來更舒適了,讚讚讚!

# by Off-Duty Sorcerer

更正是--self-contained才對

Post a comment