天天看點

Fusion Next 之 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" />
      password: <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 。(以上代碼可直接運作,可以在 

https://codepen.io/frankqian/pen/XOROBw?editors=0010

 調試)

其實這裡和架構并沒有什麼關系,因為不管用什麼架構要想做到 指派和校驗 這兩個功能,基本一定要在 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 體系之間的關系。

Fusion Next 之 Form 元件的設計之路

使用  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 在這裡 

https://codepen.io/frankqian/pen/xMdoxZ?editors=0010

 可以自己調試

表單校驗

既然能夠擷取到資料了,那邊表單校驗是順手的事情,因為校驗隻依賴資料。我們隻需要對集中固定的互動性形式和校驗規則做抽象就好了。

互動形式上大概包含以下三類

  • 輸入的時候實時校驗,一般 onChange 觸發
  • 離開焦點的時候校驗,一般 onBlur 觸發
  • 通過自定義的操作來觸發校驗,自己調用 api 觸發

常見的校驗規則抽象

規則名稱 描述 類型 觸發條件/資料類型
required 不能為空 Boolean

undefined/null/“”/[]

pattern 校驗正規表達式 正則
minLength 字元串最小長度 / 數組最小個數 Number String/Number/Array
maxLength 字元串最大長度 / 數組最大個數
length 字元串精确長度 / 數組精确個數
min 最小值 String/Number
max 最大值
format

對常用 pattern 的總結

url/email/tel/number

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見: 

https://codepen.io/frankqian/pen/vbZmXE?editors=0010
Fusion Next 之 Form 元件的設計之路

自己寫的表單元件怎麼用

現在很多React 元件是在原生元件之上又做了封裝,還有很多元件可能并沒有包裹表單元素(比如 

Fusion Select

裡面并沒有 select 元素,下拉框是自己做的 )。但是隻要你自己寫的元件也遵循表單的規則就可以使用該方案。

基本規則:  value + onChange 受控規則

這個規則其實來自原生 html 的元件,我們自己寫的元件隻要按照标準來都可以使用 Field。

Fusion Next 之 Form 元件的設計之路

自己寫的元件比起原生的表單元件會更加美觀,互動更友好。隻要遵循規範都能在 field 裡面使用,詳細demo 見 

https://codepen.io/frankqian/pen/gqRWJx?editors=0010

更人性化的功能

還有一些其他更加細粒度的規則,是為了讓你的元件更加好的适配進階功能,比如:

  • 一鍵 reset 清空所有資料。因為每個元件的接收資料類型不一樣,是以統一為在 willReceiveProps 裡面接收 value=undefined
componentWillReceiveProps(nextProps) {
    if ('value' in nextProps ) {
        this.setState({
           value: nextProps.value === undefined? []: nextProps.value   //  設定元件的被清空後的數值
        })
    }
}           
  • 一次互動操作隻抛一次 onChange

- 比如 upload 上傳,如果一次上傳觸發上百次 onChange,那麼整個頁面會跟着一起 Render 幾百次,非常影響性能

Fusion Next 之 Form 元件的設計之路

- 比如 Slider, 在拖動的時候如果實時觸發 onChange,那麼在拖動滑塊的時候可能會非常卡頓。是以滑鼠松開的那個瞬間觸發才是比較合理的操作,其他的拖拽事件可以交給 onProgress

Fusion Next 之 Form 元件的設計之路

Fusion Next 的表單元件基本都已經是按照這套規範标準實作了,詳細可以檢視這裡的文檔 

https://fusion.alibaba-inc.com/component/field

 拉到最下面

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>           
Fusion Next 之 Form 元件的設計之路
    • 水準布局 
<Form inline>...</Form>      
Fusion Next 之 Form 元件的設計之路
    • 标簽内置
<Form labelAligin="inset">...</Form>      
Fusion Next 之 Form 元件的設計之路

輔助錯誤展示

出錯的時候自動展示錯誤資訊,不需要自己 getError 判斷。 每種狀态怎麼展現由各自的元件自己實作。減少和Form的耦合

Fusion Next 之 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>
  }
}           

上面代碼中可以看出幾個優化點:

  1. 不需要關注 Field 用法,改成 Form API 的方式。用法簡單直接不少
  2. 通過 name 來進行資料初始化,也更加接近原生 form 的用法,大家更容易了解。
  3. 校驗功能 API 化,代碼更加簡潔,可讀性增強

後記

Form 的優化一定不會僅僅止于此,因為在實際業務中會遇到更加複雜的功能。

很多業務為了更加友善快捷,會抽象常用的元件布局,通過後端接口吐出JSON schema的方式直接在前端動态展示表單,雖然比較業務化當時确實友善快捷,能夠極大的解決效率問題;

又或者把常用的表單類場景做成業務元件、子產品模闆,在使用的時候直接下載下傳使用。比如:Fusion的表單類子產品:

https://fusion.alibaba-inc.com/module?category=表單

方案很多,總有适合自己的一套。

相關連結

繼續閱讀