中后台业务开发(一)「表单原理」

1. 前言

读完本文你会收获到:

  1. Form 原理最佳实践,提高日常开发效率。
  2. Form 源码中运用了哪些设计模式,丰富自己编码的武器库。

2. 原理

一句话概括:数据层面,单个实例统一管理所有表单数据,通过事件订阅和通知机制进行更新与响应,更新时触发相应组件重新渲染。

2.1. Form与Item

2.1.1. 一个实例

每一个 Form 组件都有相应的实例对象,集中管理表单状态、校验规则等。大家最常看到的应该就是如下函数:

const [form] = Form.useForm()

这个函数的最基础的作用是 创建表单实例,实例中包含状态仓库以及对状态的增删查改的方法,以及事件的订阅与通知。

2.1.2. 订阅与通知

看两段简化的代码:

js 复制代码
private fieldEntities = [];

// 将Item实例存在Form实例中
private registerField = (entity) => {
  // 存储子项实例
  this.fieldEntities.push(entity);
  ...
}

// 通知Item
private notifyObservers = (
  ...
) => {
  ...
  this.getFieldEntities().forEach(({ onStoreChange }) => {
    onStoreChange(...);
  });
  ...
};
js 复制代码
class Field extends ... {

  public componentDidMount() {
    ...
    // Item将实例注册到form实例中
    this.cancelRegisterFunc = registerField(this);
    ...
  }

  // 提供给form实例发送通知的回调函数
  public onStoreChange = (prevStore, namePathList, info) => {
    ...
    switch (info.type) {
      case 'reset':
        ...
      case 'remove':
        ...
      case 'setField': 
        this.reRender();
        ...
      case 'dependenciesUpdate':
        ...
      default:
        ...
        break;
      }
    ...
  };

	// 更新class类型的Item组件
	public reRender() {
    ...
    this.forceUpdate();
  }
}

从上面的两段简化后的代码可以清晰的看出:

在form实例中一旦有状态的变更只需要遍历Item实例的 onStoreChange ,就可以触发 Item 组件的 update。

而form实例中调用的 onStoreChange 方法实际上是在使用 form实例 的 registerField 注册item实例后,item实例中的方法

但是还是有几个细节需要交代一下:

  1. Item实例 怎么调用 form实例registerField 方法把自身给注册进去?

Item 是 Form 的子组件,在 Form 组件中通过 Context 的方法将 form 实例注入到任何层级的子组件中。所以在 Item 组件中,因为是在 Form 的包裹中,所以自然可以通过 useContext 拿到 form 实例,从而使用 registerField 将自身注册到 form 实例中。

  1. form实例中统一管理的数据如何与Item中的受控组件进行数据传递的呢?

下一节,Item与受控组件。

2.2. Item与受控组件

我们自定义受控组件或是使用通用组件,组件的 props 一般都会尽量遵循:

const { value, onChange, ... } = props

获取传入的 value 渲染组件,通过 onChange 回调函数的方式将组件变更的数据传递到上层使用。

我们来看Item组件中如何传递 value 和消费 onChange 回调的。

2.2.1. value

value比较简单,只需要Item组件中通过form实例中的方法(见上文),在数据仓库中查找对应字段的值即可。

ini 复制代码
public getValue = (...) => {
    const { getFieldsValue }: FormInstance = this.props.context;
    const namePath = this.getNamePath();
    ...
  };

2.2.2. onChange

还是在Item组件中,通过form实例中的方法 dispatch,将更新的值传递到form实例中,我们一起看一下 dispatch 方法:

js 复制代码
//使用
onChange = (...args) => {
	...
  dispatch({
    type: 'updateValue',
    namePath,
    value: newValue,
  });
  ...
}


private dispatch = (action) => {
    switch (action.type) {
      case 'updateValue': {
        const { namePath, value } = action;
        this.updateValue(namePath, value);
        break;
      }
      ...
      default:
      // Currently we don't have other action. Do nothing.
    }
  };

private updateValue = (name: NamePath, value: StoreValue) => {
    const namePath = getNamePath(name);
    const prevStore = this.store;
    this.updateStore(setValue(this.store, namePath, value));

    this.notifyObservers(prevStore, [namePath], {
      type: 'valueUpdate',
      source: 'internal',
    });
    ...
  };

可以看到当某一个Item的值发生改变时,首先 会更新form实例的数据仓库,然后 看看有没有字段依赖了当前更新的字段, 通知各订阅了消息的Item实例,最后再将 value 和 onChange 注入到子组件的props中完成闭环。

3. 源码中的设计模式

3.1. 观察者模式

3.1.1. 源码案例(见[2.1.2. 订阅与通知](#3.1.1. 源码案例(见2.1.2. 订阅与通知) "#h212"))

3.1.2. 解析

观察者模式提供了一种对象间一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并自动更新。

结合观察者模式的特点,我们在日常的编码中:

  1. 目前很多复杂的后台编辑项,经常会有不同项联动的效果,开发者就可以考虑优先使用Form表单自带的观察者模式架构去实现联动。
  2. 后台的表单只是观察者模式的实践之一,结合观察者模式 的抽象概念,有很多业务场景的功能可以考虑使用这种架构去组织代码,例如:购物车场景 。有多个组件依赖数据仓库 (购物车),数据仓库的变更会通知 各组件。大多数的真实场景购物车的初始数据可能是由后端下发的,其实也就是一次修改数据仓库的行为,包括用户的交互。而用户的交互等行为修改数据仓库后,统一通知各订阅单位 ,包括上传购物车数据至后端,也可以看作是后端的对数据仓库的订阅

在不同的场景中,观察者模式帮助实现了行为实体间的解耦,使得一个实体的变化可以通知到其他关联的实体。

3.2. 单例模式

3.2.1. 源码案例

ini 复制代码
function useForm<Values = any>(form?: FormInstance<Values>): [FormInstance<Values>] {
  const formRef = React.useRef<FormInstance>();
  ...
  
  if (!formRef.current) {
    if (form) {
      formRef.current = form;
    } else {
      ...
      const formStore: FormStore = new FormStore(forceReRender);

      formRef.current = formStore.getForm();
    }
  }
  ...
  return [formRef.current];
}

3.2.2. 解析

上面的代码中通过 useForm hook,确保表单状态的唯一性,以避免不同实例之间的状态冲突。

单例模式和观察者模式在管理全局状态、资源、事件等比较适合在一起使用,两个模式的概念能够比较好的融合,观察者模式一对多的对象关系,那中心数据仓库就可以使用单例模式管理起来,提供了一种可维护和解耦的方式。

4. 总结

在中后台的业务开发中,表单必不可少,合理的借用表单组件的设计模式来组织自己的代码结构是我们需要上的第一课。

脱离表单原理的视角,抽象后的设计模式,能让我们在未来的开发中,又多了一件趁手的武器

后续的这个中后台系列的文章都会在为大家讲解原理的基础 上,深入考究在设计模式/代码结构上有哪些值得借鉴学习的地方。

还有哪些模块 值得我们一起学习讨论 欢迎在评论区中留言

长话短说,只讲干货,我们下期再见!

最后

📚 小茗文章推荐:

关注公众号「Goodme前端团队」,获取更多干货实践,欢迎交流分享~

相关推荐
m0_748247551 小时前
Web 应用项目开发全流程解析与实战经验分享
开发语言·前端·php
m0_748255022 小时前
前端常用算法集合
前端·算法
真的很上进2 小时前
如何借助 Babel+TS+ESLint 构建现代 JS 工程环境?
java·前端·javascript·css·react.js·vue·html
web130933203982 小时前
vue elementUI form组件动态添加el-form-item并且动态添加rules必填项校验方法
前端·vue.js·elementui
NiNg_1_2342 小时前
Echarts连接数据库,实时绘制图表详解
前端·数据库·echarts
如若1233 小时前
对文件内的文件名生成目录,方便查阅
java·前端·python
滚雪球~4 小时前
npm error code ETIMEDOUT
前端·npm·node.js
沙漏无语4 小时前
npm : 无法加载文件 D:\Nodejs\node_global\npm.ps1,因为在此系统上禁止运行脚本
前端·npm·node.js
supermapsupport4 小时前
iClient3D for Cesium在Vue中快速实现场景卷帘
前端·vue.js·3d·cesium·supermap
brrdg_sefg4 小时前
WEB 漏洞 - 文件包含漏洞深度解析
前端·网络·安全