你們好,兩年前我曾經發布過一篇文章《使用新一代js模板引擎NornJ提高React.js開發體驗》,第一次嘗試推廣我創做的可擴展模板引擎 NornJ 。當時的 NornJ 徹底基於字符串模板,在一些人看來它與 React JSX 環境彷佛自然不匹配,上手晦澀一時難以看出優點。html
在發表那篇文章後不到一週的時間,我仔細參考了jsx-control-statements,不自覺萌生出一個新的想法:vue
使用 Babel 提取含特殊信息的 JSX 標籤,把它們轉換爲需運行時的渲染函數,是否能突破 JSX 現有的語法擴展能力?react
這個想法隨後就被實施:babel-plugin-nornj-in-jsx,並繼續應用於公司部門內的多個實際項目中。Babel轉換原理描述,請看這裏。ios
有了上面的轉換思路,並在繁忙業務中通過兩年斷斷續續迭代,我在今年發佈了從新設計後使用 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
咱們部門團隊自2016年起一直主力使用 Mobx 做爲 React 狀態管理方案,幾年來咱們一直受益於它的響應式數據流開發體驗十分高效,也很容易優化。axios
雖然關於 Mobx 與 Redux 等誰更優不是本篇文章裏要對比的,可是經過幾年的使用經驗,我總結出 Mobx 在配合國內最流行的 React 組件庫 Ant Design 組件,特別是表單驗證組件時可能存在的一些開發痛點:後端
Antd Form 組件原生方式使用 getFieldsValue 和 setFieldsValue (官方文檔)來對數據進行存取,這在使用 Mobx 作數據流管理時會遇到一些比較尷尬的場景:
請求後端接口把返回的表單字段數據存儲在了 Mobx observable 數據中,而後咱們須要把這些數據用 setFieldsValue 方法放置到 Form 組件實例內,各表單組件數據會更新。但這個數據更新的過程沒有用上 observable 響應式特性,感受對使用 Mobx 來講有點浪費;
從 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 這種包裝數據類型的硬傷,但像 CheckBox.Group 組件這種,每次傳入組件的值都手工執行一次 toJS 轉換值爲普通數組,也確實有點麻煩。
咱們能夠找到現有的解決方案: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 原生方式比,可能還有如下幾個讓人顧慮的方面:
參考了 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 方案按照 antd v4 版官方文檔,重寫了其中10多個可運行示例文檔,並使用 Dumi 部署在 NornJ 的文檔站點中:mobxFormData 表單示例文檔。
你們能夠拿它和 antd 官方表單示例文檔 作下對比,其實能夠看出在一樣功能的狀況下,mobxFormData 的代碼量一般會更少一些。
mobxFormData 方案在我司大部門內已有多個線上實際項目在用,因此我以爲若是您認爲它對您的開發體驗有好處,或有興趣嘗試,則能夠用於生產環境。做者也會一直堅持更新這個項目,若是發現問題很是歡迎您的反饋。
最後,依做者的實踐經驗,總結出一些做者認爲的目前 JSX 擴展方案可行經驗,在此分享給你們:
在一些文章評論中,我記得不僅一次看到過有人提過: 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 中也可能會存在,像 Antd 的 Form 組件,從早期版本直到最新的 V4 版,在我看來它的數據管理方式其實一直都相似於雙向數據綁定,但並無用指令方式 API 實現。從它的官方文檔中,也一直能夠看到對雙向綁定的描述。
對於指令的實現,不一樣的 Babel JSX 擴展項目的實現也不一樣,大多數是語法糖轉換;也有比較特殊的,好比 NornJ 的mobxBind 指令,它的實現實際上是一個React 高階組件。因此說 API 只是形式,並不必定表明底層實現。
這個領域確實比較偏,如下是做者這些年來見過的幾個 Babel JSX 擴展項目,它們都提供了流程控制等常見 JSX 擴展:
目前可擴展的 Babel JSX 插件除了做者本身開發的 NornJ:
暫時未找到其餘現有的能讓開發者擴展的,若是有朋友知道的話能夠告訴我,感謝😃