背景#
因为工作原因近一个月换了完全没用过的技术栈开发新项目。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 组件时要考虑到这一点。
- 在 Schema 转组件的过程中 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 会把{{}}
的字符串处理成函数,所以要转换一下写法。
// ...
input: {
type: "string",
"x-component": "Input",
"x-component-props": {
onClick: `{{ (event) => { console.log(event) } }}`
}
}
// ...
同样的,对需要传 ReactNode 的 props 也是如此处理,不过需要使用React.createElement
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 的方式解决的,颇为麻烦。