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