我来聊聊配置驱动的视图开发

本文首发于 欧雷流。因为我会时不时对文章进行补充、修正和润色,为了保证所看到的是最新版本,请阅读 原文

我在平时上下班开车时,全凭身体记忆与条件反射,基本不用脑子,因此脑子就空出来胡思乱想了,东想一想西想一想。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 仍是其余的,又或者是几种混用,对实际业务的进展不会形成影响。

思想总结

在文章标题中使用的是「视图开发」而不是「前端开发」是由于全文的侧重点在视图层,基本没有提到其余层的事情,但不表明仅视图层是能够配置驱动的。

理论上,在一个可以快速响应业务变化的前端架构中,应该是总体可配置,各层均可被替换,但没法替换的是设计目标、设计思想与接口协议,这些是灵魂,只要它们在,架构就没变。

相关文章
相关标签/搜索