用 Babel JSX 擴展來創造響應式 Ant Design 表單解決方案

你們好,兩年前我曾經發布過一篇文章《使用新一代js模板引擎NornJ提高React.js開發體驗》,第一次嘗試推廣我創做的可擴展模板引擎 NornJ 。當時的 NornJ 徹底基於字符串模板,在一些人看來它與 React JSX 環境彷佛自然不匹配,上手晦澀一時難以看出優點。html

Babel JSX 插件的新思路

在發表那篇文章後不到一週的時間,我仔細參考了jsx-control-statements,不自覺萌生出一個新的想法:vue

使用 Babel 提取含特殊信息的 JSX 標籤,把它們轉換爲需運行時的渲染函數,是否能突破 JSX 現有的語法擴展能力?react

這個想法隨後就被實施:babel-plugin-nornj-in-jsx,並繼續應用於公司部門內的多個實際項目中。Babel轉換原理描述,請看這裏ios

NornJ 下一代版本

有了上面的轉換思路,並在繁忙業務中通過兩年斷斷續續迭代,我在今年發佈了從新設計後使用 JSX API 的 NornJ 正式版,並重寫了文檔,源碼也用 Typescript 幾乎徹底重寫:git

github:github.com/joe-sky/nor…github

文檔(gitee.io):joe-sky.gitee.io/nornjnpm

文檔(github.io):joe-sky.github.io/nornjjson

基於 Mobx 與 JSX 語法擴展的 Ant Design 表單解決方案

咱們部門團隊自2016年起一直主力使用 Mobx 做爲 React 狀態管理方案,幾年來咱們一直受益於它的響應式數據流開發體驗十分高效,也很容易優化。axios

Mobx 適配 Antd 表單可能存在的痛點

雖然關於 Mobx 與 Redux 等誰更優不是本篇文章裏要對比的,可是經過幾年的使用經驗,我總結出 Mobx 在配合國內最流行的 React 組件庫 Ant Design 組件,特別是表單驗證組件時可能存在的一些開發痛點:後端

  • Ant Design Form 組件推薦的數據存取方式,沒法很順暢地與 Mobx 的響應式數據流結合

Antd Form 組件原生方式使用 getFieldsValue 和 setFieldsValue (官方文檔)來對數據進行存取,這在使用 Mobx 作數據流管理時會遇到一些比較尷尬的場景:

  1. 請求後端接口把返回的表單字段數據存儲在了 Mobx observable 數據中,而後咱們須要把這些數據用 setFieldsValue 方法放置到 Form 組件實例內,各表單組件數據會更新。但這個數據更新的過程沒有用上 observable 響應式特性,感受對使用 Mobx 來講有點浪費;

  2. 從 Form 組件中獲取表單字段值時要用 getFieldsValue。這樣取出來直接在 render (或 Mobx computed)中使用時,Mobx 的 observer 不會自動重渲染(重計算),可能與直覺不符:

const Demo = () => {
  const [form] = Form.useForm();

  return useObserver(() => (
    <div>
      <Form form={form} name="control-hooks">
        <Form.Item name="note" label="Note" rules={[{ required: true }]}>
          <Input />
        </Form.Item>
        <Form.Item name="gender" label="Gender" rules={[{ required: true }]}>
          <Select placeholder="Select a option and change input text above" allowClear>
            <Option value="male">male</Option>
            <Option value="female">female</Option>
            <Option value="other">other</Option>
          </Select>
        </Form.Item>
      </Form>
      //表單值更新時,如下文字不會更新
      <i>Note:{form.getFieldValue('note')}</i>
      <i>Gender:{form.getFieldValue('gender')}</i>
    </div>
  ));
};
複製代碼

固然,上述場景是有辦法解決的。可是不管怎樣解決,咱們都會感受到有兩份數據存在:Mobx 狀態的數據、以及表單本身的數據。對適應了 Mobx 響應式數據流的開發人員來講,可能會以爲麻煩。

  • 部分 Mobx 的 observable 數據,在傳入 Ant Design Form 表單組件時須要執行 toJS 轉換

這多是 Mobx observable 這種包裝數據類型的硬傷,但像 CheckBox.Group 組件這種,每次傳入組件的值都手工執行一次 toJS 轉換值爲普通數組,也確實有點麻煩。

尋找 Mobx 環境的表單方案 - mobx-react-form

咱們能夠找到現有的解決方案:mobx-react-form

它與 Antd Form 基於組件內管理數據的思路是不同的。mobx-react-form 把表單數據、驗證狀態等都交給一個含 Mobx observable 成員的特殊結構實例來管理,再經過 JSX 延展操做符 API 通知到 Form 相關組件。一個簡單的例子:

import React from 'react';
import { observer } from 'mobx-react';
import MobxReactForm from 'mobx-react-form';

const fields = [{
  name: 'email',
  label: 'Email',
  placeholder: 'Insert Email',
  rules: 'required|email|string|between:5,25',
}, {
  name: 'password',
  label: 'Password',
  placeholder: 'Insert Password',
  rules: 'required|string|between:5,25',
}];
const myForm = new MobxReactForm({ fields });

export default observer(({ myForm }) => (
  <form onSubmit={myForm.onSubmit}> <label htmlFor={myForm.$('email').id}> {myForm.$('email').label} </label> <input {...myForm.$('email').bind()} /> <p>{myForm.$('email').error}</p> <button type="submit" onClick={myForm.onSubmit}>Submit</button> <button type="button" onClick={myForm.onClear}>Clear</button> <button type="button" onClick={myForm.onReset}>Reset</button> <p>{myForm.error}</p> </form> )); 複製代碼

mobx-react-form 的數據管理思路無疑是更符合 Mobx 響應式數據流的。雖然官方沒給例子,但它在加一些擴展後應也可適配 Antd Form 組件。但咱們從上面代碼不難看出,mobx-react-form 和 Antd Form 原生方式比,可能還有如下幾個讓人顧慮的方面:

  • 用 json 方式定義各表單字段屬性,不及 Antd 的 JSX 語法更符合 React 環境的特點;
  • 用 JSX 延展操做符通知各表單組件,語法可讀性可能不是太好;
  • 它的底層驗證組件,並無提供 Antd 採用的 async-validator。

基於 JSX 擴展的表單方案 - mobxFormData

參考了 mobx-react-form 的數據管理思路,我利用 NornJ 現有的 JSX 擴展能力,開發出了基於 async-validator 的解決方案:mobxFormData ,同時支持Antd v3 & v4,性能也不錯。詳細文檔在這裏

Codesandbox 示例(若是一次沒法運行,多刷新幾回就好)

使用方式很簡單,安裝 preset:

npm install babel-preset-nornj-with-antd
複製代碼

再配一下 Babel:

{
  "presets": [
    ...,
    "nornj-with-antd"  //一般放在全部 preset 的最後面
  ]
}
複製代碼

而後就能夠在 JSX/TSX 內直接使用了:

import React from 'react';
import { Form, Input, Button, Checkbox } from 'antd';
import { useLocalStore, useObserver } from 'mobx-react-lite';
import 'nornj-react';

export default props => {
  const { formData } = useLocalStore(() => (
    <mobxFormData>
      <mobxFieldData name="userName" required message="Please input your username!" />
      <mobxFieldData name="password" required message="Please input your password!" />
      <mobxFieldData name="remember" />
    </mobxFormData>
  ));
  
  return useObserver(() => (
    <Form>
      <Form.Item mobxField={formData.userName} label="Username">
        <Input />
      </Form.Item>
      <Form.Item mobxField={formData.password} label="Password">
        <Input.Password />
      </Form.Item>
      <Form.Item mobxField={formData.remember}>
        <Checkbox>Remember me</Checkbox>
      </Form.Item>
    </Form>
  ));
};
複製代碼

如上,此方案的表單字段數據放在 <mobxFormData> 標籤返回的 formData 實例中。與 mobx-react-form 思路相似,formData 是一個扁平化的 Mobx observable 數據類型,上面包含了各表單數據字段、以及各類表單數據操做 API,使用起來很是方便,能夠很好地與 Mobx 數據流對接:

export default props => {
  const { formData } = useLocalStore(() => (
    <mobxFormData>
      <mobxFieldData name="userName" required message="Please input your username!" />
      <mobxFieldData name="password" required message="Please input your password!" />
    </mobxFormData>
  ));
  
  useEffect(() => {
    axios.get('/user', { params: { ID: 12345 } })
    .then(function (response) {
      const user = response.data;
      formData.userName = user.userName;
      formData.password = user.password;
    });
  }, []);
  
  //表單數據操做 api 都在 formData 實例上,能夠把實例傳遞給其餘組件
  const onSubmit = () =>
    formData
      .validate()
      .then(values => {
        console.log(values);
      })
      .catch(errorInfo => {
        console.log(errorInfo);
      });
  
  return useObserver(() => (
    <div>
      <Form>
        <Form.Item mobxField={formData.userName} label="Username">
          <Input />
        </Form.Item>
        <Form.Item mobxField={formData.password} label="Password">
          <Input.Password />
        </Form.Item>
        <Form.Item>
          <Button type="primary" onClick={onSubmit}>
            Submit
          </Button>
        </Form.Item>
      </Form>
      //表單值更新時,如下文字會實時更新
      <i>Username:{formData.userName}</i>
      <i>Password:{formData.password}</i>
    </div>
  ));
};
複製代碼
  • 這裏用到的 mobxFormData 是一種 JSX 擴展:標籤,它被 Babel 轉換後的實際值並非 React.createElement 方法,而只是返回了特殊的對象結構,供 Mobx 轉換爲 observable 類型,轉換原理請看這裏

  • 而 mobxField 是另外一種 JSX 擴展:指令,使用它將 formData 實例與 Form.Item 組件創建雙向數據綁定。在 mobxField 指令的底層實現中,經過配置對不一樣的 Antd 表單元素組件選取了特定的值屬性、事件屬性等進行自動更新,而且已經在該轉換時調用過 Mobx 的 toJS 方法了,無需再手工 toJS。

mobxFormData 方案的語法總體看起來,和 React JSX 環境感受也比較契合,IDE 語法提示也是完整的。除了語法,它的各方面功能其實也挺全面,Antd 原生 Form 能實現的它也幾乎都能實現。具體能夠看它的文檔和示例

mobxFormData 的各類表單示例文檔

爲了更好地服務於開發者,mobxFormData 方案按照 antd v4 版官方文檔,重寫了其中10多個可運行示例文檔,並使用 Dumi 部署在 NornJ 的文檔站點中:mobxFormData 表單示例文檔

你們能夠拿它和 antd 官方表單示例文檔 作下對比,其實能夠看出在一樣功能的狀況下,mobxFormData 的代碼量一般會更少一些。

mobxFormData 能用於生產環境嗎

mobxFormData 方案在我司大部門內已有多個線上實際項目在用,因此我以爲若是您認爲它對您的開發體驗有好處,或有興趣嘗試,則能夠用於生產環境。做者也會一直堅持更新這個項目,若是發現問題很是歡迎您的反饋。

關於 JSX 擴展,一些做者的經驗

最後,依做者的實踐經驗,總結出一些做者認爲的目前 JSX 擴展方案可行經驗,在此分享給你們:

經驗一:JSX 擴展其實能支持 IDE 代碼提示

在一些文章評論中,我記得不僅一次看到過有人提過: Babel 作的 JSX 擴展是否會沒法與現有的 Eslint 與 IDE 語法提示環境融合。這裏能夠給出一個結論:JSX 擴展其實絕大多數均可以支持 IDE 語法提示

而方法就是使用 Typescript,只要掌握一些 TS 重寫類型的知識便可,定義在 global.d.ts 內。例如:

const Test = () => <customDiv id="test">test</customDiv>
複製代碼

爲上面的 customDiv 標籤補上 TS 類型,只要這樣:

interface ICustomDiv {
  id: string;
}

declare namespace JSX {
  interface IntrinsicElements {
    /** * customDiv tag */
    customDiv: ICustomDiv;
  }
}
複製代碼

指令的話,例如:

const Test = () => <div customId="test">test</div>
複製代碼

TS 這樣寫就能夠:

declare namespace JSX {
  interface IntrinsicAttributes {
    /** * customId directive */
    customId?: string;  //由於每一個組件均可能用到,爲不影響類型檢查,因此定義爲可選的
  }
}
複製代碼

NornJ 項目全部的預置 JSX 擴展都是這樣來定義類型,代碼能夠看這裏。Eslint 的話,若是 TS 類型定義好了它一般不會受影響,但可能用到未使用的變量等,這時也不難處理簡單加個配置就好,配置方法能夠看這裏

經驗二:React 用雙向數據綁定的場景其實不等於用指令語法

還有些觀點以爲 「雙向綁定」 這個概念,彷佛在 React 環境中出現會是一種不合時宜的場景。

雙向綁定的含義理解起來是視圖組件和數據模型之間創建的綁定關係,它們會雙向同步更新。這種場景 React 中也可能會存在,像 Antd 的 Form 組件,從早期版本直到最新的 V4 版,在我看來它的數據管理方式其實一直都相似於雙向數據綁定,但並無用指令方式 API 實現。從它的官方文檔中,也一直能夠看到對雙向綁定的描述

對於指令的實現,不一樣的 Babel JSX 擴展項目的實現也不一樣,大多數是語法糖轉換;也有比較特殊的,好比 NornJ 的mobxBind 指令,它的實現實際上是一個React 高階組件。因此說 API 只是形式,並不必定表明底層實現。

經驗三:目前有哪些現存的 JSX 語法擴展方案

這個領域確實比較偏,如下是做者這些年來見過的幾個 Babel JSX 擴展項目,它們都提供了流程控制等常見 JSX 擴展:

目前可擴展的 Babel JSX 插件除了做者本身開發的 NornJ:

暫時未找到其餘現有的能讓開發者擴展的,若是有朋友知道的話能夠告訴我,感謝😃

相關文章
相關標籤/搜索