- 前端
- UI组件
【免费下载链接】formily
📱🚀 🧩 Cross Device & High Performance Normal Form/Dynamic(JSON Schema) Form/Form Builder -- Support React/React Native/Vue 2/Vue 3
FormProvider 是 Formily 在 Vue 场景下的入口组件,它通过依赖注入把由createForm创建的 Form 实例下发给子树中的所有字段组件,是整个表单状态通讯的枢纽。本文将结合 packages/vue/src/components/FormProvider.ts 源码、官方用例 与 单元测试,完整讲解 FormProvider 的签名、用法、底层注入机制、生命周期管理、嵌套隔离策略,以及它与useForm、useField、FormConsumer等配套 API 的协作方式,帮助你在 React 之外的 Vue 2 / Vue 3 项目中正确搭建 Formily 表单。
FormProvider 的定位:表单状态通讯枢纽
在 Formily 的 Vue 实现中,createForm创建出来的 Form 实例并不会自动“跑”到每一个字段组件里去。字段组件需要读取 form 上的values、errors、display等状态,也需要把自己的onInput、onFocus、onBlur等事件回传给 form。这份双向通讯的桥梁,就是 FormProvider。
官方文档对它的定位只有一句话:
入口组件,用于下发表单上下文给字段组件,负责整个表单状态的通讯,它相当于是一个通讯枢纽。
这意味着它承担了三层职责:
- 入口:一个 Vue 表单应用最外层包裹的组件;
- 下发:把 Form 实例通过 Vue 的依赖注入机制(
provide/inject)传给任意层级的后代组件; - 枢纽:字段的挂载、卸载、值变更、校验结果等状态变化,都经由这条上下文通道在 Form 模型与 UI 之间流动。
组件签名与 props
FormProvider 的类型签名如下(取自 form-provider.md):
type FormProvider = Vue.Component< any, any, any, { form: Form // 通过 createForm 创建的 form 实例 } >它只有一个核心 prop:form,类型为Form,对应源码中的IProviderProps:
export interface IProviderProps { form: Form }(见 packages/vue/src/types/index.ts)
Form是@formily/core暴露的表单模型实例,由createForm创建。Form 模型本身包含values、initialValues、errors、display、mounted等大量响应式状态,以及setValues、query、submit、onMount、onUnmount等操作方法,完整清单可参考 packages/core/docs/api/models/Form.md。
基础用法:包裹一个最小表单
官方 demo(form-provider.vue)展示了最典型的用法:
<template> <FormProvider :form="form"> <Field name="input" :component="[Input]" /> </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>要点:
form必须在data中持有(或由组合式 API 返回),并通过:form="form"绑定,保证实例在整个组件生命周期内稳定;FormProvider内部不会渲染任何额外 DOM 节点(渲染的是 Fragment),因此它可以安心地作为组件树的最外层包裹层;- 所有
Field、ObjectField、ArrayField、VoidField等字段组件都必须放在 FormProvider 的作用域内,才能拿到表单上下文。
如果你不需要自定义字段,只是想响应式地读取表单状态,可以在内部配合FormConsumer使用 scoped slot:
<template> <FormProvider :form="form"> <Field name="input" :component="[Input]" /> <FormConsumer> <template #default="{ form }"> {{ form.values.input }} </template> </FormConsumer> </FormProvider> </template>(完整示例见 form-consumer.vue,FormConsumer的语义见 form-consumer.md)
源码级原理:依赖注入如何把 Form 传遍全树
FormProvider 的实现非常精简,核心逻辑集中在 FormProvider.ts:
import { provide, defineComponent, toRef } from 'vue-demi' import { FormSymbol, FieldSymbol, SchemaMarkupSymbol, SchemaSymbol, SchemaExpressionScopeSymbol, SchemaOptionsSymbol, } from '../shared/context' import { IProviderProps, DefineComponent } from '../types' import { useAttach } from '../hooks/useAttach' import { useInjectionCleaner } from '../hooks/useInjectionCleaner' import h from '../shared/h' import { Fragment } from '../shared/fragment' export default defineComponent({ name: 'FormProvider', inheritAttrs: false, props: ['form'], setup(props: IProviderProps, { slots }) { const formRef = useAttach(toRef(props, 'form')) provide(FormSymbol, formRef) useInjectionCleaner([ FieldSymbol, SchemaMarkupSymbol, SchemaSymbol, SchemaExpressionScopeSymbol, SchemaOptionsSymbol, ]) return () => h(Fragment, {}, slots) }, }) as DefineComponent<IProviderProps>这段代码揭示了四个关键机制:
1. 注入键:FormSymbol
FormSymbol定义在 context.ts:
export const FormSymbol: InjectionKey<Ref<Form>> = Symbol('form')它是一个以Symbol('form')为标识的InjectionKey,value 类型是Ref<Form>。后代组件通过inject(FormSymbol, ref())即可拿到表单实例的响应式引用,例如useForm:
export const useForm = (): Ref<Form> => { const form = inject(FormSymbol, ref()) return form }(见 packages/vue/src/hooks/useForm.ts)
以Ref形式下发而不是直接下发实例,是为了在formprop 发生变化时(比如切换表单)让所有依赖方都能感知到新实例。
2. 生命周期接管:useAttach
useAttach负责把 form 模型的生命周期与组件生命周期对齐,实现见 useAttach.ts:
export const useAttach = <T extends IRecycleTarget>(target: Ref<T>): Ref<T> => { watch(target, (v, old, onInvalidate) => { if (v && v !== old) { old?.onUnmount() nextTick(() => v.onMount()) onInvalidate(() => v.onUnmount()) } }) onMounted(() => { target.value?.onMount() }) onUnmounted(() => { target.value?.onUnmount() }) return target }它的行为是:
- 组件挂载时:调用
form.onMount(),让 Form 模型进入 mounted 状态(form.mounted === true); - 组件卸载时:调用
form.onUnmount(),触发模型级卸载清理; - form 实例被替换时:先卸载旧实例,
nextTick后再挂载新实例,同时通过onInvalidate注册旧实例的卸载回调。
onMount/onUnmount是 Form 模型的标准生命周期方法,官方说明见 Form.md。字段组件(如ReactiveField)也通过同一个useAttach管理自己的挂载/卸载,从而保证整个字段树与 Form 模型的挂载状态严格同步。
3. 注入清理:useInjectionCleaner
useInjectionCleaner是 FormProvider 中容易被忽略但很关键的一步。它会在 FormProvider 挂载时把以下几类“残留注入”清理掉:
FieldSymbol(字段上下文)SchemaMarkupSymbol(标记式 schema)SchemaSymbol(schema 定义)SchemaExpressionScopeSymbol(schema 表达式作用域)SchemaOptionsSymbol(schema 组件映射配置)
清理的意义在于防止上下文跨表单泄漏:如果一个字段组件意外出现在 FormProvider 外部,它不至于拿到上一个表单残留的上下文。这一点在 form.spec.ts 的useInjectionCleaner测试中被专门验证。
4. 渲染:Fragment
FormProvider 本身不产生额外 DOM:
return () => h(Fragment, {}, slots)它只是把默认插槽原样渲染出来,因此你可以放心地把它放在任意层级,不会破坏 CSS 布局。
嵌套场景:子表单隔离
FormProvider 是可以嵌套使用的。在嵌套场景下,内层 FormProvider 会通过provide覆盖外层下发的FormSymbol,从而形成独立的表单上下文,同时useInjectionCleaner也会把外层残留的字段级上下文清掉,避免内层字段错误地挂到外层表单上。
单元测试 useInjectionCleaner 用例 正是这样验证的:外层FormProvider内放一个Field name="parent",其内部再嵌一个FormProvider :form="form",内层字段inner与外层字段outer分属各自的上下文,且都能正常工作:
<FormProvider :form="form"> <Field name="parent"> <FormProvider :form="form"> <Field name="inner" :component="[Input]" /> </FormProvider> <Field name="outer" :component="[Input]" /> </Field> </FormProvider>测试断言form.query('inner')与form.query('parent.outer')的mounted都为真,且各自可以独立更新值。这证明 FormProvider 既支持嵌套子表单,也能保证字段归属正确。
与配套 API 的协作关系
FormProvider 只是“入口枢纽”,真正消费它下发上下文的是下面这些 API:
| API | 消费方式 | 用途 |
|---|---|---|
| useForm | inject(FormSymbol) | 在自定义组件中读取当前 Form 实例,例如依赖form.errors实现复杂场景组件 |
| useField | inject(FieldSymbol) | 读取当前字段模型(字段级上下文由ReactiveField下发) |
| useParentForm | 递归向上查找 | 从当前字段向上找到最近的对象字段(ObjectField)或 Form |
| FormConsumer | useForm()+ scoped slot | 响应式监听表单数据变化并触发局部 UI 更新 |
| Field 系列组件(Field / ObjectField / ArrayField / VoidField) | useForm()+useField() | 创建并绑定字段模型,见 ReactiveField.ts |
其中useParentForm的实现比较有代表性,它先用useField拿到当前字段,再沿field.parent链向上递归,找到最近的ObjectField(用于处理表单分区场景),否则返回 Form 本身:
const findObjectParent = (field: GeneralField) => { if (!field) return form.value if (isObjectField(field)) return field return findObjectParent(field?.parent) }(见 packages/vue/src/hooks/useParentForm.ts)
对应的测试useParentForm用例(form.spec.ts)验证了:ObjectField内的组件拿到的是ObjectField,VoidField内的组件和外层组件拿到的都是Form。这套“上下文 + 组合式 API”的架构,正是 FormProvider 作为枢纽价值的直接体现。
Vue 2 / Vue 3 双端兼容
FormProvider 的源码直接使用vue-demi的provide、defineComponent、toRef等 API 编写,因此同一份实现可以同时运行在 Vue 2 与 Vue 3 上:
- Vue 3 场景:直接从 components/index.ts 导出;
- Vue 2 场景:通过 vue2-components.ts 做一次类型层面的转换(
DefineComponent = Vue & VueConstructor & Props)后导出,供Vue.component('FormProvider', FormProvider)或 options API 注册使用。
测试文件 form.spec.ts 正是以 Vue 2 的方式(Vue.component('FormProvider', FormProvider)+@testing-library/vue)验证 FormProvider 的基础渲染、生命周期与嵌套行为的,这意味着上述全部机制在 Vue 2 下同样成立。
总结
FormProvider 是 Formily Vue 表单架构的基石组件:它通过provide(FormSymbol, ...)把createForm创建的 Form 实例以响应式引用形式下发到整棵组件树,通过useAttach对齐表单模型与组件生命周期,通过useInjectionCleaner防止上下文跨表单泄漏,并以 Fragment 形式保持 DOM 纯净。无论你使用Field系列组件、FormConsumer,还是useForm/useField/useParentForm等组合式 API,它们的上下文来源都指向同一个枢纽——FormProvider。掌握它的注入与生命周期机制,是深入理解并排障 Formily Vue 表单的起点。
- 前端
- UI组件
【免费下载链接】formily
📱🚀 🧩 Cross Device & High Performance Normal Form/Dynamic(JSON Schema) Form/Form Builder -- Support React/React Native/Vue 2/Vue 3
相关推荐
Formily React FormProvider 详解:表单状态通信枢纽的接入与底层实现
Formily React FormProvider 详解:表单状态通信枢纽的接入与底层实现 导读 FormProvider 是 Formily 在 React
前端UI组件FormProvider:Formily React 表单状态通讯枢纽的入口组件
FormProvider:Formily React 表单状态通讯枢纽的入口组件 FormProvider 是 Formily 在 React 体系中的入口组件
前端UI组件Formily Next ArrayCards 卡片列表组件完全指南:Schema 场景下的数组卡片表单实战
Formily Next ArrayCards 卡片列表组件完全指南:Schema 场景下的数组卡片表单实战 本文以 @formily/next https:/
前端UI组件
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考