前言

Antd 中的组件大部分基于蚂蚁金服的组件库 react-component。antd 与 react-component 都是开源项目,阅读其源码可以给我们带来很多收益,比如:

  • 了解各式各样的组件背后的实现思想
  • 怎样去实现一个对开发和用户都友好的组件,即简单易用,便于扩展。
  • 学习一些我们在写业务代码时不太会用到的 React 高级用法

但是阅读过 Antd 源码就会发现,代码量巨大而且庞杂,不易抓住重点,很容易导致从入门到放弃。我个人的看法是阅读框架源码的目的是掌握其核心实现思想,整体的运行流程,对我们自己面临类似需求时有所帮助与借鉴。没有必要对代码一行一行的死抠,代码的精度应介于粗粒度与细粒度之间。

我的阅读方法分为三步:

  1. 阅读 Antd 组件的官方文档,整理出组件的核心方法与属性
  2. 找出核心方法与属性在源码中的位置,作为阅读起点
  3. 了解到组件的实现思想后,自己搞一个极简版,在手撸的过程中加深体会。

接下来的过程中我不会把 antd 中的源码拎出来解读,我会介绍极简版组件的实现思路,争取用最少的代码展现 antd 组件背后的思想。读者可以将极简版与 antd 的源码做一个对照来加深理解。

Antd Form 简述

Form 表单具有数据收集、校验和提交功能的表单,包含复选框、单选框、输入框、下拉选择框等元素。

表单一定会包含表单域,表单域可以是输入控件,标准表单域,标签,下拉菜单,文本域等。Antd 中表单域形式为 <Form.Item />

<Form.Item {...props}>{children}
</Form.Item>

核心功能点提取

核心功能点作为组件能正常使用的最小集,对 Form 表单组件而言,应该能够完成数据收集,校验,提交这三个核心功能。下面这张图是我归集的核心功能点:

核心实现

数据收集 与 校验

Form.create()

Form.create() 用来包装业务场景中会用到 Form 的组件。通过 Form.create() 包装后,在业务组件中可以调用 this.props.form 来做表单的验证 (validateFIelds),表单字段值的设置 (setFieldValue),表单字段值的获取 (getFieldValue) 等。

class FormDemo extends React.Component {render() {return <Form><Form.Item {...props}>this.props.form.getFieldDecorator([name], {initialValue: '',rules: [...],getValueFromEvent: callback})(<Input />);</Form.Item></Form>}
}export default Form.create()(FormDemo);

在实现层面上,Form.create() 会返回一个高阶函数 (HOC),此高阶函数通过属性代理(Props Proxy),将 props form 赋给作为参数传入的业务组件 WrappedComponent。通过 form 对象中的一系列方法,WrappedComponent 在原业务组件的基础上被赋予了更强大的能力(主要是指Form表单中输入控件的数据同步将被 Form 接管)。这种能力就体现在我们经常使用的 this.props.form[method]

function create() {return function decorate(WrappedComponent) {return class Decorator extends React.Component {static displayName = `HOC(${getDisplayName(WrappedComponent)})`;private form: WrappedFormUtils = {// 表单进行双向绑定getFieldDecorator: (name: string, props: FieldDecratorProps) =>       {....},// 获取输入控件的值getFieldsValue: (fieldNames) => {...},// 设置输入控件的值setFieldValue: (name, value) => {...},}....render() {return <WrappedComponent form={this.form}></WrappedComponent>;}};};
}export default create;

this.props.form[method]

getFieldDecorator(id, options)

getFieldDecorator 的关键字是接管

经过 getFieldDecorator 包装的控件,表单控件会自动添加 value(或 valuePropName 指定的其他属性)onChange(或 trigger 指定的其他属性),数据同步将被 Form 接管,这会导致以下结果:

  1. 你不再需要也不应该用 onChange 来做同步,但还是可以继续监听 onChange 等事件。
  2. 你不能用控件的 value defaultValue 等属性来设置表单域的值,默认值可以用 getFieldDecorator 里的initialValue。
  3. 你不应该用 setState,可以使用 this.props.form.setFieldsValue 来动态改变表单值。

自实现代码如下:

/*** 经过 getFieldDecorator 包装的控件, 数据同步将被 Form 接管包括设置控件值,校验参数等** @param {string} name 控件字段名*  @param {FieldDecratorProps} props 参数* @return {function} 包裹控件的高阶函数组件
*/
getFieldDecorator: (name: string, props: FieldDecratorProps) => {const {rules, initialValue, getValueFromEvent, trigger, ...others} = props;return FormItemComponent => {const error = this.getFieldError(name);return <div>{React.cloneElement(FormItemComponent, {warning: error ? true : false,name,onChange: (e: any) => {let value = e;if (typeof props.getValueFromEvent === 'function') {value = props.getValueFromEvent(e);} else if (e.target) {value = e.target.value;}this.form.setFieldValue(name, value).then(() => {if (!trigger || trigger === 'onChange') {this.validateField(name);}});},onBlur: (e: any) => {if (trigger === 'onBlur') {this.validateField(name);}},rules,value: this.state[name] ? this.state[name].value : initialValue,...others})}{error && <div className="miyun-form-item-error">{error}</div>}</div>;};
}

从代码可以看出,实现有如下几个要点:

  • 通过 React.cloneElement,为即将被监管的输入控件 (Input, Select etc…):

    1. 添加 onChange, onBlur 用来设置字段值,校验等,从而实现所谓 ‘接管’。(通过 trigger 来决定校验在 change 或 blur 时触发 )
    2. 输入控件初始值设置为 initialValue
  • 为校验结果添加错误展示信息。 (被包裹组件无感知,由 HOC 处理)
getFieldValue(name) 与 getFieldsValue(fieldNames)

在 Form.create() 返回的的高阶组件层面维护一个动态生成的维护输入控件信息的 state,包括控件 name, value, initialValue, rules 等信息。

state 结构如下:

  [name] : {rules,value: initialValue,initialValue: this.deepCopyValue(initialValue),error: ''}

方法实现如下:

 getFieldValue: (name) => {return this.state[name].value;}getFieldsValue: (fieldNames) => {const values = {};// 不传参数返回所有valuesif (!fieldNames) {this.fieldNames.forEach(name => {values[name] = this.state[name].value;});} else if (fieldNames instanceof Array) {fieldNames.forEach(name => {values[name] = this.state[name].value;});}return values;}
setFieldValue(name, value) 与 resetFields(names)
// 设置一组输入控件的值
setFieldValue: (name, value) => {return new Promise( (resolve, reject) => {this.setState({[name]: {...this.state[name],value}}, resolve);});
}// 重置一组输入控件的值(为 initialValue)与状态,如不传入参数,则重置所有组件
resetFields: names => {let fieldNames = (names instanceof Array && names.length > 0) ? names : this.fieldNames;fieldNames.forEach(name => {let currentState = this.state[name];this.setState({[name] : {...this.state[name],value: this.deepCopyValue(currentState.initialValue),error: ''}})});
}
validateFields 校验字段
/*** 校验表单。如果不传names,则校验所有字段** @param {string[] | function} names 表单字段names* @param {function} callback 校验结束回调*/validateFields: (names, callback) => {let fieldNames = null;let cb = null;let values = null;if (typeof names === 'function') {fieldNames = this.fieldNames;cb = names;values = this.form.getFieldsValue();} else {fieldNames = names ? names : this.fieldNames;cb = callback;values = this.form.getFieldsValue(fieldNames);}let isError = false;fieldNames.forEach(name => {if (this.validateField(name)) {isError = true;}});cb && cb(isError, values);}

注: 校验回调函数 callback 中参数 values 的数据结构如下:

Context 中 registerField 和 unregisterField

在 Form.Item componentDidMount 的时候,在 Form.create() 高阶组件 Decorator 中动态注册对应的 field。因此需要在 Decorator 中声明 context 用以传递关于注册/注销的方法。

function create() {return function decorate(WrappedComponent) {private form: WrappedFormUtils = {...}static childContextTypes = {registerField: PropTypes.func,unregisterField: PropTypes.func};getChildContext() {return {registerField: this.registerField,unregisterField: this.unregisterField};}// 注册表单控件registerField = (name, rules, initialValue) => {if (this.state[name]) {return;}this.setState({[name] : {rules,value: initialValue,initialValue: this.deepCopyValue(initialValue),error: ''}});this.fieldNames.push(name);}// 注销表单控件unregisterField = name => {this.setState({[name]: null}, () => {delete this.state[name];this.fieldNames.splice(this.fieldNames.indexOf(name), 1);});}}
}

Form.Item 中的相关实现代码:

export default class FormItem extends React.Component<IFormItemProps, any> {static contextTypes = {labelCol: PropTypes.object,layout: PropTypes.string,registerField: PropTypes.func,unregisterField: PropTypes.func,wrapperCol: PropTypes.object};componentWillUnmount() {const childrenArray = React.Children.toArray(this.props.children);const context = this.context;childrenArray.forEach((input: React.ReactElement) => {// TODO: 更优雅地获取 Form fieldconst props = input.props.children[0].props;if (props) {context.unregisterField(props.name);}});}componentDidMount() {const childrenArray = React.Children.toArray(this.props.children);const registerField = this.context.registerField;childrenArray.forEach((input: React.ReactElement) => {// TODO: 更优雅地获取 Form fieldconst props = input.props.children[0].props;if (props) {registerField(props.name, props.rules, props.value);}});}render() {...}
}

字段布局

Form 表单的字段布局主要依赖于 Grid (24栅格)。主要通过 Form.ItemlabelColwrapperCol

label 标签布局,同 组件,设置 span offset 值,如 {span: 3, offset: 12} 或 sm: {span: 3, offset: 12}

我们通过 Form.Item 的 render 方法实现就明白了:用户设置的 labelCol 和 wrapperCol 要符合 Gird 栅格系统的使用规范,Form.Item 作为一个高阶函数将这两个属性传递给 Grid.

另外,标签文本 label 和 标签 colon 也是在 Form.Item上设置的。

export default class FormItem extends React.Component<IFormItemProps, any> {...render() {const {classPrefix,className,label,labelAlign,children,colon,required,...restProps} = this.props;return <Row{...restProps}className={...}><Col{...labelCol}style={{textAlign: labelAlign}}className={...}><label>{ label && [label, colon] }</label></Col><Col{...wrapperCol}className={...}>{children}</Col></Row>;}
}

接下来我会继续发布我实现的另外一些组件如 Gird,Input,Menu, Select, Modal 等的实现思路,希望对您有所启发 ?

更多组件

更多组件自实现系列,更多文章请参考:

从Antd 源码到自我实现之 Grid 栅格系统

React 实现 Modal 思路简述

从Antd 源码到自我实现之 Menu 导航菜单

从Antd 源码到自我实现之 Form表单相关推荐

  1. 从Antd 源码到自我实现之 Menu 导航菜单

    Antd Menu 简述 Menu 为页面和功能提供导航的菜单列表. 导航菜单是一个网站的灵魂,用户依赖导航在各个页面中进行跳转.一般分为顶部导航和侧边导航,顶部导航提供全局性的类目和功能,侧边导航提 ...

  2. HTML入门必看-含源码案例(表格-列表-表单-图片-视频-音频)

    HTML知识点: 目录 HTML知识点: 一.第一个HTML代码 二.一些常用的简单元素,都是写在两个body标签之间的 粗体.斜体.下划线.删除效果 三.表格元素 1.普通表格     2.合并表格 ...

  3. js模拟form表单提交数据, js模拟a标签点击跳转,避开使用window.open引起来的浏览器阻止问题...

    js模拟form表单提交数据, js模拟a标签点击跳转,避开使用window.open引起来的浏览器阻止问题 js模拟form表单提交数据源码: /** * js模拟form表单提交 * @param ...

  4. Antd源码浅析(二)InputNumber组件 一

    前言 上篇我们讲了Icon组件,Icon组件是Antd源码库中实现比较简单的组件,适合大家入门,这篇文章主要和大家一起分析一下数字输入框组件,即InputNumber,难度适中,但蕴含的Antd里较为 ...

  5. antd源码-spin解析

    antd源码-spin解析 spin的作用是代表当前块正在加载中 Spin 元素的渲染 renderSpin = ({ getPrefixCls }: ConfigConsumerProps) =&g ...

  6. antd源码-Affix固钉解析

    antd源码-Affix固钉解析 固钉其实按照我自己的理解就是用固定定位将其定位到某个位置.很简单的一个效果. antd-affix我认为其核心可以概括为几点: 组件加载滚动监听,组件销毁销毁监听. ...

  7. django之:网页伪静态 JsonResponse form表单携带文件数据 CBV源码分析 模板语法传值 模板语法之过滤器 标签 自定义标签函数 过滤器、inclusion_tag模板的继承导入

    目录标题 一:网页伪静态 1.定义 2.如何实现 二:视图层 1.视图函数返回值问题 2.视图层返回json格式的数据 3.form表单携带文件数据 4.CBV源码分析 1.CBV和FBV: 2.CB ...

  8. react antd form 表单清空

    关于react_antd_desgin的学习这两天也是获取到的知识零零散散,大多在网上也能获取的到,所以隔了很长的时间,没有编写关于react相关的文章.今天之所以写也是因为公司中秋节放假,在郑州,窗 ...

  9. antd vue form表单 子组件调用父组件的方法没反应_前几天推了Vue,今天给React疯狂打call...

    在主流前端框架里,因为React的入门难度高而果断投入Vue怀抱的人绝不在少数.但我要告诉大家,如果你有一定的js基础,其实React没你想象中那么困难 任何库都要在「概念简洁」(自由度高)和「使用便 ...

最新文章

  1. WCF 改成 restful api
  2. python-print
  3. Java应用中使用ShutdownHook友好地清理现场(转)
  4. [mmu/cache]-ARM cache的学习笔记-一篇就够了
  5. PS抠图神器: KnockOut 2.0安装汉化和使用教程
  6. 无法定位程序输入点 InitializeCriticalSectionEx 于动态链接库 Kernel32.dll 上 问题解决方法
  7. 大K提醒各位常备DOS杀毒盘
  8. 开发一个app需要多少钱、APP开发需要投入多少资金?
  9. 学而思css动画使用
  10. golang使用gomonkey和monkey来mock方法或者函数时报panic: permission denied [recovered]
  11. 使用ret2libc攻击方法绕过数据执行保护
  12. 【Gitee + Hexo】从0开始搭建自己的博客网站
  13. 这社会并不是靠努力和辛苦赚钱的
  14. Negroni和Gorilla/mux 解析 Golang
  15. socat的安装与使用
  16. 菜鸟与 cef 的邂逅之旅(三):Cef3 中 C++ 与 JavaScript 的互相调用
  17. 百度收购YY,走得一步好棋
  18. bat批处理文件建立和打开
  19. 在CSS中创建径向菜单
  20. 华为S6700交换机配置

热门文章

  1. QXRService:基于高通QXRService获取SLAM Camera图像
  2. 中国电信无线网服务器,如何使用路由器共享电信天翼无线网络?
  3. jira是干什么_如何用JIRA来做需求管理?
  4. 曾扬言 机器人合法公民_曾扬言“摧毁人类”的机器人索菲亚,现状如何?如果失控了咋办?...
  5. 笔记-15 网络编程入门 UDP通信程序 TCP通信程序 练习
  6. webdav同步书签-floccus
  7. Java后端程序员都做些什么?
  8. B2C电商网站提交订单支付流程
  9. 宝岛探险(求岛的大小)
  10. logline: 是时候聊一聊前端的日志了