一、表單地獄與 hardcoded 的痛
在我們做專案開發、SaaS 平台設計或內部工具建置時,表單往往是前端最常見、但也是最容易變成災難的地方。特別是當表單具有以下特性時,開發者就像掉進了一場「表單地獄」:
- 欄位多達數十個以上
- 各欄位型別不一(文字、數字、日期、附件、select、多選)
- 有條件邏輯(A 填了才出現 B)
- 欄位間存在依賴關係
- 權限限制(某些角色只可見、不能編輯)
- 每個表單還有各自不同的驗證規則
如此如此…還有太多需要重複開發的眉眉角角
這些需求乍看合理,但在傳統開發模式下,每一項都意味著寫死在 code 裡,例如:
<!-- 傳統 Vue 寫法 -->
<div v-if="user.role === 'admin'">
<label>管理者備註</label>
<textarea v-model="form.adminNote" required></textarea>
</div>這類 hardcoded 的方式一開始可能很快,但隨著表單越來越複雜,會逐步出現以下痛點:
| 痛點 | 說明 |
| 重複勞動 | 每新增一種表單或流程,就要重新撰寫一份 template 和邏輯。 |
| 高維護成本 | 欄位異動、邏輯改變需全面檢查程式碼,風險高。 |
| 難以重用 | 同樣的欄位規則在不同表單中無法共用。 |
| 角色權限拆解困難 | 欄位的可見/可編輯權限難以模組化,常寫成大堆 if else。 |
| 配合後端困難 | 一但後端新增欄位,前端必須手動補上元件、驗證與邏輯。 |
| 版本不一致問題 | 表單欄位定義分散在不同端點與檔案,容易產生版本落差。 |
這就像是一場「用複製貼上的方式堆砌流程邏輯的長征」,當需求改變、流程改版、欄位重新配置時,開發者往往會陷入 debug 噩夢中。
為了跳出這種寫死邏輯的泥淖,我們開始思考:能不能讓表單自己「讀懂」結構與邏輯,而不是我們手動寫死?
這就是本文要探討的 —— metadata-driven UI framework 的核心價值。
二、Metadata-driven UI 是什麼?
如果 hardcoded 的表單開發是「逐一手刻欄位與邏輯」,那 metadata-driven UI 就是「宣告好結構,讓系統自己渲染欄位與行為」。
Metadata-driven UI 是一種 UI 架構設計模式,核心精神是:
UI 的結構、欄位型別、驗證規則、條件邏輯、權限控制等都由一份「中繼資料(metadata)」來描述,前端只需根據這份資料動態渲染元件,而非硬寫 template。
這份 metadata 本質上就是一份 描述 UI 的資料,例如每個欄位要顯示什麼?可不可以編輯?需要哪些選項?甚至哪些角色可見?都可以在這份 metadata 中定義。
範例:從 hardcoded 到 metadata setting
假設你原本手寫如下欄位:
<label>標題</label>
<input type="text" v-model="form.title" required :disabled="user.role !== 'editor'" />用 metadata 表達的方式可能長這樣:
{
key: 'title',
type: 'text',
label: '標題',
required: true,
disabledRoles: ['viewer']
}
而前端框架只要知道怎麼「讀這份 metadata」,就能正確地渲染出:
- 欄位名稱為「標題」
- 是 input[type="text"]
- 為必填欄位
- 如果角色是 viewer 就自動設為 disabled
這個模式帶來的思維轉變:
| 傳統寫法 | Metadata-driven UI |
| 一個表單就寫一套邏輯 | 多個表單共用渲染邏輯,改資料就能改 UI |
| 前端控制 UI 呈現 | 後端或配置決定 UI 呈現 |
| 複雜邏輯寫在 template 裡 | 條件邏輯寫在 metadata 裡,易測試 |
| 欄位改了,要改 code | 欄位改了,只需改 metadata |
有了這樣的初步了解,可以知道他適合:
- 有大量可配置的表單需求(例如 CMS、流程表單)
- 欄位會隨專案、單位、角色變動
- 欲實作可視化表單編輯器(low-code/no-code)
三、Metadata-driven 表單 Demo
Metadata 設計
const formMetadata = [
{
key: 'title',
label: '標題',
type: 'text',
required: true,
disabledRoles: ['viewer']
},
{
key: 'category',
label: '分類',
type: 'select',
options: ['建築', '機電', '土木']
},
{
key: 'remark',
label: '備註',
type: 'textarea'
}
]formData自動初始化
<template>
<form @submit.prevent="onSubmit">
<div v-for="field in formMetadata" :key="field.key" class="mb-3">
<label>{{ field.label }}</label>
<input
v-if="field.type === 'text'"
v-model="formData[field.key]"
type="text"
:required="field.required"
:disabled="isDisabled(field)"
class="form-control"
/>
<select
v-else-if="field.type === 'select'"
v-model="formData[field.key]"
class="form-select"
>
<option disabled value="">請選擇</option>
<option v-for="opt in field.options" :key="opt" :value="opt">{{ opt }}</option>
</select>
<textarea
v-else-if="field.type === 'textarea'"
v-model="formData[field.key]"
class="form-control"
rows="3"
/>
</div>
<button type="submit" class="btn btn-primary">送出</button>
</form>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const userRole = 'editor'
const formMetadata = [
{ key: 'title', label: '標題', type: 'text', required: true, disabledRoles: ['viewer'] },
{ key: 'category', label: '分類', type: 'select', options: ['建築', '機電', '土木'] },
{ key: 'remark', label: '備註', type: 'textarea' }
]
const formData = ref(
Object.fromEntries(formMetadata.map((field) => [field.key, '']))
)
const isDisabled = (field: any) => {
return field.disabledRoles?.includes(userRole)
}
const onSubmit = () => {
console.log('送出資料:', formData.value)
}
</script>
這樣就是一個最小可行、架構正確的 metadata-driven form rendering 示範。如你之後要再加入驗證(例如 required rule)、條件欄位顯示(例如 showIf)、甚至動態權限控制,都能從這個基礎擴展。(metadata 一般都是定義在後端,本篇主要介紹概念所以定義在前端)
從表單地獄,到配置驅動的優雅
在開發過程中,我們都曾為大量表單欄位的變動、客製化需求、條件邏輯與權限限制感到疲憊不堪。傳統以硬編碼方式撰寫表單,雖然直觀,但隨著需求複雜化,往往成為維護負擔與錯誤溫床。
透過 metadata-driven UI 的設計模式,我們將欄位的結構、行為、互動、驗證乃至權限抽象為一份「資料描述」,讓 UI 呈現與邏輯運作能由 metadata 所驅動。這樣的轉變不只是減少重複程式碼,更打下了未來走向 low-code 平台的基礎。