我在平時上下班開車時,全憑身體記憶與條件反射,基本不用腦子,所以腦子就空出來胡思亂想了,東想想西想想。
某天早上忽然想到:最近幾年,業界在開發時都講究以「數據驅動」的方式更新視圖,回想過去這幾個月的工作內容,發現我們的視圖層開發並不是單純的數據驅動,而是「配置驅動」。
視圖更新
讓我們先來回顧一下以往以及現在,在視圖層開發時一般是如何更新視圖的吧——
在 React、Vue 等前端庫/框架流行之前,基本以手動操作 DOM 的方式進行:
<form>
<div>
<span>是否已婚</span>
<div>
<label><input type="radio" name="married" value="true"> 是</label>
<label><input type="radio" name="married" value="false"> 否</label>
</div>
</div>
<div id="childrenCountField" style="display: none;">
<label>孩子數量</label>
<input type="text" name="childrenCount" value="">
</div>
</form>
<script>
$('[name="married"]').on('change', function() {
const $children = $('#childrenCountField');
if ($(this).val() === 'true') {
$children.show();
}
else {
$children.hide();
}
});
</script>
在 Vue 中使用的是數據綁定:
<template>
<el-form>
<el-form-item label="是否已婚">
<el-radio-group v-model="married">
<el-radio :label="true">是</el-radio>
<el-radio :label="false">否</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="孩子數量" v-show="married">
<el-input />
</el-form-item>
</el-form>
</template>
<script>
export default {
data() {
return {
married: false
};
}
}
</script>
通過配置的方式來完成同樣的事情:
<view widget="form">
<field name="married" label="是否已婚" widget="radio" />
<field name="childrenCount" label="孩子數量" widget="input" invisible="record.married !== true" />
</view>
有沒有覺得最後一種很方便,並且可讀性很強?
相較於手動操作 DOM,數據綁定相對更「智能」,這是一種數據驅動的開發方式。可單純的數據驅動只解決了基本的數據顯示問題,並沒有任何視圖層可擴展性上的支撐,比如同一個表格組件:
- 在 A 模型下想顯示 a、b、c 字段,在 B 模型下想顯示 d、e、f 字段;
- 在頁面主體中時想顯示列偏好設置、單元格文本密度調節,但在對話框中時不想要這些功能;
- 在 A 應用中表頭的邊框是尖角且背景色是淺藍色,在 B 應用中則是圓角的邊框與淡紫的背景色。
在複雜多變的中後台業務場景中,要想使一個組件能夠最大限度地複用,要想用一些組件快速搭建出一箇中後台應用,就需要一套足夠靈活、足夠強大的可擴展體系。
視圖配置
一個頁面,或者説一個視圖,可以進行配置的點主要有:模板、模型、邏輯、主題。
模板
在 web 開發中所使用的「模板」,大多是與 HTML 相符合且面向開發的,如:Vue 的模板、Pug(Jade)、Thymeleaf、FreeMarker、Velocity 等等。
然而,這裏的「模板」與 HTML 沒有直接關係,是對某個領域的視圖結構、數據結構或邏輯結構的描述,是一種外部 DSL:
- 描述數據容器的視圖模板;
- 描述搜索過濾器及操作符的搜索模板;
- 描述整體佈局的佈局模板;
- 描述紙張打印的打印模板;
- 描述調研問卷的問卷模板。
這些模板遵從相同的設計原則,使用同一套解析器,解決不同領域問題。它們分別是一套標籤集,只要有新的領域的問題要解決,就可以新增一套標籤集。
模板不僅能讓人一眼就看懂它所描述的信息,還能控制最終所呈現出的形態,詳見我之前寫的《我來聊聊面向模板的前端開發》。
模型
這裏所説的「模型」主要是指元數據。什麼是「元數據」?簡單理解,就是「用來描述數據的數據」。
假如有一張個人信息表,需要填寫如下信息:
- 姓名
- 出生日期
- 年齡
- 性別
- 是否已婚
- 孩子數量
- 月收入
- 興趣愛好
試想一下,這些信息分別是什麼數據類型?不要想當然地認為姓名就是字符串而不是長文本,年齡就是數字而不是字符串,性別就是布爾型而不是枚舉……
為了使在進行數據處理時能夠模式化,需要對要處理的數據進行描述,即使用元數據。
要描述的信息主要是數據類型及其要顯示的文本標籤,如果不是布爾型、數字、字符串等基本類型,最好描述其數據來源,比如枚舉;根據需要還可以描述是否必填、是否只讀等:
[
{
"name": "name",
"label": "姓名",
"type": "string",
"required": true
},
{
"name": "birthday",
"label": "出生日期",
"type": "date",
"required": true
},
{
"name": "age",
"label": "年齡",
"type": "integer",
"required": true
},
{
"name": "gender",
"label": "性別",
"type": "enum",
"options": [],
"required": true
},
{
"name": "married",
"label": "是否已婚",
"type": "boolean",
"required": true
},
{
"name": "childrenCount",
"label": "孩子數量",
"type": "integer",
"required": true
},
{
"name": "monthlySalary",
"label": "月收入",
"type": "currency"
},
{
"name": "hobbies",
"label": "興趣愛好",
"type": "m2m",
"options": "",
"chosen": []
}
]
元數據對視圖的影響,主要是數據相關的,對視圖形態沒什麼影響,如:要顯示哪些字段(根據元數據生成視圖模板)、字段的校驗規則、字段的編輯狀態、請求的參數等。
根據上述元數據所生成的視圖模板大概長這樣兒:
<view widget="form">
<field name="name" label="姓名" required="true" />
<field name="birthday" label="出生日期" required="true" />
<field name="age" label="年齡" required="true" />
<field name="gender" label="性別" required="true" />
<field name="married" label="是否已婚" required="true" />
<field name="childrenCount" label="孩子數量" required="true" />
<field name="monthlySalary" label="月收入" />
<field name="hobbies" label="興趣愛好" />
</view>
在使用元數據時,最好後端能陪着一起玩兒,這麼一來就省去了不少接口的設計、評審、聯調等時間,取而代之的是後端定模型。如果只能前端自己玩兒,可以利用 JSON Schema 等工具。
邏輯
如果框架設計得合理,應該能夠在不更改組件的內部實現的情況下與外部可配置的邏輯進行組合聯動。根據邏輯的輕重與組合聯動方式,可以大致分為動作與表達式這兩種。
「動作」是一段完整邏輯的抽象,與函數相當,用來描述且只描述「做什麼事」,不描述「長什麼樣」。一個可複用的動作應該是原子化的。
根據邏輯的定義、執行所在位置,可以分為客户端動作(廣義)與服務端動作:客户端動作(廣義)是定義並且執行在前端;服務端動作是定義並且執行在後端。
客户端動作(廣義)根據具體場景的用途及特性,又可分為以下幾種動作:
- 路由動作
- CRUD 動作
- 客户端動作(狹義)
- 組合動作
其中,路由動作的作用是進行頁面跳轉;CRUD 動作是對數據進行操作;客户端動作(狹義)是單純的一段邏輯,可以簡單理解為是一個 JS 函數;組合動作用於將其他類型的動作「打包」處理,就像一個調用了其他函數的函數。
服務端動作可以簡單粗暴地理解為是非常規 CRUD 的後端接口。
表達式是一種輕邏輯,主要用於字段的值計算、備選項篩選、狀態聯動等運用簡單邏輯的場景:
<view widget="form">
...
<field name="married" label="是否已婚" required="true" />
<!-- 「是否已婚」的值為 `true` 時才顯示「孩子數量」,並且必填 -->
<field name="childrenCount" label="孩子數量" required="record.married === true" invisible="record.married !== true" />
...
</view>
主題
相信看到「主題」這兩個字,第一反應是改變字體、字色、背景色等特性的「皮膚」,然而在本文的語境中,不完全正確。
上面所提到的模板、模型、邏輯等都是較為底層的配置,從外部去影響組件的呈現;而主題則是更為上層的配置,從內部或者其本身去影響組件——樣式、行為、組件及其所依賴的運行時。
「樣式」不難理解,就是改變字體、字色、背景色等特性的「皮膚」,但「行為」是什麼呢?看以下幾種需求:
- 布爾型字段在某個應用中想用 Switch 組件,在其他應用中想用 Checkbox、Radio 或 Select 等組件;
- 表格在頁面主體中時想顯示列偏好設置、單元格文本密度調節,但在對話框中時不想要這些功能。
用來解決這類需求的配置,就是「行為」。
至於為什麼組件及其所依賴的運行時也是配置,這是因為在這種體系下,主要業務邏輯被底層所接管了,組件內基本只剩屬於本身的交互邏輯。所以,無論是用 Vue、React 還是其他的,又或者是幾種混用,對實際業務的進展不會造成影響。
思想總結
在文章標題中使用的是「視圖開發」而不是「前端開發」是因為全文的側重點在視圖層,基本沒有提到其他層的事情,但不代表僅視圖層是可以配置驅動的。
理論上,在一個能夠快速響應業務變化的前端架構中,應該是整體可配置,各層都可被替換,但無法替換的是設計目標、設計思想與接口協議,這些是靈魂,只要它們在,架構就沒變。
本文其他閲讀地址:個人網站|微信公眾號