2

大家好,两年前我曾经发布过一篇文章《使用新一代js模板引擎NornJ提升React.js开发体验》,第一次尝试推广我创作的可扩展模板引擎 NornJ 。

Babel JSX 插件的新思路

在发表那篇文章后不到一周的时间,我仔细参考了jsx-control-statements,不自觉萌生出一个新的想法:

使用 Babel 提取含特殊信息的 JSX 标签,把它们转换为需运行时的渲染函数,是否能突破 JSX 现有的语法扩展能力?

这个想法随后就被实施:babel-plugin-nornj-in-jsx,并继续应用于公司部门内的多个实际项目中。Babel转换原理描述,请看这里

NornJ 下一代版本

有了上面的转换思路,并在繁忙业务中经过两年断断续续迭代,我在今年发布了重新设计后使用 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

基于 Mobx 与 JSX 语法扩展的 Ant Design 表单解决方案

我们部门团队自2016年起一直主力使用 Mobx 作为 React 状态管理方案,几年来我们一直受益于它的响应式数据流开发体验十分高效,也很容易优化。

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 插件:


joe_sky
269 声望19 粉丝

FE Developer JD.com