Formily Vue 自定义组件开发:useField Hook 读取与操作字段状态完全指南
2026/9/24 7:28:06 网站建设 项目流程
  • 前端
  • UI组件

【免费下载链接】formily

📱🚀 🧩 Cross Device & High Performance Normal Form/Dynamic(JSON Schema) Form/Form Builder -- Support React/React Native/Vue 2/Vue 3

项目地址:https://gitcode.com/gh_mirrors/fo/formily
点击查看免费下载

导读

useField是 Formily Vue 体系中面向自定义组件开发的核心 Hook,它让任意位于 Field 组件子树内的自定义组件都能直接读取当前字段的属性、值、校验状态,并主动调用字段模型的方法进行状态操作。本文基于 formily 仓库中 packages/vue/docs/api/hooks/use-field.md 的官方文档,并结合@formily/vue的实际源码与测试用例,深入讲解useField的签名、工作原理、类型检查器配合方案与响应式注意点,读完即可在自己的自定义组件中安全、高效地使用useField

一、useField 是什么:用途与适用场景

useField主要用在自定义组件内,用于完成两类核心工作:

  1. 读取当前字段属性:例如读取字段的valuedisplaypatternpathaddress、校验反馈信息等;
  2. 操作字段状态:例如调用字段的setValuesetStatesetDisplaysetPattern等方法,主动驱动字段模型变化。

在所有 Field 组件的子树内都能使用——这里的 Field 组件是广义的,包括 Field、ObjectField、ArrayField、VoidField 等。以 RecursionField 为例,它在setup中通过const parentRef = useField()获取父级字段,用于计算子字段的basePath,这正是"Field 子树内可用"的典型体现。

注意:useField拿到的是GeneralField(通用字段模型),它涵盖了 Field、ArrayField、ObjectField、VoidField 四类字段的公共能力。如果需要针对不同类型的字段做差异化处理,请配合 Type Checker(类型检查器)使用,下文第五节详解。

二、签名与类型定义

官方文档给出的签名为:

interface useField { (): Ref<Field> }

对照仓库中 packages/vue/src/hooks/useField.ts 的真实实现,签名更为精确:

import { inject, Ref, ref } from 'vue-demi' import { GeneralField } from '@formily/core' import { FieldSymbol } from '../shared/context' export const useField = <T = GeneralField>(): Ref<T> => { return inject(FieldSymbol, ref()) as any }

关键点说明:

  • 泛型参数T:默认返回Ref<GeneralField>。你可以显式传入更具体的字段类型,例如useField<FieldType>(),这在 packages/vue/src/tests/field.spec.ts 的useFormEffects测试用例中就有实际应用;
  • 返回值是Ref<T>:即一个 Vue 响应式引用,访问时需要通过.value解包,例如fieldRef.value.setValue('123')
  • 返回inject(FieldSymbol, ref()):从依赖注入容器中取出当前字段引用,若取不到则回退为空的ref(),保证在非字段上下文中调用也不会抛错。

这里涉及的FieldSymbol定义于 packages/vue/src/shared/context.ts:

export const FieldSymbol: InjectionKey<Ref<GeneralField>> = Symbol('field')

useField的全部导出由 packages/vue/src/hooks/index.ts 统一聚合,与useFormuseFormEffectsuseFieldSchemauseParentForm并列导出。

三、工作原理:provide / inject 注入链

useField之所以能"在所有 Field 子树内使用",依赖 Vue 的依赖注入机制,注入链如下:

  1. 提供方:字段渲染组件 ReactiveField 在创建字段模型后调用provide(FieldSymbol, fieldRef),把当前字段的Ref提供给整棵子树;
  2. 消费方:任意子组件(包括自定义组件、RecursionFieldReactiveField自身)通过inject(FieldSymbol, ...)拿到最近的父级字段;
  3. 嵌套取值:由于inject遵循"就近原则",在多级嵌套字段结构中,每个useField()拿到的一定是离自己最近的祖先字段。例如 ReactiveField.ts 中const parentRef = useField()用于读取父字段地址parentRef.value?.address,再结合basePath创建子字段,从而自动形成完整的字段地址链。

从源码结构看,Field/ObjectField/ArrayField/VoidField组件最终都会渲染为ReactiveField(如 Field.ts 所示,无论 Vue 2 还是 Vue 3 分支都指向ReactiveField),因此这一注入机制对四类字段统一生效。

四、基本用法:在自定义组件中读取与操作字段

4.1 读取字段属性的最小示例

参考 packages/vue/src/tests/field.spec.ts 中Input组件的写法,这是最经典的useField用法:

import { defineComponent, h } from 'vue' import { useField } from '@formily/vue' const Input = defineComponent({ props: ['value'], setup(props, { attrs, listeners }) { const fieldRef = useField() return () => { const field = fieldRef.value return h('input', { class: 'test-input', attrs: { ...attrs, value: props.value, 'data-testid': field.path.toString(), // 读取字段路径 }, on: { ...listeners, input: listeners.change, }, }) } }, })

在这个例子中,field.path.toString()直接读出了字段在表单中的路径(如cc.mm),测试断言getByTestId('cc.mm')也能证明嵌套字段路径的正确性。除了path,通过GeneralField你还能读取:

  • field.value:字段当前值;
  • field.display/field.pattern:字段的显示模式与交互模式(editable/disabled/readOnly/readPretty);
  • field.valid/field.errors/field.feedbacks:字段校验状态与反馈信息;
  • field.address/field.path:字段在表单树中的地址与路径;
  • field.form:所属表单实例。

其中displaypattern的取值与继承逻辑定义于 packages/core/src/models/BaseField.ts,它们会向上级联父字段与表单的默认值(例如默认display === 'visible'pattern === 'editable'),这解释了为什么字段模型具有"子随父动"的联动表现。

4.2 操作字段状态

useField拿到的字段模型本身是响应式模型,可直接调用其方法操作状态,例如:

const fieldRef = useField<FieldType>() // 设置值 fieldRef.value.setValue('123') // 批量修改状态 fieldRef.value.setState((state) => { state.value = '123' state.pattern = 'readPretty' }) // 切换显示/模式 fieldRef.value.setDisplay('hidden') fieldRef.value.setPattern('disabled')

值得注意的是,字段模型的setDisplay/setPattern同样定义于 BaseField.ts,其实现会在设置自身selfDisplay/selfPattern的同时维护与表单全局状态的联动关系。

4.3 在自定义组件中使用泛型约束

由于useField默认返回GeneralField,当你确定组件只会挂载在普通Field下时,可以传入更精确的类型以获得类型提示:

import { Field as FieldType } from '@formily/core' import { useField } from '@formily/vue' const fieldRef = useField<FieldType>()

这一写法在 field.spec.ts 的useFormEffects测试中被实际使用,并通过isVoidField(target)守卫后调用fieldRef.value.setValue(...)

五、GeneralField 与 Type Checker:按类型差异化处理

useField返回的是GeneralField,它只保证四类字段(Field、ArrayField、ObjectField、VoidField)的公共能力。若需按类型做差异化逻辑,官方文档明确建议配合Type Checker使用。

Type Checker 位于 packages/core/src/shared/checkers.ts,核心导出包括:

  • isField
  • isArrayField
  • isObjectField
  • isVoidField

典型写法:

import { isVoidField, isField } from '@formily/core' import { useField } from '@formily/vue' const fieldRef = useField() // 只对非 VoidField 做值操作 if (!isVoidField(fieldRef.value)) { fieldRef.value.onInput('new value') } // 只对普通 Field 做处理 if (isField(fieldRef.value)) { fieldRef.value.setValue('123') }

在 RecursionField.ts 中,schema 的typeobject/array/void及其他)被分别路由到ObjectFieldArrayFieldVoidFieldField,这与 Type Checker 的分类体系一一对应;而在 ReactiveField.ts 渲染逻辑中,也大量使用isVoidField(field)判断是否注入valuedisabled等属性——例如 VoidField 不参与onInput值写入,普通 Field 则会根据pattern === 'disabled' || pattern === 'readPretty'自动设置disabled。这些实现细节印证了"先做类型判断再做字段操作"的工程惯例。

六、响应式要点:必须用 observer 包裹自定义组件

官方文档给出了明确警告:

如果要在自定义组件内使用useField并响应字段模型变化,需要使用observer包裹自定义组件。

原因在于:useField返回的fieldRef是一个普通的Ref(由shallowRef创建),字段模型内部的响应式变化并不会自动触发自定义组件的重渲染。只有通过 Formily 的响应式观察器observer(来自@formily/reactive-vue,源码位于 packages/reactive-vue/src/observer/)包裹组件,组件才会在字段模型的valuedisplaypattern等状态变化时自动更新。

正确的做法:

import { observer } from '@formily/reactive-vue' import { defineComponent, h } from 'vue' import { useField } from '@formily/vue' const CustomField = observer( defineComponent({ setup() { const fieldRef = useField() return () => { const field = fieldRef.value // 字段 value 变化时,此处会自动重新渲染 return h('div', {}, [String(field.value)]) } }, }) )

反向印证:在 ReactiveField.ts 中,字段渲染组件本身就用observer({...})包裹,因此字段模型的displayvalue变化能够驱动表单 UI 实时刷新;而自定义组件若不包裹observer,则只能读到"初始时"的字段状态,无法随模型变化更新。

测试方面,field.spec.ts 的useFormEffects用例中,CustomField组件在onFieldChange回调里调用fieldRef.value.setValue(target.value),随后waitFor断言custom-value节点的文本更新为'123'——这一过程正是"字段状态变化 → 组件响应式更新"的完整闭环验证。

七、进阶:useField 与其他 Hook 的组合

7.1 与 useParentForm 组合

useFielduseParentForm的底层依赖。查看 packages/vue/src/hooks/useParentForm.ts 的实现:

export const useParentForm = (): Ref<Form | ObjectField> => { const field = useField() const form = useForm() const findObjectParent = (field: GeneralField) => { if (!field) return form.value if (isObjectField(field)) return field return findObjectParent(field?.parent) } return computed(() => findObjectParent(field.value)) }

可见useParentForm通过useField()拿到当前字段,再沿field.parent向上查找最近的 ObjectField,找不到则回退到 Form 实例——这是"字段子树内可用"与 Type Checker 相结合的一个经典封装案例,也是自定义组件内"向上找容器/找表单"的推荐手段。

7.2 与 useFormEffects 组合

在自定义组件中,useField常与useFormEffects搭配实现字段联动。参考 field.spec.ts 中的用例:组件通过useFormEffects订阅onFieldChange('aa', ['value'], ...),在回调里读取目标字段的新值并写入当前字段,实现跨字段的值同步。这种方式将"字段订阅"与"字段操作"解耦,是动态表单联动的最佳实践之一。

八、适用前提与使用限制

  • 必须位于字段子树内useField依赖provide注入,只有在FormProvider→ 各类 Field 组件形成的嵌套结构内才能拿到字段;脱离字段上下文时它返回空Refref()的默认回退);
  • 拿到的是 GeneralField:需要具体类型能力(如数组字段的move/remove、对象字段的setValues等)时,请配合 Type Checker 收窄类型;
  • 响应式更新依赖 observer:不包裹observer时组件不会随字段模型变化而重渲染;
  • Vue 2 / Vue 3 双兼容@formily/vue通过vue-demi实现跨版本支持,useField在两种环境中均可直接使用(见 useField.ts 对vue-demi的引入,以及 ReactiveField.ts 中针对 Vue 2 的createFieldInVue2兼容逻辑)。

结语

useField虽只有短短几行实现,却是打通"自定义组件 ↔ 字段模型"的关键桥梁。理解其inject/provide注入机制、Ref返回值约定、Type Checker 类型分派与observer响应式约束,就能在 Formily Vue 生态中自由构建高性能的自定义字段组件。建议进一步阅读 useFieldSchema、useForm、useParentForm 等相邻 Hook 文档,以及 packages/vue/src/tests/field.spec.ts 中的完整测试用例,以获得更系统的掌握。

  • 前端
  • UI组件

【免费下载链接】formily

📱🚀 🧩 Cross Device & High Performance Normal Form/Dynamic(JSON Schema) Form/Form Builder -- Support React/React Native/Vue 2/Vue 3

项目地址:https://gitcode.com/gh_mirrors/fo/formily
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询