你们好,两年前我曾经发布过一篇文章《使用新一代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:
暂时未找到其余现有的能让开发者扩展的,若是有朋友知道的话能够告诉我,感谢😃