同一份版面定義,現在也能渲染成 Browser 裡的 Web 前端

上一篇〈ERP 該為哪一代前端技術下注?〉聊到,前端是一層「依定義渲染」的皮。畫面長什麼樣,由 FormLayout 這份版面定義決定;至於由哪一種前端技術去把它畫出來,是可以替換、也可以並存的。
也就是說,從一份版面定義出發,前端是會分岔的,同一份定義可以渲染成 Desktop 程式,也可以渲染成 Browser 或 Mobile App 的畫面,這一直是框架的設計意圖。而這次要分享的,是其中 Avalonia Browser(Web)這一條分支已經做出來,可以實際在 Browser 裡跑了。
進正題前,先澄清一個容易被誤解的點:「定義驅動的前端」並不代表前端只能由定義生出來。
定義負責的是清單、主檔、主從單據這些規律的制式表單,靠 FormLayout 自動長出來,這部分是 no-code。但前端不是只有這條路,你也可以自己刻畫面,用 TableName / FieldName 的繫結把控件接上 ViewModel,照需要的版面手工排(low-code),甚至完全自訂一套畫面與互動(any-code)。
不論畫面是定義生出來的還是手工排的,繫結機制都是同一套,控件一律按 TableName / FieldName 接資料,差別只在版面由誰決定。連後端也走同一條路,no-code、low-code 或自刻的 any-code 畫面都透過同一個 connector 跟後端互通,後端介面一致,前端怎麼做都不影響它。
所以實際做一個應用,是依場景在 no-code、low-code、any-code 之間選:規律的制式表單交給定義,特殊或互動複雜的畫面自己刻。這跟後端「CRUD 由定義驅動、報表批次自己寫」的雙軌,是同一個取捨思路。
下面要講的 Avalonia Browser,就是把這整套(含定義驅動與自刻兩種做法的同一組控件)一起帶到 Browser 的其中一條前端分支。
前面說每種前端都透過同一個 connector 跟後端互通,把它放進框架的相依結構看會更具體:不管哪種前端技術,最後都收斂到 Bee.Api.Client 這個 connector,再由它跟後端對話。

原生 UI 家族(Avalonia、MAUI)透過 Bee.UI.Core 共用連線狀態與 endpoint 邏輯,Blazor 則直接接 Bee.Api.Client。這篇的主角 Avalonia Browser 不是圖上另一個方塊,而是 Bee.UI.Avalonia 涵蓋的多個平台 head 裡的 Browser 那一個;其中 Desktop 與 Browser 已完成,Mobile 還在進行中(詳見結語)。
(這只是擷取跟前端有關的一段;完整的 17 個專案相依圖見 dependency-map.zh-TW.md。)
上一篇提過,Avalonia 這條線,是我拿來把「繼承原生控件、與定義深度整合」這套做完整的。那些懂定義的欄位編輯器、可編輯的明細表格、開窗挑關連資料的按鈕、master-detail 的版面,都是在這條線上累積出來的控件。
Avalonia Browser 沒有為了 Web 把這些重做一遍。Desktop 版和 Browser 版用的是同一份 Avalonia 程式,從 App、ViewModel、View 到底層控件全部共用,差別只在最外層的平台 head,一個掛在 Desktop 的視窗環境,一個編成 WebAssembly 掛進 Browser。所以它不是「為了 Web 另外接的一套前端」,而是同一套 Avalonia 前端的另一個出海口。
好處是 Desktop 上打磨好的控件,Browser 版直接就有,行為也一致。同一個關連欄,不管在 Desktop 還是 Browser,對使用者來說做的都是同一件事,開窗挑一筆、把代碼和名稱一起帶回來。控件這層不必為平台分叉,省下來的就是不必為 Web 再養一套 UI;後端更是完全不用動,它本來就只透過 JSON-RPC 對話,不在乎連上來的是 Desktop 還是 Browser。
下面這張是 Northwind 範例在 Browser(localhost:5200)裡跑的訂單表單,master-detail 加上 Customer / Employee / Shipper 三個關連欄,明細列還有自己的產品關連欄,全部是 Desktop 那套控件原封不動編成 WebAssembly 的結果:

想自己跑起來,後端與 Web client 各一行指令:
# 後端(JSON-RPC,http://localhost:5100)
dotnet run --project Bee.Northwind.Server
# Web client dev server(Avalonia WASM,http://localhost:5200)
dotnet run --project Bee.Northwind.Browser
(Browser 端需先裝 wasm-tools workload;連線與 demo / demo 登入等完整步驟見 Bee.Northwind.Browser README。)
Avalonia 的控件都收在 src/Bee.UI.Avalonia 這個 net10.0 控件庫,Desktop 與 Browser 兩個 head 都引用它,裡面的東西兩邊直接共用。
每個欄位該用哪種編輯器,由定義裡的 ControlType 決定,框架照它建出對應的控件:
ControlType |
用途 |
|---|---|
TextEdit |
單行文字 |
MemoEdit |
多行文字 |
DateEdit |
日期 |
YearMonthEdit |
年月 |
DropDownEdit |
下拉選單 |
CheckEdit |
勾選 |
ButtonEdit |
關連欄,開窗挑一筆帶回外鍵與顯示欄 |
每個 ControlType 在 Bee.UI.Avalonia 裡都對應一個同名的編輯器控件(ControlType.TextEdit 對應 TextEdit,依此類推),各自繼承原生控件、懂自己那個欄位的定義;另有 Auto,讓框架依欄位型別自動挑一個。
畫面層的兩個容器也一樣共用:ListView 負責清單瀏覽,FormView 負責單筆檢視與編輯(master 區加明細表)。這些編輯器加上這兩個 View,就是上一篇講的「繼承控件 + View 層」那一整套。
真正要為 Web 另外處理的,集中在兩個跟平台能力有關的點。
第一個是連線設定存哪裡。Desktop 用 FileEndpointStorage 寫成檔案,Browser 沙箱不能寫檔,改用走 localStorage 的 BrowserLocalStorageEndpointStorage。兩者都實作同一個 IEndpointStorage 介面,在 head 指定一行就替換掉,控件那層無感。
第二個是對話框怎麼呈現。Browser 沒有原生視窗,Window.ShowDialog 會直接拋例外,所以 LookupDialog 和 RowEditDialog 內部用 OperatingSystem.IsBrowser() 分一條路,Desktop 照開原生 Window,Browser 把同一個 LookupPanel、RowEditPanel 掛到 OverlayLayer 上的浮層。挑資料、編明細的邏輯是共用的,差別只在最後裝它的容器。
把 Web 這條分支補上之後,定義驅動的前端就多了一個實際能跑的落點。Desktop 和 Browser 共用同一套 Avalonia 控件,要為 Web 另外處理的也就前面那兩點。
順著同一條路再往下,是把這套控件帶到 Avalonia 的 Mobile(Android / iOS)。原理跟 Browser 一樣,多接一個平台 head、控件本身不用重做,再補上 Mobile 端單一視窗、觸控與小螢幕該處理的地方。這條還沒做,之後再來分享。(補一句避免混淆:Bee.UI.Maui 是另一條獨立的前端分支,跟這裡講的 Avalonia Mobile 不是同一回事。)
📘 HackMD 原文筆記:
👉 https://hackmd.io/@jeff377/avalonia-browser-frontend
📢 歡迎轉載,請註明出處
📬 歡迎追蹤我的技術筆記與實戰經驗分享
Facebook | HackMD | GitHub | NuGet