# 核心概念
@formily/vue 本身架构不复杂,因为它只是提供了一系列的组件和 Hooks 给用户使用,但是我们还是需要理解以下几个概念:
- 表单上下文
- 字段上下文
- 协议上下文
- 模型绑定
- 协议驱动
- 三种开发模式
# 表单上下文
从架构图中我们可以看到 FormProvider 是作为表单统一上下文而存在,它的地位非常重要,主要用于将@formily/core 创建出来的Form实例下发到所有子组件中,不管是在内置组件还是用户扩展的组件,都能通过useForm读取到Form实例
# 字段上下文
从架构图中我们可以看到不管是 Field/ArrayField/ObjectField/VoidField,会给子树下发一个 FieldContext,我们可以在自定义组件中读取到当前字段模型,主要是使用useField来读取,这样非常方便于做模型映射
# 协议上下文
从架构图中我们可以看到RecursionField会给子树下发一个 FieldSchemaContext,我们可以在自定义组件中读取到当前字段的 Schema 描述,主要是使用useFieldSchema来读取。注意,该 Hook 只能用在SchemaField和RecursionField子树中使用
# 模型绑定
想要理解模型绑定,需要先理解什么是MVVM,理解了之后我们再看看这张图:
在 Formily 中,@formily/core 就是 ViewModel,Component 和 Decorator 就是 View,@formily/vue 就是将 ViewModel 和 View 绑定起来的胶水层,ViewModel 和 View 的绑定就叫做模型绑定,实现模型绑定的手段主要有useField,也能使用connect和mapProps,需要注意的是,Component 只需要支持 value/onChange 属性即可自动实现数据层的双向绑定。
# 协议驱动
协议驱动渲染算是 @formily/vue 中学习成本最高的部分了,但是学会了之后,它给业务带来的收益也是很高,总共需要理解 4 个核心概念:
- Schema
- 递归渲染
- 协议绑定
- 三种开发模式
# Schema
formily 的协议驱动主要是基于标准 JSON Schema 来进行驱动渲染的,同时我们在标准之上又扩展了一些x-*
属性来表达 UI,使得整个协议可以具备完整描述一个复杂表单的能力,具体 Schema 协议,参考Schema API 文档
# 递归渲染
何为递归渲染?递归渲染就是组件 A 在某些条件下会继续用组件 A 来渲染内容,看看以下伪代码:
{ <---- RecursionField(条件:object;渲染权:RecursionField)
"type":"object",
"properties":{
"username":{ <---- RecursionField(条件:string;渲染权:RecursionField)
"type":"string",
"x-component":"Input"
},
"phone":{ <---- RecursionField(条件:string;渲染权:RecursionField)
"type":"string",
"x-component":"Input",
"x-validator":"phone"
},
"email":{ <---- RecursionField(条件:string;渲染权:RecursionField)
"type":"string",
"x-component":"Input",
"x-validator":"email"
},
"contacts":{ <---- RecursionField(条件:array;渲染权:RecursionField)
"type":"array",
"x-component":"ArrayTable",
"items":{ <---- RecursionField(条件:object;渲染权:ArrayTable组件)
"type":"object",
"properties":{
"username":{ <---- RecursionField(条件:string;渲染权:RecursionField)
"type":"string",
"x-component":"Input"
},
"phone":{ <---- RecursionField(条件:string;渲染权:RecursionField)
"type":"string",
"x-component":"Input",
"x-validator":"phone"
},
"email":{ <---- RecursionField(条件:string;渲染权:RecursionField)
"type":"string",
"x-component":"Input",
"x-validator":"email"
},
}
}
}
}
}
@formily/vue 递归渲染的入口是SchemaField,但它内部实际是使用 RecursionField 来渲染的,因为 JSON-Schema 就是一个递归型结构,所以 RecursionField 在渲染的时候会从顶层 Schema 节点解析,如果是非 object 和 array 类型则直接渲染具体组件,如果是 object,则会遍历 properties 继续用 RecursionField 渲染子级 Schema 节点。
这里有点特殊的情况是 array 类型的自增列表渲染,需要用户在自定义组件内使用RecursionField进行递归渲染,因为自增列表的 UI 个性化定制程度很高,所以就把递归渲染权交给用户来渲染了,这样设计也能让协议驱动渲染变得更加灵活。
那 SchemaField 和 RecursionField 有啥差别呢?主要有两点:
- SchemaField 是支持 Markup 语法的,它会提前解析 Markup 语法生成JSON Schema移交给 RecursionField 渲染,所以 RecursionField 只能基于 JSON Schema 渲染
- SchemaField 渲染的是整体的 Schema 协议,而 RecursionField 渲染的是局部 Schema 协议
# 协议绑定
前面讲了模型绑定,而协议绑定则是将 Schema 协议转换成模型绑定的过程,因为 JSON-Schema 协议是 JSON 字符串,可离线存储的,而模型绑定则是内存间的绑定关系,是 Runtime 层的,比如x-component
在 Schema 中是组件的字符串标识,但是在模型中的 component 则是需要组件引用,所以 JSON 字符串与 Runtime 层是需要转换的。然后我们就可以继续完善一下以上模型绑定的图:
总结下来,在 @formily/vue 中,主要有 2 层绑定关系,Schema 绑定模型,模型绑定组件,实现绑定的胶水层就是 @formily/vue,需要注意的是,Schema 绑定字段模型之后,字段模型中是感知不到 Schema 的,比如要修改enum
,就是修改字段模型中的dataSource
属性了,总之,想要更新字段模型,参考Field,想要理解 Schema 与字段模型的映射关系可以参考Schema文档
# 三种开发模式
从架构图中我们可以看到整个 @formily/vue 是有三种开发模式的,对应不同用户:
- Template 开发模式
- JSON Schema 开发模式
- Markup Schema 开发模式
# Template 开发模式
该模式主要是使用 Field/ArrayField/ObjectField/VoidField 组件
<template>
<FormProvider :form="form">
<Field name="input" :component="[Input, { placeholder:'请输入' }]" />
</FormProvider>
</template>
<script>
import { Input } from 'ant-design-vue'
import { createForm } from '@formily/core'
import { FormProvider, Field } from '@formily/vue'
import 'ant-design-vue/dist/antd.css'
export default {
components: { FormProvider, Field },
data() {
return {
Input,
form: createForm(),
}
},
}
</script>
# JSON Schema 开发模式
该模式是给 SchemaField 的 schema 属性传递 JSON Schema 即可
<template>
<FormProvider :form="form">
<SchemaField :schema="schema" />
</FormProvider>
</template>
<script>
import { Input } from 'ant-design-vue'
import { createForm } from '@formily/core'
import { FormProvider, createSchemaField } from '@formily/vue'
import 'ant-design-vue/dist/antd.css'
const { SchemaField } = createSchemaField({
components: {
Input,
},
})
export default {
components: { FormProvider, SchemaField },
data() {
return {
form: createForm(),
schema: {
type: 'object',
properties: {
input: {
type: 'string',
'x-component': 'Input',
'x-component-props': {
placeholder: '请输入',
},
},
},
},
}
},
}
</script>
# Markup Schema 开发模式
该模式算是一个对源码开发比较友好的 Schema 开发模式,同样是使用 SchemaField 相关组件。
Markup Schema 模式主要有以下几个特点:
- 主要依赖 SchemaStringField/SchemaArrayField/SchemaObjectField...这类描述标签来表达 Schema
- 每个描述标签都代表一个 Schema 节点,与 JSON-Schema 等价
- SchemaField 子节点不能随意插 UI 元素,因为 SchemaField 只会解析子节点的所有 Schema 描述标签,然后转换成 JSON Schema,最终交给RecursionField渲染,如果想要插入 UI 元素,可以在 SchemaVoidField 上传
x-content
属性来插入 UI 元素
<template>
<FormProvider :form="form">
<SchemaField>
<SchemaStringField
x-component="Input"
:x-component-props="{ placeholder: '请输入' }"
/>
<div>我不会被渲染</div>
<SchemaVoidField x-content="我会被渲染" />
<SchemaVoidField :x-content="Comp" />
</SchemaField>
</FormProvider>
</template>
<script>
import { Input } from 'ant-design-vue'
import { createForm } from '@formily/core'
import { FormProvider, createSchemaField } from '@formily/vue'
import 'ant-design-vue/dist/antd.css'
const SchemaComponents = createSchemaField({
components: {
Input,
},
})
const Comp = {
render(h) {
return h('div', ['我也会被渲染'])
},
}
export default {
components: { FormProvider, ...SchemaComponents },
data() {
return {
form: createForm(),
Comp,
}
},
}
</script>
← 核心架构