一个月的Formily使用经验总结

背景

因为工作原因近一个月换了完全没用过的技术栈开发新项目。React、Storybook 这些倒还好，基本一天就上手了，倒是 Formily 这厮陆续摸索了半个多月才算入门，趁一阶段工作结束空隙我就把这些经验总结一下。

因为项目原因我使用 Formily 的方式不一定是最佳实践甚至不一定是对的，对表单方面的使用也约等于零，主要场景集中于使用@formily/antd-v5和自制组件上。

基础

Formily 是阿里开发的一套以 Schema 描述表单的开源框架，不过我们项目中使用它的理由并不是构建表单，而是用于动态构建页面。

Formily 里有三个比较重要的概念：Form、Field 和 Schema。

Form 是整个表单的核心，其实例可用于获取和设置表单的校验、值、字段、字段联动等内容

Field 是 Form 的组成部件，也就是字段，比如表单里设置了一个叫 name 为keyword的输入框，那每次修改输入框的内容时，form.values.keyword就会跟着变动。

Schema 是描述 Field 的数据，可以简单的认为其与 ReactNode 等价，具体一个 Schema 能包含什么内容可以参考官方文档。

在 Schema 转组件的过程中 Formily 会默认代理组件的value和onChange，所以要开发 Formily 组件时要考虑到这一点。

从一个简单的页面开始

先从一个简单的示例开始

上面的代码创建一个 ==“带边框的 div，里面装着一个输入框和另一个带了两个输入框的容器”== 的场景，从代码中我们可以得到几个结论：

Schema 的层级结构相当于 HTML 中的层级结构，其中的properties起到了类似children的作用。

createForm中可以定义表单的一些初始状态？

x-component代表当前位置需要渲染的元素，可以支持原生的标签可自定义的组件，其中的自定义组件需要在createSchemaField中进行注册。

x-component-props中定义了组件的 props，x-decorator定义了包装这个组件的组件。

简单操作一下表单，可以发现 Schema 和实际的表单值的映射关系。

如果要管理不同字段的值，schema 的type非常关键。

当type为void的时候，表单会忽略这一层的路径。

当type为object的时候，该字段会成为承接子字段的对象。

当type为基本类型的时候，该字段代表具体的值。

当时我们本来打算封装一个组合了 Card + Tabs 功能的组件，但是在 Formily 的机制下，字段本身不能既表示自己的值（Tabs 的 activeKey），也成为包含子字段的对象，所以最后打消了这种做法。

函数处理

如果坚持使用 JSON Schema，那往 Schema 里塞函数的做法就显得不那么合理了。在 Schema 中，Formily 会把{{}}的字符串处理成函数，所以要转换一下写法。

Copy
// ...
input: {
type: "string",
"x-component": "Input",
"x-component-props": {
onClick: `{{ (event) => { console.log(event) } }}`
}
}
// ...

同样的，对需要传 ReactNode 的 props 也是如此处理，不过需要使用React.createElement

Copy
import { createElement } from "react";
import { Input } from '@formily/antd-v5'
import { SearchOutlined } from '@ant-design/icons';

// ...
const SchemaField = createSchemaField({
components: {
Input
},
scope: {
createElement,
SearchOutlined
}
})
// ...
input: {
type: "string",
"x-component": "Input",
"x-component-props": {
suffix: `{{ createElement(SearchOutlined) }}`
}
}
// ...

字段联动

这部分内容官网文档说得就挺好的，就不赘述了

数据传递

我们当时的页面有一个联动逻辑，当在顶部导航栏切换时间后，下面的各个图表都要同步更新数据，而我们的解决方式如下，不一定是最佳实践。

组件开发流程

Formily 组件和常规的 React 组件差别还是挺大的，如果把@formily/antd-v5作为官方的推荐实践方式的话，那就不能以之前的思路来开发组件。

以 antd 的 Table 为例，无论是columns还是dataSource都是作为 props 的一部分传递给组件的，但在 Formily 中，数据应只由于默认配置的props.value来处理，所以开发时应该做好渲染数据与props.value的转换。对接已有组件时也可以用官方的mapProps方法做映射。

要较真的话数据放在哪都是可以实现功能的，但能简单的从form.values拿到所有值还是比把数据分散到 field 的componentProps要方便的。

除此之外，善用useForm、useField和useFieldSchema可以方便获取父子字段的内容。

一些坑

ReactNode 转 Schema

虽然可以用{{createElement}}的方式传递 ReactNode，但对于自定义组件来说还需要提前把组件传入 scope 中，对于动态的 Schema 来说，这难以做到按需导入。

我当时的做法是组件内部做拦截。用useFieldSchema拿到 Schema，然后判断对应的属性是否也是一个 Schema 对象，如果是则返回一个RecursionField来渲染 Schema。

拦截写完后乍一看还没什么毛病，但是在 antd 的 Table sortIcon 时就傻眼了，渲染的图标在点击后完全没有状态变化。

个人猜测这跟 Formily 的渲染机制有关，RecursionField的渲染结果被缓存了，只是重新运行函数传递新的 props 还不足以触发其重新渲染。

onChange

还是跟 Table 的过滤有关，原流程中点击表头的排序会触发表格的onChange事件获得排序信息。

但我在尝试换了多种写法后发现仍不能触发这个onChange，看了源码后才发现为了防止冒泡官方给onChange覆盖了个空函数。你们是完全用不到这个功能吗？

业务层面没什么解决方法，这里最后用了 patch 的方式解决的，颇为麻烦。