我把核心押在後端與定義,前端當一層可換的皮

做 ERP 系統這些年,有一件事再清楚不過:真正的核心資產是後端邏輯。業務規則、流程、資料結構的生命週期動輒十幾二十年;相對地,前端技術換得飛快,每隔幾年就有新的當紅選擇。把心血綁死在某一代前端技術上,系統往後演進會很痛。
幸好 ERP 的畫面相當「制式」,清單、表單、主從單據、查詢,版型來來去去就那幾種。既然這麼規律,我把它定義化,讓一份定義成為整套系統的單一來源。這是我的框架 Bee.NET 的核心做法,也是我在業界用了很多年、主流框架卻很少這樣走的取法,想藉這篇分享出來。
為了驗證這套做法,我用它做了一個 Northwind 進銷存範例。如果只想先抓重點,最有感的是這幾件事:
下面就一個個講這幾點怎麼來的。
定義驅動最省事、也最好維護的一點,就是「同一份 FormSchema 可以餵給不同前端」。我在 samples 裡陸續放了幾個最小可跑的範例,把各種前端都接到同一個後端試過一輪:
連線策略分兩種:Local(同行程,效能最好)與 Remote(HTTP)。對「能不能接上」來說,這些範例已經給了答案:可以。
但這些範例證明的都是同一件事:能接上。我真正在意的,是接上之後那一段。
把一個 TextBox 擺到畫面上、綁一個欄位的值,這誰都會。可是定義裡寫的遠不只「值」:
MaxLength)?ListItems)?ReadOnly)?光綁值遠遠不夠。我要的是控件自己讀懂這些 metadata、自己改變行為。那問題就只剩一個:這個「讀懂」該擺在哪?
我的目標很明確:讓前端開發者幾乎不用碰 UI 細節,把心力都留給真正重要的商業邏輯。要做到這點,控件就得和定義「完全整合」。而我一路試下來,能做到完全整合的,就只有把原生控件繼承下來改寫這條路。
一般的做法,是直接用原生控件,再用外部的 binding 或 behavior 把定義接上去。控件是乾淨,但定義在一邊、控件在另一邊,中間永遠隔著一層膠水,每加一個欄位都得在外面重接一次,前端開發者還是得管一堆 UI 接線。
繼承改寫就沒這個問題。我把原生控件繼承下來,做成一個天生就懂定義的子類:
public class TextEdit : TextBox, IFieldEditor
{
// 綁到資料物件的某個欄位;自動套用該欄位定義的 MaxLength 等 metadata
}
呼叫端擺一個 TextEdit,它自己會去讀對應欄位的定義,外面什麼都不用接。對前端開發者來說,UI 這層幾乎是透明的,擺上去就對了,剩下的注意力全留給商業邏輯。
代價是要改得動原生控件,得先摸熟它的底層細節,主題樣式、編輯行為這些地方都有眉角,需要在框架這層多花心力。但反過來,應用端就幾乎不必碰這些。
再往下,我把共用的綁定狀態(接哪個資料物件、哪個欄位、值怎麼回寫、怎麼擋掉 echo 迴圈)收成一個元件,再加上一個方便的設計:容器設一次資料來源,底下每個帶欄位名的編輯器就自動接上去,擺上畫面就會動,一個都不用手接。
同一套「繼承原生控件、讓它懂定義」的原則,一層層往上套,就長成一整張表單。
最底層是各種欄位編輯器:文字、多行、日期、年月、下拉、勾選,還有關連欄用的按鈕編輯器。每一個都是某個原生控件的子類,各自讀懂自己那個欄位的定義。
往上一層是表格控件,把欄位編輯器組成一張可編輯的明細表;要顯示哪些欄位、哪些可編輯,一樣由定義決定。
最上層是畫面層。我把它拆成兩個容器,對應 ERP 最常見的兩種畫面:一個負責清單瀏覽,一個負責單筆檢視與編輯(master 區加明細表)。這條線是順著使用者的習慣切的,不是技術限制。
最能體現這套原則的是關連欄。定義裡標一句「這個欄位關連到哪張表、要帶回哪些欄位」,版面產生器就自動把它變成一個會開窗挑選的按鈕;選完外鍵和顯示文字一起寫回,重載時再由後端 JOIN 算回來。呼叫端一行 UI 都不用寫,定義驅動的省事,在這裡最明顯。
前面講的這套(繼承控件、拆出 View 層),我沒有一次鋪到所有前端,而是先挑一個來試做,選的是 Avalonia。為什麼是它?
因為 Avalonia 本身就是跨平台:同一套 UI 程式碼,再各自配一個輕薄的平台啟動專案,就能跑在桌面(Windows / macOS / Linux)、Web(WebAssembly)和行動 App 上。把深度整合的控件做在它上面,這同一套控件未來就有機會一路延伸到 Web 和 App,不只是服務桌面。一次打磨、多處受惠,這是其他只綁單一平台的前端給不了的。
目前我先用桌面程式把這些前端控件實作出來,Web 和行動版之後再做。也因此這套「繼承控件 + View 層」的深度整合,現階段只在 Avalonia 上做完整;其他前端(如 MAUI、Blazor)還停在比較簡單的動態表單渲染,等這邊定稿再談移植。
為了讓這套東西不只是「控件展示」,我做了一個比較完整的示範應用 bee-northwind-avalonia,用大家熟悉的 Northwind 進銷存案例。

它正是整個框架的核心重點:這九張表單裡,有八張完全是純定義產生的,一行 UI 或 CRUD 程式碼都沒寫。要新增一張主檔、一張帶多重 lookup 的單據、或一組 master-detail,對開發者來說就是寫幾個定義檔:用 FormSchema 描述欄位與關連、配上版面與資料表定義,存檔重啟,連資料表都依定義自動建好,一張能新增/查詢/修改/刪除、會開窗挑關連資料的完整表單就出現了。
還有一點,讀者大概會更有感:翻遍整個專案,你一句 SQL 都找不到。建表、查詢、增刪改,背後該下的 SQL 全是框架照定義即時組出來的,開發者從頭到尾不必手寫。
而且既然 SQL 是框架照各家資料庫的方言組出來的,同一套定義就能換著資料庫跑:目前支援 SQL Server、PostgreSQL、MySQL、Oracle 與 SQLite,要換一家只是改設定。示範為了方便,用的是免安裝的 SQLite。
唯一寫了程式碼的是訂單那張:單號怎麼編、狀態能不能轉、金額怎麼算,這些才是真正的商業邏輯,我才動手寫一個業務物件,其餘全部交給定義。這正是我要的分工:開發者的時間該花在商業邏輯上,不是花在把一個又一個欄位接到畫面上。
它純粹引用已發行的 NuGet 套件,clone 下來就能跑,算是這套做法目前能做到什麼程度的體檢。
這套做法還有一個更不主流的地方。前面那些編輯器綁的不是強型別 DTO,而是 DataSet,所以都是「按欄位名」取值,而不是綁某個強型別屬性。
現在主流幾乎都用強型別 DTO,DataSet 這種老東西早就少有人提了。但放在定義驅動底下,它反而更搭:資料的形狀本來就是執行時依定義長出來的,用一個按欄位名存取的鬆散容器,比維護一大堆要跟著定義同步的 DTO 類別省事得多,定義改一個欄位,也不必回頭改一串對應的 class。
當然它有代價,而且很直接:沒有編譯期型別檢查,欄位名打錯、型別取錯,編譯器不會吭一聲。這正是當年大家離開 DataSet 的理由。
補這個洞,我靠的還是定義。既然一切都從定義長出來,我就拿定義當單元測試的底,對著它驗證行為、把覆蓋率拉高,讓測試去抓那些編譯器幫不到的錯。強型別靠編譯器擋錯,我靠定義加測試擋錯,只是把保護網從編譯期挪到了測試。定義驅動這種場景,用 DataSet 反而比較合適。
AI 對結構化資料的理解力很高,而定義檔正好是結構化的,對 LLM 來說特別好讀。再搭配一份描述這些定義檔彼此關係的 skill(FormSchema、版面、資料表定義怎麼對應、哪些欄位要一致),AI 的理解會更到位,要照需求生成一整組正確的定義檔也更容易。新增一張表單,也就能從「人寫定義」變成「描述需求、讓 AI 產出定義」。前面那個 Northwind 示範就是現成例子:裡頭的 FormSchema、版面、資料表定義,全部都是我描述需求、由 AI 產出來的。
相比之下,對於 ERP 這種大型又複雜的系統,邏輯若散在程式碼各處,AI 要正確改動會非常吃力;而定義集中又結構化,反而剛好接得上這個需求。
廣度上,同一份定義已經接得上各種前端;深度上,我先在 Avalonia 把這套「繼承控件、拆出 View 層」初步做起來,已經能組成一套應用。
繼承控件不是免費的:要改動原生控件得懂它的底層,有一定難度。小專案不一定划得來,偶爾接一兩個欄位,照一般做法用個 behavior 反而更省。但 ERP 不一樣,維護期動輒十年以上,這種前期投入攤到那麼長的生命週期就很有價值:在框架這層多花一次心力,把前端收斂成依定義生成的一層皮,換來的是後面好多年應用端都省事。這是我從自己的系統長出來的選擇,不一定適合你的專案,請自己權衡。
📘 HackMD 原文筆記:
👉 https://hackmd.io/@jeff377/schema-driven-ui
📢 歡迎轉載,請註明出處
📬 歡迎追蹤我的技術筆記與實戰經驗分享
Facebook | HackMD | GitHub | NuGet
「關連到另一張表」的外鍵,要能開窗挑一筆、把代碼與名稱一起帶回來
=> 這可能還不夠, 可能還有其他資訊需要帶回.(如: 規格, 儲位, 歷史交易...)
帶回欄位不是固定的,是定義出來的,可以帶回來源表單的任何欄位。
同一表單嗎? 還是可以跨表單?
要是需要多個相關欄位值情況, 再查詢回傳值呢?以下是舉例
A1.先填入廠商,廠別資訊
A2.查詢品號(回傳品名與廠商的採購單價明細)
PS: 廠商的採購單價明細:需要配合A1資訊讀取
跨多層表單,來源表單的欄位有可能由另一個來源表單過來,可以參考下面文章
https://hackmd.io/@jeff377/drmap
稍微看懂, 太久沒接觸開發框架了, 覺得讀取來好累. ![]()