大家好,两年前我曾经发布过一篇文章《使用新一代js模板引擎NornJ提升React.js开发体验》,第一次尝试推广我创作的可扩展模板引擎 NornJ 。当时的 NornJ 完全基于字符串模板,在一些人看来它与 React JSX 环境似乎天然不匹配,上手晦涩一时难以看出优势。
在发表那篇文章后不到一周的时间,我仔细参考了jsx-control-statements,不自觉萌生出一个新的想法:
使用 Babel 提取含特殊信息的 JSX 标签,把它们转换为需运行时的渲染函数,是否能突破 JSX 现有的语法扩展能力?
这个想法随后就被实施:babel-plugin-nornj-in-jsx,并继续应用于公司部门内的多个实际项目中。Babel转换原理描述,请看这里。
有了上面的转换思路,并在繁忙业务中经过两年断断续续迭代,我在今年发布了重新设计后使用 JSX API 的 NornJ 正式版,并重写了文档,源码也用 Typescript 几乎完全重写:
github:https://github.com/joe-sky/nornj
文档(gitee.io):https://joe-sky.gitee.io/nornj
文档(github.io):https://joe-sky.github.io/nornj
我们部门团队自2016年起一直主力使用 Mobx 作为 React 状态管理方案,几年来我们一直受益于它的响应式数据流开发体验十分高效,也很容易优化。
虽然关于 Mobx 与 Redux 等谁更优不是本篇文章里要对比的,但是通过几年的使用经验,我总结出 Mobx 在配合国内最流行的 React 组件库 Ant Design 组件,特别是表单验证组件时可能存在的一些开发痛点:
Antd Form 组件原生方式使用 getFieldsValue 和 setFieldsValue (官方文档)来对数据进行存取,这在使用 Mobx 做数据流管理时会遇到一些比较尴尬的场景:
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 方案的语法整体看起来,和 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:
暂时未找到其他现有的能让开发者扩展的,如果有朋友知道的话可以告诉我,感谢😃