Form 表單元件的設計之路
前端的Form 表單主要用於解決資料獲取、資料校驗、資料賦值 這三大類問題。這篇文章裡面的提供的解決方案能夠比較完美的用在 React 框架上,但是解決問題的思路相信應該是可以使用於任何框架語言。
中後臺的表單元件已經不僅僅有 input 和 select,可能還擴充套件到 範圍選擇器、日期選擇器 等,這些元件往往為了實現更優雅的UI和更使用的互動會在原生的元件上面做多層封裝,而經過多層疊加後可能已經看不到原生表單元素的影子了。比如經過封裝下面這段 DOM 結構經過樣式修改也可能成為一個輸入元件,雖然完全看不到 input 的影子。
<span> <span contentEditable></span> </span>複製程式碼
所以為了便於大家理解我這裡從傳統的原生 form 說起,好讓大家有一個遞進的過程。
引子:原生 form 表單
最初始的一份程式碼如下, 程式碼很簡單,看著也很舒服 。
<form action="/api/post" method="post"> username: <input name="username" /> passowrd: <input name="password" /> <button type="submit">submit</button> </form>複製程式碼
但是你開始做資料校驗相關,表單就立刻變得複雜多了。如下:程式碼增多了一倍。
<script> function checkname(target) { const value = target.value; if (value.length < 10) { document.getElementById('username_msg').innerHTML = '長度必須>10' } else { document.getElementById('username_msg').innerHTML = '' } } function checkpassword(target) { const value = target.value; if (!value.match(/^[\w]{6,16}$/)) { document.getElementById('password_msg').innerHTML = '密碼必須 6-16 位字母數字' } else { document.getElementById('password_msg').innerHTML = '' } } function getInitData() { ajax({ url:'/api/data', success:function(data) { document.getElementById('username') = data.username; }); } getInitData(); </script> <form action="/api/post" method="post"> username: <input name="username" onchange="checkname(this)"/> <span id="username_msg"></span> passowrd: <input name="password" onchange="checkpassword(this)"/> <span id="password_msg"></span> <button type="submit">submit</button> </form>複製程式碼
如果把DOM的部分也用JS來實現,基本可以做到只修改JS不需要再動DOM結構,但是也讓JS的複雜度增高不少。
React 裡面所有的DOM結構都是自己通過JS 生成的,JSX也可以方便的實現DOM結構。但這裡我拿原生表單舉例,只是想說用 React 寫出來的原生表單,並不比用原生 js 的優雅多少!!!
React 中的原生 form 表單
同樣一段最簡單的功能,套在 react 框架下面是這個樣子。
class Demo extends React.Component { render() { return <form action="/api/post" method="post"> username: <input name="username" /> passowrd: <input name="password" /> <button type="submit">submit</button> </form> } }複製程式碼
比如同樣想要實現校驗輸入自動 校驗 和 賦值 ,看下面一段程式碼,想想就是一大堆事情要做。
class Demo extends React.Component { state = { username: '', password: '', usernameMsg: '', passwordMsg: '', }; checkname = e => { // 獲取資料 const value = e.target.value; // 受控模式賦值 this.setState({ username: value, }); // 校驗資料 if (value.length < 10) { this.setState({ usernameMsg: '長度必須>10', }); } else { this.setState({ usernameMsg: '', }); } }; checkpassword = e => { // 獲取資料 const value = e.target.value; // 受控模式賦值 this.setState({ password: value, }); // 校驗資料 if (!value.match(/^[\w]{6,16}$/)) { this.setState({ passwordMsg: '密碼必須 6-16 位字母數字', }); } else { this.setState({ passwordMsg: '', }); } }; handleSubmit = () => { ajax({ url: '/api/post', data: { username: this.state.username, password: this.state.password, }, success: () => { // success }, }); }; render() { // 獲取資料和錯誤資訊 const { username, password, usernameMsg, passwordMsg } = this.state; return ( <form action="/api/post" method="post"> username: <input value={username} onChange={this.checkname} /> <span>{usernameMsg}</span> passowrd: <input value={password} onChange={this.checkpassword} /> <span>{passwordMsg}</span> <button type="submit" onClick={this.handleSubmit}> submit </button> </form> ); } }複製程式碼
程式碼有點長,但是基本可以總結出一個現象,要想實現表單資料獲取、校驗,基本離不開 onChange 這個方法,而且是有幾個表單控制元件,就要寫幾個 onChange 。(以上程式碼可直接執行,可以在 codepen.io/frankqian/p… 除錯)
其實這裡和框架並沒有什麼關係,因為不管用什麼框架要想做到 賦值 和 校驗 這兩個功能,基本一定要在 input 上面繫結 onChange。 所以如果有個通用的工具可以自動幫你把這些onChange的繫結都做了,再把校驗規則固定下,是不是所有的表單問題都可以解決了呢?是的通用表單解決方案就是按照這種思路設計出來的!
適用於所有 React 表單元件的解決方案
所有的用 React 寫成的元件都可以使用該方案。甚至 非 React 體系也可以使用改思路來解決問題。
基於所有表單控制元件都需要繫結 onChange 做資料獲取和校驗的原則,所以我設計了一個 Field 工具。這個工具原理很簡單,就是可以自動幫你繫結 value + onChange 解決上面一長串程式碼的問題。
const field = new Field(this); field.init('username');複製程式碼
field.init 會自動返回 value + onChange ,內容如下:
{ value: "", onChange: ƒ () }複製程式碼
下面這張圖簡單表面 Field 和 React 體系之間的關係。
使用 Field 獲取資料
import {Field} from '@alifd/next'; class Demo extends React.Component { field = new Field(this); handleSubmit = () => { console.log(this.field.getValues()); // 獲取資料 } render() { const {init} = this.field; return <form> username: <input {...init('username')} /> passowrd: <input {...init('password')} /> <button onClick={this.handleSubmit} >submit</button> </form> } }複製程式碼
這樣一個表單的資料獲取問題就解決了,程式碼簡潔了很多。 Demo 在這裡 codepen.io/frankqian/p… 可以自己除錯
表單校驗
既然能夠獲取到資料了,那邊表單校驗是順手的事情,因為校驗只依賴資料。我們只需要對集中固定的互動性形式和校驗規則做抽象就好了。
互動形式上大概包含以下三類
- 輸入的時候實時校驗,一般 onChange 觸發
- 離開焦點的時候校驗,一般 onBlur 觸發
- 通過自定義的操作來觸發校驗,自己呼叫 api 觸發
常見的校驗規則抽象
規則名稱 |
描述 |
型別 |
觸發條件/資料型別 |
required | 不能為空 | Boolean | undefined/null/“”/[] |
pattern | 校驗正則表示式 | 正則 | |
minLength | 字串最小長度 / 陣列最小個數 | Number | String/Number/Array |
maxLength | 字串最大長度 / 陣列最大個數 | Number | String/Number/Array |
length | 字串精確長度 / 陣列精確個數 | Number | String/Number/Array |
min | 最小值 | Number | String/Number |
max | 最大值 | Number | String/Number |
format | 對常用 pattern 的總結 url/email/tel/number |
String | String |
validator | 自定義校驗 | Function |
這裡說明下表單是弱型別的資料。比如 input 框裡面你希望使用者輸入的是整數,返回的 value 型別可能有兩種
- "123456", String 型別的整數校驗方式為 :/\d+/
- 123456, Number 型別的整數校驗方式為: typeof Value === 'number'
這個時候要求使用者一定要返回 Number 型別才能校驗非常不友好,所以在 Field 校驗邏輯裡面就把型別的問題處理掉了,而不是交給使用者去判斷。
上面是小插曲,我們繼續看如下 Field + 表單的程式碼,解決了資料獲取、表單校驗的所有功能
import { Field } from '@alifd/next'; class Demo extends React.Component { field = new Field(this); handleSubmit = (e) => { e.preventDefault(); this.field.validate(); // 自定義校驗 console.log(this.field.getValues()); // 獲取資料 } render() { const {init, getError} = this.field; return <form> username: <input {...init('username', {rules: { required: true, minLength: 10}})} /> <span style={{color: 'red'}}>{getError('username')}</span>{/**錯誤資訊**/} passowrd: <input {...init('password', {rules: { pattern: /^[\w]{6,16}$/, message: '密碼必須 6-16 位字母數字' }})} /> <span style={{color: 'red'}}>{getError('password')}</span>{/**錯誤資訊**/} <button onClick={this.handleSubmit} >validate</button> </form> } }複製程式碼
這樣之前可能需要 70 行的程式碼 24 行就可以解決了,可以讓程式碼清晰不少。除錯demo見: codepen.io/frankqian/p…
自己寫的表單元件怎麼用
現在很多React 元件是在原生元件之上又做了封裝,還有很多元件可能並沒有包裹表單元素(比如 Fusion Select 裡面並沒有 select 元素,下拉框是自己做的 )。但是隻要你自己寫的元件也遵循表單的規則就可以使用該方案。
基本規則: value + onChange 受控規則
這個規則其實來自原生 html 的元件,我們自己寫的元件只要按照標準來都可以使用 Field。
自己寫的元件比起原生的表單元件會更加美觀,互動更友好。只要遵循規範都能在 field 裡面使用,詳細demo 見 codepen.io/frankqian/p…
更人性化的功能
還有一些其他更加細粒度的規則,是為了讓你的元件更加好的適配高階功能,比如:
- 一鍵 reset 清空所有資料。因為每個元件的接收資料型別不一樣,所以統一為在 willReceiveProps 裡面接收 value=undefined
componentWillReceiveProps(nextProps) { if ('value' in nextProps ) { this.setState({ value: nextProps.value === undefined? []: nextProps.value//設定元件的被清空後的數值 }) } }複製程式碼
- 一次互動操作只拋一次 onChange
- 比如 upload 上傳,如果一次上傳觸發上百次 onChange,那麼整個頁面會跟著一起 Render 幾百次,非常影響效能
- 比如 Slider, 在拖動的時候如果實時觸發 onChange,那麼在拖動滑塊的時候可能會非常卡頓。所以滑鼠鬆開的那個瞬間觸發才是比較合理的操作,其他的拖拽事件可以交給 onProgress
Fusion Next 的表單元件基本都已經是按照這套規範標準實現了,詳細可以檢視這裡的文件 fusion.design/component/f… 拉到最下面
Form 元件讓體驗持續升級
上面知道了 Field 可以解決校驗、獲取、賦值等資料方面的問題,但是並不能解決 UI 和 互動的問題,在佈局和錯誤展示的時候需要自己來控制。
讓佈局更輕鬆
場景的佈局有水平 inline 佈局、垂直的分欄佈局,通過 FormItem 的 api 可以非常輕鬆的做到
- 垂直佈局
<Form> <FormItem label="Username:"> <Input name="first"placeholder="first"/> <Input name="second" placeholder="second"/> </FormItem> <FormItem label="Password:" required> <Input htmlType="password" name="pass" placeholder="Please enter your password!"/> </FormItem> <FormItem label=" "> <Form.Submit>Submit</Form.Submit> </FormItem> </Form>複製程式碼

<Form inline>...</Form>複製程式碼
- 標籤內建
<Form labelAligin="inset">...</Form>複製程式碼
輔助錯誤展示
出錯的時候自動展示錯誤資訊,不需要自己 getError 判斷。 每種狀態怎麼展現由各自的元件自己實現。減少和Form的耦合
每個元件的載入中、成功、失敗,都由元件自己實現,Form 只是在校驗的時候傳遞 state 給各個元件,這樣不需要 Form 去關心每個元件應該展現為什麼樣!
<Input state="error" />// 錯誤狀態 <Input state="loading" /> // 載入中 <Input state="success" /> // 成功 <DatePicker state="error" /> // 錯誤狀態複製程式碼
進一步優化 Form 讓使用更簡單
以上我們還是 Field + Form 配合來使用的,程式碼基本是這個樣子。
import { Form, Input, Field, Button } from '@alifd/next'; const FormItem = Form.Item; class Demo extends React.Component { field = new Field(this); handleSubmit = () => { this.field.validate(); } render() { const {init} = this.field; return<Form field={this.field}> <FormItem label="Username:"> <Input {...init('username', { rules: {required} })} /> </FormItem> <FormItem label="Password:"> <Input {...init('password', { rules: {pattern:/[\w]{6,16}/} })} htmlType="password" /> </FormItem> <FormItem label=" "> <Button onClick={this.handleSubmit} >Submit</Button> </FormItem> </Form> } }複製程式碼
可能寫多了之後就會想, 每個元件都要使用 init 、都需要寫 rules 規則 ,而且在 jsx 中寫一大串的 JSON 資料。
是否有方法讓資料獲取和校驗變得更簡單,讓程式碼再進一步的簡化呢?
進一步整合 Field 能力而弱化用法
針對以上問題對 Form 進一步優化,把 Field 的能力整合進了 Form,而把 Field 的用法進一步弱化,讓大家不需要再關心 init/取資料 等問題。程式碼如下:
import { Form, Input, Button } from '@alifd/next'; const FormItem = Form.Item; class Demo extends React.Component { handleSubmit = (values, errors) => { if (errors) { // 校驗出錯 return; } console.log(values) // 獲取資料 } render() { return<Form> <FormItem label="Username:" required> <Input name"username" /> </FormItem> <FormItem label="Password:" pattern={/[\w]{6,16}/}> <Input name="password" htmlType="password" /> </FormItem> <FormItem label=" "> <Form.Submit validate onClick={this.handleSubmit} >Submit</Form.Submit> </FormItem> </Form> } }複製程式碼
上面程式碼中可以看出幾個優化點:
- 不需要關注 Field 用法,改成 Form API 的方式。用法簡單直接不少
- 通過 name 來進行資料初始化,也更加接近原生 form 的用法,大家更容易理解。
- 校驗功能 API 化,程式碼更加簡潔,可讀性增強
後記
Form 的優化一定不會僅僅止於此,因為在實際業務中會遇到更加複雜的功能。
很多業務為了更加方便快捷,會抽象常用的元件佈局,通過後端介面吐出JSON schema的方式直接在前端動態展示表單,雖然比較業務化當時確實方便快捷,能夠極大的解決效率問題;
又或者把常用的表單類場景做成業務元件、模組模板,在使用的時候直接下載使用。比如:Fusion的表單類模組: fusion.design/module?cate…
方案很多,總有適合自己的一套。