Antd 中的组件大部分基于蚂蚁金服的组件库 react-component。antd 与 react-component 都是开源项目,阅读其源码可以给我们带来很多收益,比如:
但是阅读过 Antd 源码就会发现,代码量巨大而且庞杂,不易抓住重点,很容易导致从入门到放弃。我个人的看法是阅读框架源码的目的是掌握其核心实现思想,整体的运行流程,对我们自己面临类似需求时有所帮助与借鉴。没有必要对代码一行一行的死抠,代码的精度应介于粗粒度与细粒度之间。
我的阅读方法分为三步:
接下来的过程中我不会把 antd 中的源码拎出来解读,我会介绍极简版组件的实现思路,争取用最少的代码展现 antd 组件背后的思想。读者可以将极简版与 antd 的源码做一个对照来加深理解。
Form 表单具有数据收集、校验和提交功能的表单,包含复选框、单选框、输入框、下拉选择框等元素。
表单一定会包含表单域,表单域可以是输入控件,标准表单域,标签,下拉菜单,文本域等。Antd 中表单域形式为
<Form.Item {...props}>
{children}
</Form.Item>
核心功能点作为组件能正常使用的最小集,对 Form 表单组件而言,应该能够完成数据收集,校验,提交这三个核心功能。下面这张图是我归集的核心功能点:
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;
getFieldDecorator 的关键字是接管
经过 getFieldDecorator 包装的控件,表单控件会自动添加 value(或 valuePropName 指定的其他属性)onChange(或 trigger 指定的其他属性),数据同步将被 Form 接管,这会导致以下结果:
- 你不再需要也不应该用 onChange 来做同步,但还是可以继续监听 onChange 等事件。
- 你不能用控件的 value defaultValue 等属性来设置表单域的值,默认值可以用 getFieldDecorator 里的initialValue。
- 你不应该用 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>;
};
}
从代码可以看出,实现有如下几个要点:
在 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 = {};
// 不传参数返回所有values
if (!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) => {
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: ''
}
})
});
}
/**
* 校验表单。如果不传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 的数据结构如下:
在 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 field
const 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 field
const props = input.props.children[0].props;
if (props) {
registerField(props.name, props.rules, props.value);
}
});
}
render() {
...
}
}
Form 表单的字段布局主要依赖于 Grid (24栅格)。主要通过 Form.Item 的 labelCol 和 wrapperCol
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 导航菜单