iT邦幫忙

0

ERP 該為哪一代前端技術下注?

  • 分享至 

  • xImage
  •  

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

schema-driven-ui

做 ERP 系統這些年,有一件事再清楚不過:真正的核心資產是後端邏輯。業務規則、流程、資料結構的生命週期動輒十幾二十年;相對地,前端技術換得飛快,每隔幾年就有新的當紅選擇。把心血綁死在某一代前端技術上,系統往後演進會很痛。

幸好 ERP 的畫面相當「制式」,清單、表單、主從單據、查詢,版型來來去去就那幾種。既然這麼規律,我把它定義化,讓一份定義成為整套系統的單一來源。這是我的框架 Bee.NET 的核心做法,也是我在業界用了很多年、主流框架卻很少這樣走的取法,想藉這篇分享出來。

為了驗證這套做法,我用它做了一個 Northwind 進銷存範例。如果只想先抓重點,最有感的是這幾件事:

  • 一份定義,前端畫面、後端邏輯、資料表結構一起生成
  • 示範應用的九張表單裡有八張零程式碼,只有訂單因為有商業規則才寫了一個業務物件
  • 整個專案找不到一句 SQL,存取的 SQL 全由框架照定義組出來
  • 同一套定義可換著資料庫跑,支援 SQL Server / PostgreSQL / MySQL / Oracle / SQLite
  • 前端控件是繼承原生控件改寫、與定義深度整合,開發者幾乎不必碰 UI 細節
  • 要換 UI 技術,換的只是「依定義渲染」的一層皮,後端與定義不動
  • 定義是結構化的,AI 很容易讀懂,也能照需求幫你生成定義檔(這個 Northwind 範例的定義就是 AI 產的)

下面就一個個講這幾點怎麼來的。


1️⃣ 一份定義,要長在很多種前端上

定義驅動最省事、也最好維護的一點,就是「同一份 FormSchema 可以餵給不同前端」。我在 samples 裡陸續放了幾個最小可跑的範例,把各種前端都接到同一個後端試過一輪:

  • Console:純 API client,驗證 JSON-RPC 呼叫
  • Blazor Server:元件在伺服器端、in-process 直接派發(不繞 HTTP)
  • Blazor WebAssembly:同一套元件搬到瀏覽器內、走 HTTP
  • MAUI:原生行動 App 渲染同一份 schema
  • Avalonia:桌面(Windows / macOS / Linux)
  • 純 JavaScript:client 端完全沒有 .NET,直接打 JSON-RPC

連線策略分兩種:Local(同行程,效能最好)與 Remote(HTTP)。對「能不能接上」來說,這些範例已經給了答案:可以。

但這些範例證明的都是同一件事:能接上。我真正在意的,是接上之後那一段。


2️⃣ 「能接上」之後,控件還是不懂定義

把一個 TextBox 擺到畫面上、綁一個欄位的值,這誰都會。可是定義裡寫的遠不只「值」:

  • 這個欄位最多幾個字(MaxLength)?
  • 這是下拉選單,選項有哪些(ListItems)?
  • 這個欄位目前該唯讀嗎(ReadOnly)?
  • 這個欄位是「關連到另一張表」的外鍵,要能開窗挑一筆、把代碼與名稱一起帶回來嗎?

光綁值遠遠不夠。我要的是控件自己讀懂這些 metadata、自己改變行為。那問題就只剩一個:這個「讀懂」該擺在哪?


3️⃣ 要和定義完全整合,控件就得繼承下來改寫

我的目標很明確:讓前端開發者幾乎不用碰 UI 細節,把心力都留給真正重要的商業邏輯。要做到這點,控件就得和定義「完全整合」。而我一路試下來,能做到完全整合的,就只有把原生控件繼承下來改寫這條路。

一般的做法,是直接用原生控件,再用外部的 binding 或 behavior 把定義接上去。控件是乾淨,但定義在一邊、控件在另一邊,中間永遠隔著一層膠水,每加一個欄位都得在外面重接一次,前端開發者還是得管一堆 UI 接線。

繼承改寫就沒這個問題。我把原生控件繼承下來,做成一個天生就懂定義的子類:

public class TextEdit : TextBox, IFieldEditor
{
    // 綁到資料物件的某個欄位;自動套用該欄位定義的 MaxLength 等 metadata
}

呼叫端擺一個 TextEdit,它自己會去讀對應欄位的定義,外面什麼都不用接。對前端開發者來說,UI 這層幾乎是透明的,擺上去就對了,剩下的注意力全留給商業邏輯。

代價是要改得動原生控件,得先摸熟它的底層細節,主題樣式、編輯行為這些地方都有眉角,需要在框架這層多花心力。但反過來,應用端就幾乎不必碰這些。

再往下,我把共用的綁定狀態(接哪個資料物件、哪個欄位、值怎麼回寫、怎麼擋掉 echo 迴圈)收成一個元件,再加上一個方便的設計:容器設一次資料來源,底下每個帶欄位名的編輯器就自動接上去,擺上畫面就會動,一個都不用手接。


4️⃣ 從一個欄位,到一張表格,到一整張表單

同一套「繼承原生控件、讓它懂定義」的原則,一層層往上套,就長成一整張表單。

最底層是各種欄位編輯器:文字、多行、日期、年月、下拉、勾選,還有關連欄用的按鈕編輯器。每一個都是某個原生控件的子類,各自讀懂自己那個欄位的定義。

往上一層是表格控件,把欄位編輯器組成一張可編輯的明細表;要顯示哪些欄位、哪些可編輯,一樣由定義決定。

最上層是畫面層。我把它拆成兩個容器,對應 ERP 最常見的兩種畫面:一個負責清單瀏覽,一個負責單筆檢視與編輯(master 區加明細表)。這條線是順著使用者的習慣切的,不是技術限制。

最能體現這套原則的是關連欄。定義裡標一句「這個欄位關連到哪張表、要帶回哪些欄位」,版面產生器就自動把它變成一個會開窗挑選的按鈕;選完外鍵和顯示文字一起寫回,重載時再由後端 JOIN 算回來。呼叫端一行 UI 都不用寫,定義驅動的省事,在這裡最明顯。


5️⃣ 為什麼是 Avalonia 當試點

前面講的這套(繼承控件、拆出 View 層),我沒有一次鋪到所有前端,而是先挑一個來試做,選的是 Avalonia。為什麼是它?

因為 Avalonia 本身就是跨平台:同一套 UI 程式碼,再各自配一個輕薄的平台啟動專案,就能跑在桌面(Windows / macOS / Linux)、Web(WebAssembly)和行動 App 上。把深度整合的控件做在它上面,這同一套控件未來就有機會一路延伸到 Web 和 App,不只是服務桌面。一次打磨、多處受惠,這是其他只綁單一平台的前端給不了的。

目前我先用桌面程式把這些前端控件實作出來,Web 和行動版之後再做。也因此這套「繼承控件 + View 層」的深度整合,現階段只在 Avalonia 上做完整;其他前端(如 MAUI、Blazor)還停在比較簡單的動態表單渲染,等這邊定稿再談移植。


6️⃣ Northwind 範例:八張表單零程式碼、一句 SQL 都沒有

為了讓這套東西不只是「控件展示」,我做了一個比較完整的示範應用 bee-northwind-avalonia,用大家熟悉的 Northwind 進銷存案例。

Northwind 執行畫面

它正是整個框架的核心重點:這九張表單裡,有八張完全是純定義產生的,一行 UI 或 CRUD 程式碼都沒寫。要新增一張主檔、一張帶多重 lookup 的單據、或一組 master-detail,對開發者來說就是寫幾個定義檔:用 FormSchema 描述欄位與關連、配上版面與資料表定義,存檔重啟,連資料表都依定義自動建好,一張能新增/查詢/修改/刪除、會開窗挑關連資料的完整表單就出現了。

還有一點,讀者大概會更有感:翻遍整個專案,你一句 SQL 都找不到。建表、查詢、增刪改,背後該下的 SQL 全是框架照定義即時組出來的,開發者從頭到尾不必手寫。

而且既然 SQL 是框架照各家資料庫的方言組出來的,同一套定義就能換著資料庫跑:目前支援 SQL Server、PostgreSQL、MySQL、Oracle 與 SQLite,要換一家只是改設定。示範為了方便,用的是免安裝的 SQLite。

唯一寫了程式碼的是訂單那張:單號怎麼編、狀態能不能轉、金額怎麼算,這些才是真正的商業邏輯,我才動手寫一個業務物件,其餘全部交給定義。這正是我要的分工:開發者的時間該花在商業邏輯上,不是花在把一個又一個欄位接到畫面上

它純粹引用已發行的 NuGet 套件,clone 下來就能跑,算是這套做法目前能做到什麼程度的體檢。


7️⃣ 另一個逆主流的選擇:DataSet

這套做法還有一個更不主流的地方。前面那些編輯器綁的不是強型別 DTO,而是 DataSet,所以都是「按欄位名」取值,而不是綁某個強型別屬性。

現在主流幾乎都用強型別 DTO,DataSet 這種老東西早就少有人提了。但放在定義驅動底下,它反而更搭:資料的形狀本來就是執行時依定義長出來的,用一個按欄位名存取的鬆散容器,比維護一大堆要跟著定義同步的 DTO 類別省事得多,定義改一個欄位,也不必回頭改一串對應的 class。

當然它有代價,而且很直接:沒有編譯期型別檢查,欄位名打錯、型別取錯,編譯器不會吭一聲。這正是當年大家離開 DataSet 的理由。

補這個洞,我靠的還是定義。既然一切都從定義長出來,我就拿定義當單元測試的底,對著它驗證行為、把覆蓋率拉高,讓測試去抓那些編譯器幫不到的錯。強型別靠編譯器擋錯,我靠定義加測試擋錯,只是把保護網從編譯期挪到了測試。定義驅動這種場景,用 DataSet 反而比較合適。


8️⃣ 定義檔對 AI 很友善

AI 對結構化資料的理解力很高,而定義檔正好是結構化的,對 LLM 來說特別好讀。再搭配一份描述這些定義檔彼此關係的 skill(FormSchema、版面、資料表定義怎麼對應、哪些欄位要一致),AI 的理解會更到位,要照需求生成一整組正確的定義檔也更容易。新增一張表單,也就能從「人寫定義」變成「描述需求、讓 AI 產出定義」。前面那個 Northwind 示範就是現成例子:裡頭的 FormSchema、版面、資料表定義,全部都是我描述需求、由 AI 產出來的。

相比之下,對於 ERP 這種大型又複雜的系統,邏輯若散在程式碼各處,AI 要正確改動會非常吃力;而定義集中又結構化,反而剛好接得上這個需求。


✅ 結語

廣度上,同一份定義已經接得上各種前端;深度上,我先在 Avalonia 把這套「繼承控件、拆出 View 層」初步做起來,已經能組成一套應用。

繼承控件不是免費的:要改動原生控件得懂它的底層,有一定難度。小專案不一定划得來,偶爾接一兩個欄位,照一般做法用個 behavior 反而更省。但 ERP 不一樣,維護期動輒十年以上,這種前期投入攤到那麼長的生命週期就很有價值:在框架這層多花一次心力,把前端收斂成依定義生成的一層皮,換來的是後面好多年應用端都省事。這是我從自己的系統長出來的選擇,不一定適合你的專案,請自己權衡

延伸閱讀


📘 HackMD 原文筆記:
👉 https://hackmd.io/@jeff377/schema-driven-ui

📢 歡迎轉載,請註明出處
📬 歡迎追蹤我的技術筆記與實戰經驗分享
FacebookHackMDGitHubNuGet


圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

1
doesjudas
iT邦新手 2 級 ‧ 2026-06-16 12:22:59

「關連到另一張表」的外鍵,要能開窗挑一筆、把代碼與名稱一起帶回來
=> 這可能還不夠, 可能還有其他資訊需要帶回.(如: 規格, 儲位, 歷史交易...)

看更多先前的回應...收起先前的回應...
jeff377 iT邦新手 3 級 ‧ 2026-06-16 12:28:01 檢舉

帶回欄位不是固定的,是定義出來的,可以帶回來源表單的任何欄位。

doesjudas iT邦新手 2 級 ‧ 2026-06-16 13:38:41 檢舉

同一表單嗎? 還是可以跨表單?
要是需要多個相關欄位值情況, 再查詢回傳值呢?以下是舉例
A1.先填入廠商,廠別資訊
A2.查詢品號(回傳品名與廠商的採購單價明細)
PS: 廠商的採購單價明細:需要配合A1資訊讀取

jeff377 iT邦新手 3 級 ‧ 2026-06-16 16:48:45 檢舉

跨多層表單,來源表單的欄位有可能由另一個來源表單過來,可以參考下面文章
https://hackmd.io/@jeff377/drmap

doesjudas iT邦新手 2 級 ‧ 2026-06-16 17:26:40 檢舉

稍微看懂, 太久沒接觸開發框架了, 覺得讀取來好累. /images/emoticon/emoticon06.gif

我要留言

立即登入留言