2023-02-02
表單
在 React 中裸用表單需要維護(hù)大量的 value 和 onChange,自然需要選擇合適的表單方案。那么在做技術(shù)選型前,不妨先列出我們對(duì)于react 表單方案的期望。
基礎(chǔ)功能
這些功能點(diǎn)是每個(gè)表單方案必須擁有的,也都比較基礎(chǔ),市面上大部分表單方案 star 多的少的、自己造輪子的 也都會(huì)囊括這些功能。
?? 收集表單數(shù)據(jù)
?? 管理表單狀態(tài)(未驗(yàn)證、未提交、校驗(yàn)狀態(tài)等)
?? 支持表單校驗(yàn)
?? 支持自定義觸發(fā)校驗(yàn)時(shí)機(jī)(submit/hover/實(shí)時(shí)/自定義等)
?? 支持自定義校驗(yàn)錯(cuò)誤后的信息展示
?? 支持自定義組件/接入第三方UI庫(kù)
羅列表單方案
通過搜索引擎找出 比較常用的 / 成熟的 / 熱門的 表單方案:
· Antd Form
· Fusion Next Form
· formik
· react-final-form
· NoForm
· uform
· redux-form
· react-jsonschema-form
· Informed
· formal
這么多表單方案,該如何做選擇?我們先大致過一遍,redux-form 依賴 redux,dan 都說(shuō) You Might Not Need Redux,從耦合性的角度考慮表單方案不應(yīng)該依賴 redux ,同理 Fusion Next Form 是 fusion 內(nèi)建的表單方案,react-jsonschema-form 強(qiáng)依賴了 Bootstrap,暫不考慮。Antd Form 基于的 rc-form 可以脫離 Antd 使用。
hooks 形式的有 Informed、formal、formik@v2.x、react-final-form-hooks。對(duì)于新的一些技術(shù)我更看重他的使用場(chǎng)景,使用新技術(shù)會(huì)帶來(lái)什么好處?曾經(jīng)我也激進(jìn)過,一味求新,現(xiàn)在更多的是把技術(shù)當(dāng)作實(shí)現(xiàn)產(chǎn)品的工具。目前來(lái)看這些并沒有帶來(lái)特別明顯的優(yōu)勢(shì),反倒是需要承擔(dān)“小白鼠”的角色,所以暫且先觀望看看。
關(guān)注點(diǎn)
初篩完一輪后,詳細(xì)看了文檔,整理出了如下一些關(guān)注點(diǎn),希望通過這些功能點(diǎn)對(duì)這些方案做一次橫向的對(duì)比。
表單的描述形式
既然是 react 的表單方案,大部分都是基于 JSX 的。表單的最上層大同小異,無(wú)非會(huì)有個(gè) Form 或是自己的 Class,在這里不做討論,更多討論的是表單項(xiàng)的代碼書寫方式。
JSX + JSON
第一類的代表是 rc-form、formal,在 JSX 里寫一個(gè) JSON 描述校驗(yàn)規(guī)則,將和表單項(xiàng)有關(guān)的信息(字段名,校驗(yàn)規(guī)則等)都集中在一處描述,通過展開運(yùn)算符向 UI 組件傳入“處理好的”props,自動(dòng)綁定 value、onChange。 這個(gè)應(yīng)該是最常用的,我的感受是表單一旦多起來(lái)或是代碼寫多了,會(huì)占用大量篇幅,滿屏幕的 JSON 可能會(huì)有點(diǎn)視覺疲勞。
// createForm()(Component)
render() {
const { getFieldProps } = this.props.form;
return (
<input {...getFieldProps('name', {
rules: {
required: true,
message: 'Please input your name!'
}
})}/>
);
}
表單元素抽象概念
第二大類則是有 Field 或是 FormItem (之后簡(jiǎn)稱為 F)的表單元素概念,這樣的設(shè)計(jì)我更加看好,將 JSON 的寫法改成了正常的 JSX,整體感官上舒服了不少,也有比較多的庫(kù)都實(shí)現(xiàn)了類似的 API。F 作為表單元素的各種抽象,對(duì)外提供一致接口,例如字段名、校驗(yàn)規(guī)則、表單域組件、value 等等。
<F type="email" name="email" placeholder="Email" />
<F component="select" name="color">
<option value="red">Red</option>
<option value="green">Green</option>
<option value="blue">Blue</option>
</F>
<F name="firstName" component={CustomInputComponent} />
<F name="age">
{({ input, meta }) => (
<div>
<label>Age</label>
<input {...input} type="text" placeholder="Age" />
{meta.error && meta.touched && <span>{meta.error}</span>}
</div>
)}
</F>
那么表單元素 F 包含了什么?表單標(biāo)簽?表單域?錯(cuò)誤提示?每個(gè)庫(kù)對(duì)此有不同的理解。
formik
個(gè)人理解 formik 更傾向于 F 是一個(gè)純粹的表單域(Input, Select ...),F(xiàn)ield 的 component 字段默認(rèn)就是 input,認(rèn)為“完整”的 F 是 Fieldset,這個(gè)在 demo 中有體現(xiàn),當(dāng)然 API 也并沒有限制開發(fā)者自由發(fā)揮,F(xiàn)ield 支持 render props。
react-final-form
react-final-form 的 F 設(shè)計(jì)的比較開放,并沒有官方的說(shuō)法,一千個(gè)前端就有一千個(gè)哈姆雷特,F(xiàn) 是什么由開發(fā)者定義,提供了三個(gè)字段 component, render, children 供選擇。
uform
uform 的 F 是什么由開發(fā)者定義,API 設(shè)計(jì)得和前兩者不太一樣,F(xiàn) 默認(rèn)是一個(gè)表單域,開發(fā)者可以通過 registerFieldMiddleware 這個(gè) API 設(shè)置 F 的 Wrapper。
NoForm
NoForm 對(duì)于 F 有自己的理解,認(rèn)為 F 是一個(gè)完整的表象元素抽象,在文檔中有詳細(xì)描述。他給我的感受和前幾個(gè)不太一樣,他為開發(fā)者設(shè)計(jì)好了表單的一切,試圖給出一個(gè)最佳實(shí)踐,什么元素(表單標(biāo)簽、錯(cuò)誤提示、表單域前綴、后綴等)應(yīng)該放在哪里,對(duì)應(yīng)的你需要傳入哪個(gè)參數(shù)都幫你決策好了,當(dāng)然這樣的設(shè)計(jì)也犧牲掉了一部分靈活性,使用起來(lái)更像一個(gè)表單域的 Wrapper。
<F label="input" name="input">
<Input />
</F>
UI 組件適配
現(xiàn)在前端開發(fā)項(xiàng)目已經(jīng)離不開 UI 組件庫(kù)了,作為表單方案,如何接入組件庫(kù)也是一個(gè)關(guān)注點(diǎn)。
rc-form、formal
通過特定的 API 結(jié)合 Spread syntax 往 UI 組件傳遞參數(shù),主要是 value 和 onChange,不需要專門編寫對(duì)應(yīng)的適配 UI 組件,能夠快速接入。
import { Input } from 'antd';
{getFieldDecorator('name', {
rules: [{ required: true }],
//valuePropName: 'checked', // <Switch/>
})(
<Input />
)}
表單元素概念的設(shè)計(jì)通常還會(huì)有一層 “適配層” 亦或是 “接入層”,類似 Adapter / Wrapper 的概念,用于更快速的接入第三方 UI 組件庫(kù)。上層的 F 負(fù)責(zé)維護(hù) value,error、onChange 等等,適配層根據(jù)下層 UI 組件的要求傳遞這些屬性,下層的 UI 組件負(fù)責(zé)純展示。各個(gè)庫(kù)對(duì)適配層的設(shè)計(jì)也不盡相同。
formik
formik 的 Field 會(huì)傳遞兩個(gè)特定的參數(shù),分別是 field 和 form,前者主要包含 value, onChange, onBlur,后者包含 isSubmitting, touched, errors 等一些表單狀態(tài)及工具方法。因?yàn)榇蟛糠?UI 組件接收的是 value 和 onChange,所以需要專門編寫對(duì)應(yīng)的適配層。
import { Input } from 'antd';
const InputWrapper = ({ field: {name}, form: { touched, errors }, ...restProps }) => (
<div>
<Input {...field} {...restProps}/>
{touched[field.name] &&
errors[field.name] && <div className="error">{errors[field.name]}</div>}
</div>
)
react-final-form
react-final-form 和 formik 在 F 設(shè)計(jì)上雷同,提供了 input 和 meta,分別對(duì)應(yīng) formik 的 field 和 form,在字段上有些許差異,就不做多余的描述了,見官方demo:
https://codesandbox.io/s/40mr0v2r87
https://codesandbox.io/s/9ywq085k9w
uform
uform 提供了 registerFormField 用于注冊(cè)表單字段組件,registerFieldMiddleware 用于設(shè)置 wrapper。當(dāng)然作為阿里內(nèi)部的表單框架,自帶了自家的兩個(gè)UI庫(kù)適配層 @uform/antd, @uform/next,還是深度定制的。
import { Input } from 'antd'
// 最簡(jiǎn)版
registerFormField('testInput', Input)
registerFieldMiddleware(Field => {
return props => {
const { errors, schema } = props
// errors handle todo
return React.createElement(
'div',
{},
React.createElement(span, {}, schema.title),
React.createElement(Field, props)
)
}
})
<Field type="testInput" label="name" />// 應(yīng)用
值得一提的是這個(gè) string type 的設(shè)計(jì),寫表單的時(shí)候不需要從外部 import 組件,預(yù)先注冊(cè)好之后,F(xiàn)ield 組件就會(huì)幫助開發(fā)者匹配相對(duì)應(yīng)的組件,是一個(gè)能夠提升工作幸福感的設(shè)計(jì)。
NoForm
如上文提到 NoForm 的 FormItem 本身更像是個(gè)表單域的 Wrapper,而且 FormItem 在內(nèi)部會(huì)做一個(gè) cloneElement,將處理好的 props 傳遞給子組件,因此接入 UI 庫(kù)理論上甚至不需要編寫額外的適配層。但是出于對(duì)外提供一致接口的考慮,比如 Switch 的值是 checked,Input 是 value,還是需要有這個(gè) UI 適配層。當(dāng)然同作為阿里內(nèi)部的框架,也提供了兩個(gè)組件庫(kù)的適配層 nowrapper。
import { Input } from 'antd'
<FormItem label="input" name="input">
<Input />
</FormItem>
表單校驗(yàn)
表單校驗(yàn)是每個(gè)表單方案繞不過的一道坎,也是我們的重點(diǎn)關(guān)注點(diǎn)之一。通常會(huì)支持表單級(jí)、字段級(jí)的驗(yàn)證,常規(guī)功能在這里就不討論了,歸納總結(jié)了一些看到的特點(diǎn):
校驗(yàn)規(guī)則外置
通常的校驗(yàn)規(guī)則是與 F 一一對(duì)應(yīng),在 F 相關(guān)的 JSX 中描述,每個(gè) F 上會(huì)有類似 validate 的字段 或是 通過展開運(yùn)算符傳入,代表是 react-final-form, uform, rc-form, formal。
F name="email" validate={...}>
...
</F>
formik, NoForm, formal 支持校驗(yàn)規(guī)則外置,這樣的設(shè)計(jì)應(yīng)該是基于“關(guān)注點(diǎn)分離”的原則,JSX 用于組織 UI,校驗(yàn)規(guī)則并不是 UI 的固有屬性應(yīng)該分離出來(lái),從而進(jìn)一步降低耦合性,目的是幫助我們寫出清晰、易維護(hù)的代碼。
formik 官方推薦使用 Yup(一個(gè)功能強(qiáng)大的規(guī)則校驗(yàn)工具,鏈?zhǔn)斤L(fēng)格的 API 設(shè)計(jì)),并針對(duì) Yup 做了優(yōu)化,提供 validationSchema 屬性方便接入。
NoForm 提供了 validateConfig 用于外置校驗(yàn)規(guī)則,需按規(guī)則傳入 JSON 對(duì)象,用 async-validator 作為校驗(yàn)工具。
const validateRules = {
email: string()
.email('Invalid email')
.required('Required'),
// ...
}
<Form validateRules={validateRules}>
<F name="email">
...
</F>
</Form>
動(dòng)態(tài)校驗(yàn)
有一類場(chǎng)景例如注冊(cè)需要輸入的兩次密碼一致。還有一類場(chǎng)景是表單有聯(lián)動(dòng),選擇了 B 后,C 需要必填。我把這些稱為 動(dòng)態(tài)校驗(yàn),表單校驗(yàn)會(huì)依賴用戶的輸入 或是 表單聯(lián)動(dòng)帶來(lái)的校驗(yàn)規(guī)則的改變。
formik 推薦的 Yup 提供了 ref 獲取其他字段的引用。
let schema = object({
baz: ref('foo.bar'),
foo: object({
bar: string(),
}),
x: ref('$x'),
});
schema.cast({ foo: { bar: 'boom' } }, { context: { x: 5 } });
// => { baz: 'boom', x: 5, foo: { bar: 'boom' } }
NoForm 的 validateConfig 支持動(dòng)態(tài)配置。
const validateConfig = {
username: {type: "string", required: true},
age: (values, context) => { // dynamic validate config
const { username } = values;
return {type: "string", required: !!username };
}
}
其他方案都提供了類似 values 的字段供回調(diào)函數(shù)拿到所有字段值用于處理邏輯,例如 react-final-form validate 的 allValues, uform 的 x-rules,rc-form validateFields 的 values 等等。除此之外 uform 還引入了 effects 的概念來(lái)解決聯(lián)動(dòng)問題。
處理聯(lián)動(dòng)
數(shù)據(jù)聯(lián)動(dòng),歸根結(jié)底是字段間的相互依賴關(guān)系,同時(shí)附加了依賴動(dòng)作,同時(shí)依賴動(dòng)作的執(zhí)行是存在時(shí)序的。
聯(lián)動(dòng)在表單中比較常見,比較理想的是框架能夠約束開發(fā)者優(yōu)雅的去處理聯(lián)動(dòng),拒絕面條式代碼。最常見的是表單項(xiàng)的顯示隱藏,NoForm 的 If、react-final-form 的 demo 都提供了類似 jsx-control-statements 的思路,更像是 JSX 函數(shù)表達(dá)式的語(yǔ)法糖。
// 偽代碼
<F label="showA" name="showA">
<CheckBox />
</F>
<If condition={ showA === true }>
<F label="A" name="A">
<Input />
</F>
</If>
其他更復(fù)雜的聯(lián)動(dòng)場(chǎng)景只有 uform 交出了自己的答卷,在其 文檔 中有較為詳細(xì)的羅列及其解決方案,當(dāng)然解決問題的同時(shí)也引入了很多其他概念。
數(shù)組、嵌套表單
表單嵌套、數(shù)組字段這兩類場(chǎng)景在日常開發(fā)中也經(jīng)常遇到,表單方案對(duì)此也有不同的設(shè)計(jì),試圖幫助開發(fā)者更優(yōu)雅的處理此類場(chǎng)景。
字段的嵌套結(jié)構(gòu)
rc-form、react-final-form、 formik 的字段名支持點(diǎn)括號(hào)語(yǔ)法,即支持以嵌套結(jié)構(gòu)定義字段名,例如 object.a.b、array[2] 等。
<F name="object.a">
...
</F>
uform、 NoForm 則是根據(jù)節(jié)點(diǎn)的父子關(guān)系來(lái)定義字段的結(jié)構(gòu)。
<F name="object">
<F name="a">
...
</F>
</F>
數(shù)組類型的字段
此類場(chǎng)景一般還會(huì)伴隨著表單項(xiàng)的動(dòng)態(tài)增刪,因此 react-final-form、 formik 除了支持點(diǎn)括號(hào)外還提供了工具類 FieldArray,提升開發(fā)體驗(yàn)。具體見 formik FieldArray demo、react-final-form FieldArray demo。
而 uform 提供了 createArrayField,NoForm 則提供了 repeater。
性能開銷
性能是每個(gè)框架繞不開的話題,在開始之前,我有這樣的一個(gè)思考:表單的性能問題遇到的多嘛?
結(jié)合自己的工作經(jīng)驗(yàn),個(gè)人認(rèn)為表單性能問題通常情況下不會(huì)遇到,個(gè)別情況下會(huì)存在,例如表單嵌套,大型項(xiàng)目的配置頁(yè)等場(chǎng)景。排除特殊場(chǎng)景,一個(gè)頁(yè)面中包含大量表單項(xiàng)本身就是不合理的,這樣的設(shè)計(jì)會(huì)影響用戶體驗(yàn)(腦補(bǔ)畫面:用戶正在操作滿屏幕的表單),所以在問題成為問題之前可能不需要傾注太多精力,比較好的策略是出現(xiàn)問題解決問題,當(dāng)然有足夠的精力能夠提前知曉/解決問題也是好的。
回過頭來(lái)在做技術(shù)調(diào)研時(shí)還是需要有一個(gè)全面的考量,性能開銷的關(guān)注點(diǎn)主要在于單個(gè)表單項(xiàng)的改動(dòng)是否會(huì)引起整個(gè)表單重新渲染,即觀察用戶在某些字段中輸入值時(shí),其他無(wú)關(guān)聯(lián)的表單項(xiàng)會(huì)不會(huì) rerender。
經(jīng)測(cè)試 react-final-form 和 uform 做到了這點(diǎn),在內(nèi)部實(shí)現(xiàn)上均使用了發(fā)布訂閱,每個(gè)表單項(xiàng)只會(huì)訂閱和自己相關(guān)的改動(dòng),實(shí)現(xiàn)單個(gè)改動(dòng)不影響全量。其他表單方案的狀態(tài)管理至上而下,均會(huì)造成整個(gè)表單重新渲染,formik 因此提供了 FastField 用于改進(jìn)其性能,內(nèi)置 shouldComponentUpdate 阻止不必要的渲染。
序列化
表單的序列化常見于 動(dòng)態(tài)表單需求 或是 可視化搭建系統(tǒng)上,這類序列化場(chǎng)景主要看業(yè)務(wù)需求,通常會(huì)約定一個(gè) JSON DSL 定義數(shù)據(jù)結(jié)構(gòu)用于描述表單。
這類需求一般比較偏業(yè)務(wù),通用的可能并不好用,所以框架上能做的并不多,有個(gè)別框架提供了自己的方案,比較有名的是 react-jsonschema-form, uform 也有自己的理解并提供了 Form Schema。
其他關(guān)注點(diǎn)
· less magic,這是一個(gè)加分項(xiàng),內(nèi)部實(shí)現(xiàn)的越簡(jiǎn)單意味著潛在的 bug 越少,調(diào)試更簡(jiǎn)單,黑魔法是一把雙刃劍。
· 確保開源項(xiàng)目的可維護(hù)性,即作者有沒有充足的熱情持續(xù)維護(hù)下去,可以觀察遺留的 issue 數(shù)量、社區(qū)討論、pr 跟進(jìn)情況等等。
小結(jié)
框架的內(nèi)部實(shí)現(xiàn)方式有很多,設(shè)計(jì)的選擇和權(quán)衡也不同,目標(biāo)也不同,不能一概而論。比如在我看來(lái),uform、 NoForm 想做的是一個(gè)開箱即用的方案,我什么都給你做好了,UI適配層、聯(lián)動(dòng)方案、校驗(yàn)啥都有,直接用就好;而其他方案都做的比較精簡(jiǎn),只提供基礎(chǔ)通用的部分,其他的交給開發(fā)者自行選擇設(shè)計(jì),帶來(lái)的好處是約束少可發(fā)揮空間大。
開班時(shí)間:2021-04-12(深圳)
開班盛況開班時(shí)間:2021-05-17(北京)
開班盛況開班時(shí)間:2021-03-22(杭州)
開班盛況開班時(shí)間:2021-04-26(北京)
開班盛況開班時(shí)間:2021-05-10(北京)
開班盛況開班時(shí)間:2021-02-22(北京)
開班盛況開班時(shí)間:2021-07-12(北京)
預(yù)約報(bào)名開班時(shí)間:2020-09-21(上海)
開班盛況開班時(shí)間:2021-07-12(北京)
預(yù)約報(bào)名開班時(shí)間:2019-07-22(北京)
開班盛況
Copyright 2011-2023 北京千鋒互聯(lián)科技有限公司 .All Right
京ICP備12003911號(hào)-5
京公網(wǎng)安備 11010802035720號(hào)