本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战! javascript
这是第 107 篇不掺水的原创,想获取更多原创好文,请搜索公众号关注咱们吧~ 本文首发于政采云前端博客:最熟悉的陌生人rc-form前端
咱们也许会常用例如 Ant Design、Element UI、Vant 等第三方组件库来快速在项目中完成页面的布局效果和简单的交互功能。java
可是咱们可能会忽略掉在这些优秀的第三方库中的某些组件可能也依赖于其余优秀的库!正如咱们使用频率很高的 Ant Design 中的 Form 组件(这里我说的是 React 版本的)。react
其实这些优秀的开源库内部使用了优秀的第三方库 rc-form,正如咱们常用的 getFieldDecorator、getFieldsValue、setFieldsValue、validateFields 等这些 Api,其实这些都是 rc-form 暴露出来的方法。ios
咱们都知道 React 框架设计模式和 Vue 不一样,Vue 中做者已经帮咱们实现了数据的双向绑定,数据驱动视图,视图驱动数据的改变,可是 React 中须要咱们手动调用 setState 实现数据驱动视图的改变,请看下面的代码。git
import React, { Component } from "react";
export default class index extends Component {
state = {
value1: "peter",
value2: "123",
value3: "23",
};
onChange1 = ({ target: { value } }) => {
this.setState({ value1: value });
};
onChange2 = ({ target: { value } }) => {
this.setState({ value2: value });
};
onChange3 = ({ target: { value } }) => {
this.setState({ value3: value });
};
submit = async () => {
const { value1, value2, value3 } = this.state;
const obj = {
value1,
value2,
value3,
};
const res = await axios("url", obj)
};
render() {
const { value1, value2, value3 } = this.state;
return (
<div> <form action=""> <label for="">用户名: </label> <input type="text" value={value1} onChange={this.onChange1} /> <br /> <label for="">密码: </label> <input type="text" value={value2} onChange={this.onChange2} /> <br /> <label for="">年龄: </label> <input type="text" value={value3} onChange={this.onChange3} /> <br /> <button onClick={this.submit}>提交</button> </form> </div>
);
}
}
复制代码
上面是一个表单登陆的简单功能!要想实现表单数据的实时更新须要在表单 onChange 的时候手动更新 state 状态;axios
从上面代码中能够看出,这样写功能也能实现,可是当咱们的表单多的时候,难道页面要写十几个 onChange 事件去实现页面的数据驱动视图的更新吗?这样考虑一下实际上是不妥的;后端
这个时候 rc-form 就应运而生了,rc-form 建立一个数据集中管理仓库,这个仓库负责统一收集表单数据验证、重置、设置、获取值等逻辑操做,这样咱们就把重复无用功交给 rc-form 来处理了,以达到代码的高度可复用性!设计模式
Api 名称 | 说明 | 类型 |
---|---|---|
getFieldDecorator | 用于和表单进行双向绑定, | Function(name) |
getFieldsValue | 获取一组字段名对应的值,会按照对应结构返回。默认返回现存字段值,当调用 getFieldsValue(true) 时返回全部值 |
(nameList?: NamePath[], filterFunc?: (meta: { touched: boolean, validating: boolean }) => boolean) => any |
getFieldValue | 获取对应字段名的值 | (name: NamePath) => any |
setFieldsValue | 设置一组表单的值 | (values) => void |
setFields | 设置一组字段状态 | (fields: FieldData[]) => void |
validateFields | 触发表单验证 | (nameList?: NamePath[]) => Promise |
isFieldValidating | 检查一组字段是否正在校验 | (name: NamePath) => boolean |
getFieldProps | 获取对应字段名的属性 | (name: NamePath) => any |
import { createForm } from "../../rc-form";
// import ReactClass from './ReactClass'
const RcForm = (props) => {
const {
form: { getFieldDecorator, validateFields },
} = props;
const handleSubmit = (e) => {
e && e.stopPropagation();
validateFields((err, value) => {
if (!err) {
console.log(value);
}
});
};
return (
<div style={{ padding: 20, background: "#fff" }} > <form> <label>姓名:</label> {getFieldDecorator("username", { rules: [{ required: true, message: "请输入用户名!" }], initialValue:'initialValue', })(<input type="text" />)} <br /> <label>密码:</label> {getFieldDecorator("password", { rules: [ { required: true, message: "请输入密码!" }, { pattern: /^[a-z0-9_-]{6,18}$/, message:'只容许数字!' } ], })(<input type="password" style={{ marginTop: "15px" }} /> )} <br /> <button onClick={handleSubmit} style={{ marginTop: "15px" }}> 提交 </button> </form> </div>
);
};
export default createForm()(RcForm);
复制代码
注意: 通过 createForm 方法处理的组件(就是 Ant Design 中 Form 的 create( ) 方法),会自动向组件没注入 form 对象,组件自己也就拥有了这些 Api 。数组
Demo 只是简单的基于 rc-form 实现了表单的装饰、表单验证、数据收集等功能。那么如何实现更加具备针对性的,适用多种业务场景的表单组件呢?
绕开优秀的开源的组件库不说,若是哪一天这些优秀的开源做品再也不开源了,那咱们怎么办?
为了不这种状况发生,或者若是仅是为了咱们本身的职业生涯规划,使本身更上一层楼的话也是有必要的去学习一下优秀的三方库的设计理念。就算看一下别人的代码风格也是有必要的。其实仍是须要咱们本身了解 rc-form 的设计思路的;只有了解了这些优秀开源做品的精髓,咱们即便不用开源库,也能够封装本身的代码库以及相似 Ant Design 中 Form 这些优秀的组件的。
都知道咱们平时编写业务组件通常只要用到表单都会用到 createForm 或者 Form.create( ) 这些方法对本身的组件进行包装,那么咱们就从这里开始咱们的故事。
import createBaseForm from './createBaseForm';
function createForm(options) {
return createBaseForm(options, [mixin]);
}
export default createForm;
复制代码
能够看到其实 createForm 只是作了一层封装,真正的调用函数是 createBaseForm,那么着重看一下 createBaseForm 函数内部实现。
上面的图片中能够看出这个函数利用闭包特性返回一个新函数,这个函数的参数其实就是你的业务组件对象,通过 createBaseForm 内部加工以后返回给你的是一个注入了 form 对象的组件。也就是咱们常说的这个 createBaseForm 是一个高阶组件。
那么也就清楚了 Ant Design 的 Form.create()
方法就是 rc-form
中的 createBaseForm
方法的替代!通过 createBaseForm
包装的组件将会注入 form 对象, 而 form
属性中提供的 getFieldDecorator 以及 fieldsStore 实例则是实现数据自动收集的关键。
咱们就先从最初的的渲染表单的逻辑开始,咱们业务场景中用到的表单组件都会使用 getFieldDecorator 包装一下。固然,我说的是 Ant Design 4.0 之前的版本, 那么咱们就先从这里开始看起。
这里首先说明一下,此篇文章我只是浅析一下整个表单数据双向绑定的简单过程,由于这个是 rc-form 的核心,精力有限具体的细节处理留待之后慢慢研究。那么咱们就来看一下 getFieldDecorator
方法作了些什么?
getFieldDecorator(name, fieldOption) {
const props = this.getFieldProps(name, fieldOption);
return fieldElem => {
// We should put field in record if it is rendered
this.renderFields[name] = true;
const fieldMeta = this.fieldsStore.getFieldMeta(name);
const originalProps = fieldElem.props;
fieldMeta.originalProps = originalProps;
fieldMeta.ref = fieldElem.ref;
const decoratedFieldElem = React.cloneElement(fieldElem, {
...props,
...this.fieldsStore.getFieldValuePropValue(fieldMeta),
});
return supportRef(fieldElem) ? (
decoratedFieldElem
) : (
<FieldElemWrapper name={name} form={this}> {decoratedFieldElem} </FieldElemWrapper>
);
};
},
复制代码
此处我删除了一些可有可无的代码,由于这样看起来更加清晰明了。 首先对传入的表单组件调用 getFieldProps 方法进行了 props 的构建处理,接着返回一个函数,这个函数参数就是咱们使用 getFieldDecorator 传入的表单组件,调用 fieldsStore 中的 getFieldMeta 获取表单组件的配置数据,兼容原有组件的配置属性以及对不支持 ref 组件的处理,最终返回一个克隆后的挂载处理后的一些配置对象的组件!
既然用到了 fieldsStore,那么这里要说一下 fieldsStore,fieldsStore 中包含了当前 form 的主要信息和一些处理表单数据的方法。
class FieldsStore {
constructor(fields) {
this.fields = internalFlattenFields(fields);
this.fieldsMeta = {};
}
}
复制代码
fieldMeta 能够当作是一个表单项的描述,以传入的 name 为索引 key,支持嵌套、存储表单数据, 即配置信息不涉及值的问题,主要包括:
onChange
checked
fields 主要用于记录每一个表单的实时属性,主要包括:
dirty 数据是否已经改变,但未校验
errors 校验文案
name 字段名称
touched 数据是否更新过
value 字段的值
validating 校验状态
那么接下来仍是要看一下 getFieldProps 方法内部是如何实现 props 构建的?
getFieldProps(name, usersFieldOption = {}) {
// 从新组装 props
const fieldOption = {
name,
trigger: DEFAULT_TRIGGER,
valuePropName: 'value',
validate: [],
...usersFieldOption,
};
const {
rules,
trigger,
validateTrigger = trigger,
validate,
} = fieldOption;
const fieldMeta = this.fieldsStore.getFieldMeta(name);
// 初始值处理
if ('initialValue' in fieldOption) {
fieldMeta.initialValue = fieldOption.initialValue;
}
// 组装 inputProps
const inputProps = {
...this.fieldsStore.getFieldValuePropValue(fieldOption),
ref: this.getCacheBind(name, `${name}__ref`, this.saveRef),
};
if (fieldNameProp) {
inputProps[fieldNameProp] = formName ? `${formName}_${name}` : name;
}
// 收集验证规则
const validateRules = normalizeValidateRules(validate, rules, validateTrigger);
const validateTriggers = getValidateTriggers(validateRules);
validateTriggers.forEach((action) => {
if (inputProps[action]) return;
inputProps[action] = this.getCacheBind(name, action, this.onCollectValidate);
});
// 不走效验的组件使用 onCollect 收集组件的值
if (trigger && validateTriggers.indexOf(trigger) === -1) {
inputProps[trigger] = this.getCacheBind(name, trigger, this.onCollect);
}
return inputProps;
},
复制代码
删除了一些细节代码, 先来看看 getFieldProps 首先进行了默认值的处理,若是用户没有设置 trigger
和 valuePropName
则使用默认值,随后调用 fieldsStore
中的getFieldMeta
方法,fieldsStore
实例对象在整个过程当中尤其关键,它的做用是做为一个数据中心,让咱们免除了手动去维护 form
中绑定的各个值。那么咱们看一下 fieldsStore.getFieldMeta
作了那些工做?
getFieldMeta(name) {
this.fieldsMeta[name] = this.fieldsMeta[name] || {};
return this.fieldsMeta[name];
}
复制代码
此函数做用在于根据组件传递的 name 属性获取数据中心的 fieldMeta,若是没有则默认空对象,也就是首次渲染返回初始值。 重要的是 inputProps 的组装环节,第一步调用 getFieldValuePropValue
方法获取当前 props,而后加入 ref 属性,接下来是效验规则的收集。
const validateRules = normalizeValidateRules(validate, rules, validateTrigger);
const validateTriggers = getValidateTriggers(validateRules);
validateTriggers.forEach((action) => {
if (inputProps[action]) return;
inputProps[action] = this.getCacheBind(name, action, this.onCollectValidate);
});
if (trigger && validateTriggers.indexOf(trigger) === -1) {
inputProps[trigger] = this.getCacheBind(name, trigger, this.onCollect);
}
复制代码
validateRules
便是全部的表单组件效验规则,validateTriggers
即全部效验规则触发的事件名, 那么咱们就看一下 nomalizeValidateRules
以及 getValidateTriggers
方法是如何收集验证规则的。
function normalizeValidateRules(validate, rules, validateTrigger) {
const validateRules = validate.map((item) => {
const newItem = {
...item,
trigger: item.trigger || [],
};
if (typeof newItem.trigger === 'string') {
newItem.trigger = [newItem.trigger];
}
return newItem;
});
if (rules) {
validateRules.push({
trigger: validateTrigger
? [].concat(validateTrigger)
: [],
rules,
});
}
return validateRules;
}
function getValidateTriggers(validateRules) {
return validateRules
.filter(item => !!item.rules && item.rules.length)
.map(item => item.trigger)
.reduce((pre, curr) => pre.concat(curr), []);
}
复制代码
其会将 validate
、 rules
组合,返回一个数组,其内部的元素为一个个规则对象,而且每一个元素都存在一个能够为空的 trigger
数组,而且将 validateTrigger
做为 rule
的 triggers
推入 validateRules
中,咱们回回头看一下 validateTrigger
。
const fieldOption = {
name,
trigger: DEFAULT_TRIGGER,
valuePropName: 'value',
validate: [],
...usersFieldOption,
};
const {
rules,
trigger,
validateTrigger = trigger,
validate,
} = fieldOption;
复制代码
这里能够看出若是用户配置了触发验证方法时默认使用配置的 trigger
,若是用户没有设置 trigger
则默认使用默认 onChange
。
getValidateTriggers
则是将全部触发事件统一收集至一个数组,随后经过 forEach 循环将全部 validateTriggers
中的事件都绑定上同一个处理函数 getCacheBind 上。
validateTriggers.forEach((action) => {
if (inputProps[action]) return;
inputProps[action] = this.getCacheBind(
name,
action,
this.onCollectValidate
);
});
复制代码
下面再来看一下触发验证规则绑定事件 action 的 getCacheBind 函数作了哪些操做?
getCacheBind(name, action, fn) {
if (!this.cachedBind[name]) {
this.cachedBind[name] = {};
}
const cache = this.cachedBind[name];
if (
!cache[action] ||
cache[action].oriFn !== fn
) {
cache[action] = {
fn: fn.bind(this, name, action),
oriFn: fn,
};
}
return cache[action].fn;
},
复制代码
暂且忽略 cachedBind 方法,这里能够看到 getCacheBind 方法主要对传入的 fn 作了一个改变 this 指向的逻辑处理,真正的处理函数则是 onCollectValidate
,那咱们来看一下 onCollectValidate
作了什么?
onCollectValidate(name_, action, ...args) {
const { field, fieldMeta } = this.onCollectCommon(name_, action, args);
const newField = {
...field,
dirty: true,
};
this.fieldsStore.setFieldsAsDirty();
this.validateFieldsInternal([newField], {
action,
options: {firstFields: !!fieldMeta.validateFirst,},
});
},
复制代码
当 onCollectValidate
被调用,也就是数据校验函数被触发时,首先调用了 onCollectCommon 方法,那么这个函数是干什么的?
onCollectCommon(name, action, args) {
const fieldMeta = this.fieldsStore.getFieldMeta(name);
if (fieldMeta[action]) {
fieldMeta[action](...args);
} else if (fieldMeta.originalProps && fieldMeta.originalProps[action]) {
fieldMeta.originalProps[action](...args);
}
const value = fieldMeta.getValueFromEvent ?
fieldMeta.getValueFromEvent(...args) :
getValueFromEvent(...args);
if (onValuesChange && value !== this.fieldsStore.getFieldValue(name)) {
const valuesAll = this.fieldsStore.getAllValues();
const valuesAllSet = {};
valuesAll[name] = value;
Object.keys(valuesAll).forEach(key => set(valuesAllSet, key, valuesAll[key]));
onValuesChange({
[formPropName]: this.getForm(),
...this.props
}, set({}, name, value), valuesAllSet);
}
const field = this.fieldsStore.getField(name);
return ({ name, field: { ...field, value, touched: true }, fieldMeta });
},
复制代码
onCollectCommon
主要是获取了包装组件最新的值,随后将其包装在对象中返回,返回后将其组装为一个新的名为 newField
的对象。
而 fieldsStore.setFieldsAsDirty
则是标记包装组件的校验状态,暂且略过,随后执行 validateFieldsInternal
,咱们看一下 validateFieldsInternal 函数。
validateFieldsInternal( fields, { fieldNames, action, options = {} }, callback, ) {
const allRules = {};
const allValues = {};
const allFields = {};
const alreadyErrors = {};
fields.forEach(field => {
const name = field.name;
if (options.force !== true && field.dirty === false) {
if (field.errors) {
set(alreadyErrors, name, { errors: field.errors });
}
return;
}
const fieldMeta = this.fieldsStore.getFieldMeta(name);
const newField = {
...field,
};
newField.errors = undefined;
newField.validating = true;
newField.dirty = true;
allRules[name] = this.getRules(fieldMeta, action);
allValues[name] = newField.value;
allFields[name] = newField;
});
this.setFields(allFields);
// in case normalize
Object.keys(allValues).forEach(f => {
allValues[f] = this.fieldsStore.getFieldValue(f);
});
if (callback && isEmptyObject(allFields)) {
callback(
isEmptyObject(alreadyErrors) ? null : alreadyErrors,
this.fieldsStore.getFieldsValue(fieldNames),
);
return;
}
// console.log(allRules);
const validator = new AsyncValidator(allRules);
if (validateMessages) {
// console.log(validateMessages);
validator.messages(validateMessages);
}
validator.validate(allValues, options, errors => {
const errorsGroup = {
...alreadyErrors,
};
// ...
const expired = [];
const nowAllFields = {};
Object.keys(allRules).forEach(name => {
const fieldErrors = get(errorsGroup, name);
const nowField = this.fieldsStore.getField(name);
// avoid concurrency problems
if (!eq(nowField.value, allValues[name])) {
expired.push({
name,
});
} else {
nowField.errors = fieldErrors && fieldErrors.errors;
nowField.value = allValues[name];
nowField.validating = false;
nowField.dirty = false;
nowAllFields[name] = nowField;
}
});
this.setFields(nowAllFields);
// ...
}
复制代码
由于 validateFieldsInternal
主要逻辑都是在调用 AsyncValidator
进行异步校验以及对特殊场景的处理,咱们暂时略过只看数据收集部分,咱们看到在最后调用了 this.setFields(allFields);
并传入了新的值,接下来就看一下 setFields
方法。
setFields(maybeNestedFields, callback) {
const fields = this.fieldsStore.flattenRegisteredFields(maybeNestedFields);
this.fieldsStore.setFields(fields);
if (onFieldsChange) {
const changedFields = Object.keys(fields)
.reduce((acc, name) => set(acc, name, this.fieldsStore.getField(name)), {});
onFieldsChange({
[formPropName]: this.getForm(),
...this.props
}, changedFields, this.fieldsStore.getNestedAllFields());
}
this.forceUpdate(callback);
},
复制代码
咱们能够看到,setFields
首先对传入的值进行与初始化类似的验证,随后调用 fieldsStore 实例中的 setFields 方法将值存入 fieldsStore
, 暂时忽略 onFieldsChange
,以后调用 forceUpdate
更新视图。到此,咱们简单的描述了整个流程。
表单数据更新大体流程以下:
总结:
用户输入或者选择表单组件的行为都会触发 getFieldDecorator(HOC) 高阶组件,进而调用 getFieldProps 组装组件 props,这个方法中若是表单组件中配置了 validateRules 以及 validateTriggers 的话(也就是 rules 对象)就调用 onCollectValidate 方法收集效验规则。而后就是设置表单组件的最新的值到 fieldsStore 中, 并调用 this.forceUpdate( ) 更新 UI 视图!
若是咱们没有配置 validateRules 以及 validateTriggers 等规则,那就使用 onCollect 方法收集最新的数据并更新到 fieldsStore 中。不对表单进行单独验证,,从而在设置最新值 setFields 方法中调用 this.forceUpdate( ) 更新 UI 视图!
总结:
想一下假如当我改变输入框的值得时候是否是会引发表单的从新渲染的问题。 因此这也就致使了渲染性能的问题! 那么必然会有优化的方法,有兴趣的能够看看 rc-field-form。
文章只是总体浅析实现思路,若有不一样意见,欢迎联系我交流!
Serverless Custom (Container) Runtime
开源地址 www.zoo.team/openweekly/ (小报官网首页有微信交流群)
政采云前端团队(ZooTeam),一个年轻富有激情和创造力的前端团队,隶属于政采云产品研发部,Base 在风景如画的杭州。团队现有 40 余个前端小伙伴,平均年龄 27 岁,近 3 成是全栈工程师,妥妥的青年风暴团。成员构成既有来自于阿里、网易的“老”兵,也有浙大、中科大、杭电等校的应届新人。团队在平常的业务对接以外,还在物料体系、工程平台、搭建平台、性能体验、云端应用、数据分析及可视化等方向进行技术探索和实战,推进并落地了一系列的内部技术产品,持续探索前端技术体系的新边界。
若是你想改变一直被事折腾,但愿开始能折腾事;若是你想改变一直被告诫须要多些想法,却无从破局;若是你想改变你有能力去作成那个结果,却不须要你;若是你想改变你想作成的事须要一个团队去支撑,但没你带人的位置;若是你想改变既定的节奏,将会是“5 年工做时间 3 年工做经验”;若是你想改变原本悟性不错,但老是有那一层窗户纸的模糊… 若是你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的本身。若是你但愿参与到随着业务腾飞的过程,亲手推进一个有着深刻的业务理解、完善的技术体系、技术创造价值、影响力外溢的前端团队的成长历程,我以为咱们该聊聊。任什么时候间,等着你写点什么,发给 ZooTeam@cai-inc.com