我來聊聊配置驅動的視圖開發

本文首發於 歐雷流。因爲我會時不時對文章進行補充、修正和潤色,爲了保證所看到的是最新版本,請閱讀 原文

我在平時上下班開車時,全憑身體記憶與條件反射,基本不用腦子,因此腦子就空出來胡思亂想了,東想一想西想一想。html

某天早上突然想到:最近幾年,業界在開發時都講究以「數據驅動」的方式更新視圖,回想過去這幾個月的工做內容,發現咱們的視圖層開發並非單純的數據驅動,而是「配置驅動」。前端

視圖更新

讓咱們先來回顧一下以往以及如今,在視圖層開發時通常是如何更新視圖的吧——web

在 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,數據綁定相對更「智能」,這是一種數據驅動的開發方式。可單純的數據驅動只解決了基本的數據顯示問題,並無任何視圖層可擴展性上的支撐,好比同一個表格組件:frontend

  • 在 A 模型下想顯示 a、b、c 字段,在 B 模型下想顯示 d、e、f 字段;
  • 在頁面主體中時想顯示列偏好設置、單元格文本密度調節,但在對話框中時不想要這些功能;
  • 在 A 應用中表頭的邊框是尖角且背景色是淺藍色,在 B 應用中則是圓角的邊框與淡紫的背景色。

在複雜多變的中後臺業務場景中,要想使一個組件可以最大限度地複用,要想用一些組件快速搭建出一箇中後臺應用,就須要一套足夠靈活、足夠強大的可擴展體系。ide

視圖配置

一個頁面,或者說一個視圖,能夠進行配置的點主要有:模板、模型、邏輯、主題。函數

模板

在 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 仍是其餘的,又或者是幾種混用,對實際業務的進展不會形成影響。

思想總結

在文章標題中使用的是「視圖開發」而不是「前端開發」是由於全文的側重點在視圖層,基本沒有提到其餘層的事情,但不表明僅視圖層是能夠配置驅動的。

理論上,在一個可以快速響應業務變化的前端架構中,應該是總體可配置,各層均可被替換,但沒法替換的是設計目標、設計思想與接口協議,這些是靈魂,只要它們在,架構就沒變。

相關文章
相關標籤/搜索