Formily React 上下文体系全解析:从 FormContext 到 SchemaOptionsContext 的个性化定制指南
2026/9/23 17:01: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
点击查看免费下载

在 Formily 的 React 实现(@formily/react)中,一套精心设计的 React Context 构成了整个表单内核(@formily/core)与 JSX 视图层之间的桥梁。无论是获取当前表单实例、读取当前字段对象,还是收集 JSX Markup 写法并转换为标准 JSON Schema,都依赖这些上下文。本文以官方 API 文档 context.md 为主线,结合仓库源码逐层拆解每一个 Context 的职责、签名、提供者与消费方式,帮助你在做复杂个性化定制(自定义组件、自定义渲染器、深度封装 SchemaField)时有的放矢。

一、Context 体系总览

在 packages/react/src/shared/context.ts 中,官方一共定义了 7 个 Context(官方文档列出 6 个,另有 1 个SchemaComponentsContext可在源码中找到),全部通过createContext创建,默认值均为null

Context 名称类型默认值主要用途文档是否收录
FormContextFormnull获取当前 Form 实例
FieldContextGeneralFieldnull获取当前字段实例
SchemaMarkupContextSchemanull收集 JSX Markup 写法的 Schema 标签并转换为 JSON Schema
SchemaContextSchemanull获取当前字段的 Schema 信息
SchemaExpressionScopeContextanynullSchema 表达式作用域
SchemaOptionsContextISchemaFieldReactFactoryOptionsnull获取createSchemaField传入的全局参数
SchemaComponentsContextSchemaReactComponentsnull字符串到组件的映射表(源码补充)❌(源码可见)

此外,同一文件还导出了一个ContextCleaner工具组件,用于在进入FormProvider子树时把除FormContext之外的上下文统一重置为undefined,避免组件在非表单环境中误读到“脏”数据,其实现基于createContextCleaner对多个 Context 的Provider进行折叠:

export const ContextCleaner = createContextCleaner( FieldContext, SchemaMarkupContext, SchemaContext, SchemaExpressionScopeContext, SchemaComponentsContext, SchemaOptionsContext )

所有 Context 均从包入口 packages/react/src/index.ts 通过export * from './shared'对外导出,因此你可以直接import { FormContext } from '@formily/react'使用,也可以通过官方提供的useFormuseFielduseFieldSchemauseExpressionScope等 Hook 间接消费。

二、FormContext:获取当前 Form 实例

FormContext是整个表单树的根上下文,承载着由createForm()创建的Form实例,是任何需要访问表单状态(valueserrorssubmitsetValues等)的自定义组件的入口。

签名(见 context.ts)

import { Form } from '@formily/core' const FormContext = createContext<Form>(null)

提供者:FormProvider

FormProvider.tsx 是唯一的 Provider 组件,它先用useAttach挂载表单生命周期,再以ContextCleaner包裹FormContext.Provider

export const FormProvider: ReactFC<IProviderProps> = (props) => { const form = useAttach(props.form) return ( <ContextCleaner> <FormContext.Provider value={form}>{props.children}</FormContext.Provider> </ContextCleaner> ) }

消费方式:useFormHook

官方封装的 useForm.ts 就是对useContext(FormContext)的直接包装:

export const useForm = <T extends object = any>(): Form<T> => { return useContext(FormContext) }

典型的自定义组件用法:

import React from 'react' import { FormProvider, useForm } from '@formily/react' import { createForm } from '@formily/core' const CustomComponent = () => { const form = useForm() return ( <button onClick={() => form.submit(console.log)}> 提交(当前字段数:{Object.keys(form.values).length}) </button> ) } export default () => { const form = createForm() return ( <FormProvider form={form}> <CustomComponent /> </FormProvider> ) }

需要特别说明的是,若自定义组件需要拿到“最近的祖先表单”,官方推荐使用 useParentForm.ts 中封装的useParentForm:它会优先向上查找最近的ObjectField父级,找不到才回退到useForm()得到的表单实例,这在嵌套子表单(如数组项内的独立表单)场景下尤其有用。

三、FieldContext:获取当前字段实例

FieldContext保存的是当前渲染位置所属的字段实例,类型为GeneralField(即FieldObjectFieldArrayFieldVoidField的联合类型),通过它可以在自定义组件内部读取field.valuefield.titlefield.pathfield.selfErrors等字段级状态。

签名(见 context.ts)

import { GeneralField } from '@formily/core' const FieldContext = createContext<GeneralField>(null)

提供者:四大字段组件

FieldContext.Provider由以下四个组件在渲染各自字段时写入:

  • Field.tsx:普通字段
  • ObjectField.tsx:对象字段
  • ArrayField.tsx:数组字段
  • VoidField.tsx:虚拟字段

Field为例:

return ( <FieldContext.Provider value={field}> <ReactiveField field={field}>{props.children}</ReactiveField> </FieldContext.Provider> )

消费方式:useFieldHook

useField.ts 同样是一行包装:

export const useField = <T = GeneralField>(): T => { return useContext(FieldContext) as any }

在 field.spec.tsx 测试中可以找到真实消费案例,自定义组件直接读取useField().path作为 DOM 的data-testid

const Custom = () => { return <div>const SchemaMarkupContext = createContext<Schema>(null)

提供者与收集机制:SchemaField / MarkupRender

在 SchemaField.tsx 中,createSchemaField生成的SchemaField组件在渲染前先通过renderMarkup()阶段执行 JSX 收集:

const renderMarkup = () => { env.nonameId = 0 if (props.schema) return null return render( <SchemaMarkupContext.Provider value={schema}> {props.children} </SchemaMarkupContext.Provider> ) }

SchemaField.Markup(以及StringObjectArrayBooleanNumberDateDateTimeVoid等快捷类型)最终都走内部的MarkupRender:它通过useContext(SchemaMarkupContext)拿到父级 Schema 实例,然后按父级类型调用parent.addProperty(name, props)(object/void 类型)或parent.setItems(schema)/appendArraySchema(array 类型)把当前标签挂载为子节点,并把新生成的子 Schema 继续通过SchemaMarkupContext.Provider下发,实现递归收集:

function MarkupRender(props: any) { const parent = useContext(SchemaMarkupContext) if (!parent) return <Fragment /> if (parent.type === 'object' || parent.type === 'void') { const schema = parent.addProperty(props.name, props) return ( <SchemaMarkupContext.Provider value={schema}> {renderChildren()} </SchemaMarkupContext.Provider> ) } // array 分支:parent.setItems / appendArraySchema ... }

该机制在 schema.markup.spec.tsx 中有大量测试覆盖,包括x-contentx-component等属性的收集与渲染。理解这一层,有助于你弄清楚“JSX Markup 与 JSON Schema 两种写法为何等价”这一核心原理。

五、SchemaContext:当前字段的 Schema 信息

SchemaContext保存的是“当前字段”对应的Schema实例(来自@formily/json-schema),用于在自定义组件内读取字段的 schema 元信息(如schema.titleschema.typeschema['x-component']schema.properties),是编写递归渲染器(RecursionField 式组件)的关键上下文。

签名(见 context.ts)

const SchemaContext = createContext<Schema>(null)

提供者:RecursionField

RecursionField.tsx 在每次递归渲染前,把当前子 Schema 注入SchemaContext

return ( <SchemaContext.Provider value={fieldSchema}> {render()} </SchemaContext.Provider> )

消费方式:useFieldSchemaHook

useFieldSchema.ts 直接消费该上下文:

export const useFieldSchema = (): Schema => { return useContext(SchemaContext) }

在 schema.markup.spec.tsx 的recursion field测试中,自定义对象组件CustomObject正是通过useFieldSchema()拿到自身 schema,再用<RecursionField schema={schema} />渲染其子节点;配合onlyRenderProperties属性,还可以只渲染子属性而不渲染自身,这构成了自定义布局组件的标准范式:

const CustomObject2: React.FC = () => { const field = useField() const schema = useFieldSchema() return ( <RecursionField name={schema.name} basePath={field.address} schema={schema} onlyRenderProperties /> ) }

六、SchemaExpressionScopeContext:Schema 表达式作用域

JSON Schema 中的表达式(x-reactions依赖、x-component-props里的模板表达式等)在编译求值时需要一个“作用域”,SchemaExpressionScopeContext就是作用域对象的载体,类型为any,默认null

签名(见 context.ts)

export const SchemaExpressionScopeContext = createContext<any>(null)

提供者:ExpressionScope 组件

ExpressionScope.tsx 是核心 Provider,它读取外层作用域并通过lazyMerge与当前传入值合并后下发,因此作用域天然支持多层叠加:

export const ExpressionScope: ReactFC<IExpressionScopeProps> = (props) => { const scope = useContext(SchemaExpressionScopeContext) return ( <SchemaExpressionScopeContext.Provider value={lazyMerge(scope, props.value)}> {props.children} </SchemaExpressionScopeContext.Provider> ) }

RecordScope(提供$record$index$lookup)与RecordsScope在底层也复用ExpressionScope注入作用域,见 RecordScope.tsx。

消费方式:useExpressionScopeHook

useExpressionScope.ts 直接返回当前作用域对象。官方文档 useExpressionScope.md 给出了作用域的三种来源:createSchemaField顶层传入、SchemaField组件属性传入、以及自定义组件内部由ExpressionScope/RecordScope/RecordsScope下发。一个完整的消费示例:

import React from 'react' import { createForm } from '@formily/core' import { FormProvider, createSchemaField, useExpressionScope, RecordScope, } from '@formily/react' const form = createForm() const Custom = () => { const scope = useExpressionScope() return ( <code> <pre>{JSON.stringify(scope, null, 2)}</pre> </code> ) } const SchemaField = createSchemaField({ components: { Custom }, scope: { topScope: { aa: 123 } }, }) export default () => ( <FormProvider form={form}> <RecordScope getRecord={() => ({ name: 'Record Name', code: 'Record Code' })} getIndex={() => 2} > <SchemaField scope={{ propsScope: { bb: 321 } }}> <SchemaField.String name="custom" x-component="Custom" /> </SchemaField> </RecordScope> </FormProvider> )

RecursionField在把 schema 转换为字段 props 时,也会读取该作用域并传给schema.toFieldProps({ scope })(见 RecursionField.tsx),这也是x-reactions表达式能访问作用域变量的底层原因。

七、SchemaOptionsContext:SchemaField 工厂全局参数

SchemaOptionsContext保存createSchemaField(options)传入的全局参数,类型为ISchemaFieldReactFactoryOptions,目前包含两个字段(见 types.ts):

export interface ISchemaFieldReactFactoryOptions< Components extends SchemaReactComponents = any > { components?: Components // 组件映射表 scope?: any // 全局表达式作用域 }

签名(见 context.ts)

const SchemaOptionsContext = createContext<ISchemaFieldReactFactoryOptions>(null)

提供者:createSchemaField 生成的 SchemaField

在 SchemaField.tsx 中,createSchemaField会把工厂参数与组件级参数同时下发:

return ( <SchemaOptionsContext.Provider value={options}> <SchemaComponentsContext.Provider value={lazyMerge(options.components, props.components)} > <ExpressionScope value={lazyMerge(options.scope, props.scope)}> {renderMarkup()} {renderChildren()} </ExpressionScope> </SchemaComponentsContext.Provider> </SchemaOptionsContext.Provider> )

可见componentsscope都支持“工厂级默认 + 组件级覆盖”的合并策略。需要说明的是,文档列出的SchemaOptionsContext主要用于获取工厂参数;而源码中同级的SchemaComponentsContext(context.ts)则负责把 schema 中x-component/x-decorator声明的字符串映射到真实组件,ReactiveField.tsx 中FormPath.getIn(components, target)正是通过它完成字符串路径(如'Input.Password')到组件的解析,两者配合实现了 JSON Schema 驱动的组件注册机制。

八、基于 Context 体系的二次定制实战

掌握以上上下文后,最典型的落地场景是在自定义组件内同步感知字段状态。由于FormContextFieldContext的值由 Formily 的响应式模型驱动,配合@formily/react导出的observer(详见 observer.md),可以实现字段变化自动重渲染的个性化组件:

import React from 'react' import { observer } from '@formily/react' import { useField, useForm } from '@formily/react' export const CustomLabel = observer(() => { const field = useField() const form = useForm() return ( <div> <span>字段路径:{field.path.toString()}</span> <span>字段标题:{field.title}</span> <span>错误信息:{(field.selfErrors || []).join(', ')}</span> <span>表单校验中:{String(form.submitting)}</span> </div> ) })

若要进一步深入,建议配合阅读以下仓库文档与源码:

  • 文档:useForm.md、useField.md、useFieldSchema.md、useExpressionScope.md、useParentForm.md
  • 上下文定义:packages/react/src/shared/context.ts
  • 提供者组件:FormProvider.tsx、SchemaField.tsx、RecursionField.tsx、ExpressionScope.tsx
  • 消费 Hook:packages/react/src/hooks
  • 测试用例:field.spec.tsx、schema.markup.spec.tsx、expression.spec.tsx

九、小结

@formily/react的 Context 体系是“内核模型 ↔ 视图层”之间的标准通信协议:FormContextFieldContext提供实例访问,SchemaContextSchemaMarkupContext支撑 Schema 的双向转换,SchemaExpressionScopeContextSchemaOptionsContext完成表达式作用域与全局参数的传递,SchemaComponentsContext则补齐了字符串组件映射的能力。理解这些上下文的提供者与消费链路,是在 Formily 上做复杂个性化定制(自定义组件、递归渲染、多级作用域)的基础,也是阅读其他 API 文档时最值得优先掌握的一环。

  • 前端
  • 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),仅供参考

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

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

立即咨询