31

有赞云-可配置表单的实践

 4 years ago
source link: https://tech.youzan.com/you-zan-yun-ke-pei-zhi-biao-dan-de-shi-jian/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

一、背景

前端开发过程中,往往会遇到很多的表单。简单表单尚可,但复杂表单让人尤为头疼。

比如有一个用来提交 请假单申请 的表单。

第一个表单项是 Radio,为性别:分别是 两种选项。

第二个表单项是个 Select,为请假原因:分别为 事假年假调休病假 这四种选项。另外,当上一个性别选项,选择为 后,则额外增加一个 产假 的选项。

第三个表单项是个 Uploader,为图片上传组件,用于上传医院证明:该项仅在请假原因选择为 病假产假 后显示,其余情况不显示。

这个场景已经略微有点复杂度了。很多时候如果不好好设计,将写出不好维护的代码。

那如果更加复杂的场景呢?

有赞云业务中,这种场景非常常见,于是推出了 zan-form 来解决这个问题。

二、配置式

对于上面提到的场景,先思考下,用普通方式怎么写。用普通的方式写一遍后,会发现有点难受。为什么呢?

因为 jsx ,本质上是现实中的物理模型,适合像积木一样,去拼凑出各种各样的 UI。

它一旦加上复杂的判断逻辑后,就会很杂乱,需要花大量精力去整理和修饰。

再想一下 配置式 ,第一直觉是什么?是逻辑和规则。

表单跟一般的 UI 不一样,尤其复杂的表单,它特别重逻辑,但是视觉复杂度上并不高,不会存在特别多层级的嵌套。所以用配置式来写复杂表单,也许是一个更好的方案。

下面是用配置式表单解决上面提到的场景:

[
  {
    _component: "FormRadioGroupField",
    _name: "sex",
    data: [
      {
        text: "男",
        value: "male"
      },
      {
        text: "女",
        value: "female"
      }
    ]
  },
  {
    _component: "FormSelectField",
    _name: "qingjiaType",
    data: [
      {
        text: "事假",
        value: "shijia"
      },
      {
        text: "年假",
        value: "nianjia"
      },
      {
        text: "调休",
        value: "tiaoxiu"
      },
      {
        text: "病假",
        value: "bingjia"
      },
      {
        text: "产假",
        value: "chanjia"
      }
    ]
  },
  {
    _component: "ImageUploader",
    _name: "hospitalMaterial",
    _show: values => ["bingjia", "chanjia"].includes(values.qingjiaType),
    tokenUrl: "http://somecdn.youzan.com"
  }
];

在上面的配置代码中, _component_name 就不多说了,解释下 _show 方法。

_show 目前仅支持传入一个同步函数,其中入参为 values

vlaues 等于 zentForm.getFormValues() 所取得的值。

_show 的出参为一个布尔值,当 true 时,表示该组件显示;当为 false 时,表示该组件不显示。

总体来说,就是把一份配置文件,做为一个数组进行遍历。当遇到 _show 方法时,执行之,并根据其 返回值,决定是实例化的这个组件,还是直接返回 null

三、禁用 children

配置式组件,是否应该支持 children ,这是一个有争议性的问题。

支持后有利有弊,但最终还是决定禁止 children 的使用。

原因是,一旦支持实现 children 后,整个配置就偏视觉,而非偏逻辑了。

所以,以下方式是不被允许的:

// 下面的写法,会报错。因为不被允许使用 children
{
  _component: 'FormRadioGroupField',
  _name: 'sex',
  children: [
    {
      _component: 'Radio',
      value: 'male',
      text: '男'
    },
    {
      _component: 'Radio',
      value: 'female',
      text: '女'
    }
  ]
},

当然,为了方便使用,已经对 zent 自带的 FormCheckboxGroupFieldFormRadioGroupField 这两个组件进行了封装。封装后用法跟 FormSelectField 类似,仅需要传入 [{ text: 'text', value: 'value' }] 即可。如下所示:

// 封装后的 FormRadioGroupField 用法
{
  _component: "FormRadioGroupField",
  _name: "sex",
  data: [
    {
      text: "男",
      value: "male"
    },
    {
      text: "女",
      value: "female"
    }
  ]
},

所以对自己写的表单组件,如果需要用到 children 属性,会遇到一些问题,需要做一些改造。

当然,也有其他方式可以绕过,就是后面小节会提到的 _slot

四、注册自定义表单组件

到目前为止,能够直接在配置文件中使用的组件,都是 zent.Form 下的组件。

那么对于自定义组件怎么处理呢?

当前在 zan-form 内部维护了一个 componentLib 对象。

在该对象中, key 是组件名, value 是 Component。当解析配置文件的时候,会根据 _component 字段,去找到对应的组件,并实例化它。

所以我们只需要想办法,在 componentLib 中放入我们自定义的组件就可以了。

zan-form 提供了 zanForm.register('ComponentName', MyComponent) 方法,可以在 componentLib 中注册我们自己的组件。

注意,第三节也提到过,如果自定义组件需要用到 children 属性,需要对该组件进行改造,因为当前的配置式是不支持 children 的。

为了能在 zentForm 中更好地使用,自定义组件需要暴露 value 属性,以及支持 onChange 方法做为回调,并用 zent.Form.Filed 包裹。

五、插槽

某些场景下,纯粹的配置式无法满足需求,则需要用 插槽 来实现扩展了。

插槽 的使用如下:

// form.config.js
[
  {
    _slot: "ImageUploader"
  },
  {
    _slot: "MyFooter"
  }
];

// Form.jsx
import zanForm from "zan-form";  
import formConfig from "./form.config.js";

const Slot = zanForm.Slot;

class Form extends Component {  
  render = () => {
    return zanForm(formConfig, this)(
      <React.Fragment>
        <Slot id="ImageUploader">
          <ImageUploader>点击上传图片</ImageUploader>
        </Slot>
        <Slot id="MyFooter">
          <Footer>我是页脚</Footer>
        </Slot>
      </React.Fragment>
    );
  };
}

如何使用 插槽 ,总结起来就是两个步骤:

首先,在配置中,选择合适的点,预留一个插槽。

然后,在配置外的 zanForm 中,使用 Slot 定义一个 待插入组件 ,并赋予唯一 id。

注意, _slot 可以跟 _show 一起使用,但是其他的,例如 _fetch_data_format 等,一律不支持。

再说下 _slot 的实现原理,其实也很简单:

zan-form 内部维护有一个 slotMap 对象,以 Slot.id 做为 keySlot.children 做为 value 。当遍历配置文件时,遇到 _slot ,则去 slotMap 里面取对应的 Slot.children ,并填充进插槽即可。

六、与服务端的交互

在日常开发中,存在着大量与服务端交互的场景。

比如 FormSelectField 中的 data ,需要从服务端获取。

一般的方式是,在 componentDidMount 中获取数据,并且通过 setState 塞入到 FormSelectFielddata 中去。

然而在配置文件中,这似乎很难实现。那么在配置式中,怎么样获取远程数据呢?

目前可以通过 _fetch_data 方法来返回一个 Promise 的方式来实现,如下所示:

// "店铺类型"的Select
{
  _component: 'FormSelectField',
  _name: 'shopType',
  _fetch_data: () => {
    return getShopType().then(items => {
      return items.map(item => {
        return {
          text: item.desc,
          value: item.type,
        };
      });
    });
  },
  data: [],
},

以下两点值得注意:

1、原组件(如 FormSelectField 组件)必须支持 data 属性。因为获取到的数据,会默认塞入到 data 中去。

2、 _fetch_data 只会触发一次。

实现方式:

这个特性,目前是通过在【原组件】外面,又另外包裹了一层 DecoratorCoponent 实现的。

DecoratorComponent 触发 componentDidMount 的时候,就去调用 _fetch_data ,并将 data 通过 props 传给【原组件】。

也就解释了上面提到的,为什么 _fetch_data 只会触发一次。

七、重启组件

存在一种场景,如下:

第一行表单项,是一个 FormSelectField ,代表省份。

第二行表单项,也是一个 FormSelectField ,代表城市。但是该项会根据省份 id,动态从服务端获取城市数据。也就是说,上一项省份改变时,该项城市列表也会改变。

根据上述第六节提到的,如果只用 _fetch_data ,那么根据省份 id 获取城市列表数据,只会触发一次,不会再触发第二次。

所以提出了一个 重启 组件的概念。

重启组件的内部实现,实际上分为两个步骤:

第一步,在 DecoratorCoponent 中改变【原组件】的 key 来重启原组件。

第二步,重新触发自身的 _fetch_data 函数,重新将 data 通过 props 传给【原组件】。

如下所示:

[
  {
    _component: "FormSelectField",
    _name: "province",
    required: "请选择您所在省份",
    data: [{ text: "浙江省", value: 30 }, { text: "广东省", value: 31 }]
  },
  {
    _component: "FormSelectField",
    _name: "city",
    _fetch_data: values => {
      if (!values.province) {
        return Promise.resolve([]);
      } else {
        return fetchCityByProvince(values.province);
      }
    },
    _subscribe: (prevValues, values, restart) => {
      if (values.province !== prevValues.province) {
        restart();
      }
    },
    required: "请选择您所在城市",
    data: []
  }
];

重启 ,需先 订阅 。使用 _subscribe 可订阅。

_subscribe 方法中接受三个入参,分别是上一个状态的 values ,本状态的 values ,以及一个用来重启组件的 restart 方法。

比较两次状态的 values.province ,如果不同,则触发重启。

至于 _subscribe ,其实是借助 DecoratorCoponent 中的 componentDidUpdate 实现的。

八、格式化组件

有时候,需要对组件做一些样式上的修改,那么在配置式中,怎么做呢?

目前提供了一个 _format 方法来实现这一点。

如下所示:

[
  {
    _component: "FormInputField",
    _name: "age",
    _format: ($component, values) => (
      <div>
        {$component}
        <span>岁</span>
      </div>
    ),
    label: "年龄"
  }
];

format 方法中,\$component 是组件实例,values 同 _show 方法的 values 。它们做为参数传入,最后返回一个改动后的组件实例。

_format 方法的内部实现,跟 _show 类似。

九、表单回填

一些场景下,比如编辑的时候:需要当从服务端取回数据,并在表单上回显。

这种场景该怎么做呢?

zan-form 提供了一个 zanForm.setValues(values, this) 方法,即可把数据回填回去。

本质上, setValues 方法,是对 zentForm.setFieldsValue() 的一个递归调用,最终使 zentForm.getFormValues() 的值趋向于稳定。

十、有赞云招人

有赞云大量 hc 招聘前端,有兴趣的可以加微信 socialHunter 咨询!

欢迎关注我们的公众号

bqyERrE.png!web

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK