🎯 什麼情境該想到我

當你「想把頁面產生分兩步,先產生與版面無關的邏輯結構,再統一套上外觀」的時候。

⚙️ 怎麼用(步驟 / 公式)

意圖:把「呈現什麼」與「長什麼樣」分成兩個步驟,讓整站外觀能集中統一調整。

做法要點:

  • 第一步:把領域資料轉成一個與最終外觀無關的「邏輯呈現結構」(描述有哪些欄位、表格、清單等抽象元素)。
  • 第二步:把該邏輯結構轉成實際 HTML,套上一致的樣式與版型。
  • 想全站換皮或改版型,只需改第二步。

★★ 它的核心價值:把「有什麼」與「長什麼樣」分開

第一步:領域資料 → ★ 邏輯呈現結構(「這是一個表格,有三欄,第二欄要標紅」)
第二步:邏輯結構 → ★ 具體輸出(HTML/JSON/CSV/PDF)
→ ★★ 換版面只改第二步;改內容只改第一步

★★ 今天最大規模的實踐就是「設計系統」:
第一步產生 <Card>、<DataTable>、<Badge variant="warning"> 這種語義元件;
第二步(設計系統的實作)決定它們實際長什麼樣。
★ 改一次設計系統,全站一致地變——這正是兩步驟視圖承諾的東西。

★★ Go 的三種落地方式

// ★ ① 共用 layout + 具名區塊(★ 最輕,html/template 內建)
{{ define "layout" }}
<html><body>
  <nav>…</nav>
  {{ block "content" . }}{{ end }}      // ★ 第二步:骨架
</body></html>
{{ end }}
 
{{ define "content" }} … {{ end }}      // ★ 第一步:這一頁的內容
 
// ★★ ② 明確的「呈現模型」型別(★ 真正的兩步驟)
type Column struct{ Title string; Align string }
type Cell   struct{ Text string; Emphasis string }    // ★ 語義,不是樣式
type Table  struct{ Columns []Column; Rows [][]Cell }
 
func orderTable(orders []Order) Table { … }           // ★ 第一步:純函式,好測
// → 第二步:一個樣板把 Table 渲染成 HTML;另一個渲染成 CSV
 
// ★ ③ 元件式(htmx / templ / 前端框架)
// → ★ 每個語義元件有自己的渲染實作

★★ ② 的最大好處:第一步是純函式,可以用表格測試窮舉。
「金額為負要標紅」這條規則變成一個斷言 cell.Emphasis == "negative",
不必解析 HTML。

★★ 什麼時候值得做(三個明確訊號)

① ★★ 同一份資料要輸出多種格式
   HTML 頁面 + CSV 匯出 + PDF 報表 + Email 版
   → ★ 沒有兩步驟時,四份程式碼各自實作「金額為負要標紅」⇒ 一定會不一致

② ★★ 版面要全站一致地改
   「所有的警示標籤改成橘色」→ ★ 改一處 vs 改 47 個樣板

③ ★ 需要對「呈現邏輯」寫測試
   → ★★ 第一步是純函式,測試不必解析 HTML

★★ 它的代價:多一層抽象,而那一層會漏

★ 三個真實的問題:
① ★★ 邏輯結構總是不夠表達力
   「這一欄要 sticky」「這個 badge 要有 tooltip」→ ★ 邏輯模型裡沒有這個概念
   → ★★ 於是它長出 CSSClass string 或 Attrs map[string]string 欄位
   → ★ 一旦有了這兩個欄位,「樣式與內容分離」就破功了

② ★ 兩層都要維護
   加一個欄位要改「邏輯模型 → 第一步 → 第二步」三處

③ ★★ 除錯多一層
   「為什麼這個數字沒有標紅」→ ★ 要先查第一步有沒有設 Emphasis,
     再查第二步有沒有處理那個值

★★ 判準:你真的有「多種輸出」或「全站改版」的需求嗎?
沒有(只有一個 HTML 介面,而且版面穩定)→ 樣板視圖 直接來,這是純成本。

★ 實務上「CSV 匯出」是最常見的第二種輸出,而它通常不需要完整的兩步驟——
一個獨立的 toCSV(orders) 函式就夠了。

★★ 一個常被忽略的替代:讓第一步「只產生資料,不產生呈現」

// ★ 不做完整兩步驟,但做到 80% 的收益:
type OrderRow struct {
    ID        string
    Total     Money
    IsOverdue bool      // ★★ 語義旗標(而不是 CSSClass: "text-red")
    IsVIP     bool
}
// → ★ 樣板自己決定 IsOverdue 要怎麼呈現
// → ★★ 這就達成了「業務語義與樣式分離」,而不需要一個完整的呈現模型

★★ 這是實務上最划算的點:
view struct 只放「業務語義的布林與值」,絕不放 CSS class 或顏色。
→ ★ 樣式的決定完全留在樣板/CSS 裡,而測試可以斷言語義旗標。

★ 兄弟辨析

差別
樣板視圖★ 一步到位;★ 多數情況的正確選擇
轉換視圖★ 可以是兩步驟的任一步(或兩步都是)
資料傳輸物件★ 第一步的輸出就是它
拆分階段★ 同一個「先解析再處理」想法的一般形式

🧪 我實際套用的紀錄

  • (待填)

⚠️ 注意 / 什麼時候不適用

  • 需要各頁外觀差異很大時,共用第二步反而綁手綁腳。

  • 多了一層中介結構,較單步視圖複雜。

  • 只有少數頁面、外觀不需高度一致時不划算。

  • ★★ 沒有「多種輸出」或「全站改版」的需求,這就是純成本。 用 樣板視圖 直接來。

  • ★★ 邏輯結構總是不夠表達力,於是長出 CSSClass 或 Attrs 欄位——一旦有了它們,「樣式與內容分離」就破功了。

  • ★★ 最划算的替代:view struct 只放業務語義的布林與值(IsOverdue bool),絕不放 CSS class 或顏色。這達成 80% 的收益而沒有第二層。

  • ★ 「CSV 匯出」是最常見的第二種輸出,而它通常只需要一個獨立的 toCSV() 函式。

  • ★ 第一步要是純函式,這樣「金額為負要標紅」可以用斷言測試而不必解析 HTML。

  • ★ 兩層都要維護:加一個欄位要改三處。

  • 除錯多一層:「為什麼沒標紅」要查兩個地方。

🔗 相關工具