Formily Vue 的 connect 接入方案无侵入打通第三方组件库与 Formily 表单体系【免费下载链接】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 Vue 生态中connect是连接第三方组件库如 ant-design-vue、Element Plus 等与 Formily 表单状态管理核心的桥梁函数。它以无侵入为设计目标不需要修改第三方组件的源码只需通过组件映射器Mapper将 Field 的字段状态自动映射为组件的 props即可让任意 Vue 组件直接参与 Formily 的双向数据绑定、校验反馈与阅读态切换。本文将以 packages/vue/docs/api/shared/connect.md 为骨架结合 connect 源码实现、官方示例与单元测试完整讲解connect的签名、工作原理、内置映射器mapProps/mapReadPretty的用法以及如何在 Schema 场景与普通 Field 场景下落地。一、connect 是什么无侵入接入的设计初衷在 Formily 的 Vue 实现中Field是一个同时承载数据状态与UI 组件的复合抽象字段的值、校验结果、显隐、禁用、阅读态等状态由formily/core统一管理而具体的 UI 渲染则交给用户选择的第三方组件。问题在于第三方组件如ant-design-vue的Input、Form.Item并不认识 Formily 的字段状态协议——它们只认识value、onChange、placeholder这类原生 props。connect解决的就是这个鸿沟。正如文档描述所言它主要用于对第三方组件库的无侵入接入 Formily无侵入不改动第三方组件源码、不 fork、不包一层破坏原有 API 的壳可组合通过传入多个组件映射器Component Mapper对组件进行逐步加工每个映射器接收目标组件、返回一个新组件形成管道式pipe的加工链面向三方库通用既适用于 ant-design-vue也适用于 Element Plus 或任意符合 Vue 组件规范的库。从实现上看connect的核心逻辑位于 packages/vue/src/shared/connect.ts#L97-L125短短几十行即完成了管道装配与跨 Vue 2 / Vue 3 的适配底层依赖vue-demi保证同一份代码在两个大版本下都能运行。二、签名与类型一次读懂入参、映射器与返回值原文档给出了connect的类型签名packages/vue/docs/api/shared/connect.mdinterface IComponentMapperT extends Vue.Component { (target: T): Vue.Component } interface connectT extends Vue.Component { (target: T, ...args: IComponentMapperT[]): Vue.Component }拆解如下组成含义第一个参数target要接入 Formily 的第三方组件或任意 Vue 组件后续参数...args一个或多个组件映射器每个映射器都是(target) Vue.Component形式的纯函数按传入顺序对组件逐层加工返回值加工后的新组件可直接用于Field的component属性、decorator属性或注册进SchemaField的components中在仓库的实际类型定义中packages/vue/src/types/index.ts#L54-L56IComponentMapper被定义为export interface IComponentMapperT extends VueComponent any { (target: T): VueComponent }文档同时强调通常我们会使用内置的 mapProps 和 mapReadPretty 映射器。也就是说日常使用中你几乎不需要手写映射器——两个内置映射器已经覆盖了状态到 props 的映射与阅读态组件替换两大高频需求详见本文第三、四节。三、管道式装配connect 的实现原理Vue 2 / Vue 3 双版本适配阅读 connect 源码 可以看到它的实现非常轻量核心是reduce管道export function connectT extends VueComponent( target: T, ...args: IComponentMapper[] ): T { const Component args.reduce((target: VueComponent, mapper) { return mapper(target) }, target) /* istanbul ignore else */ if (isVue2) { const functionalComponent defineComponent({ functional: true, name: target.name, render(h, context) { return h(Component, context.data, context.children) }, }) return markRaw(functionalComponent) as T } else { const functionalComponent defineComponent({ name: target.name, setup(props, { attrs, slots }) { return () { return h(Component, { props, attrs }, slots) } }, }) return markRaw(functionalComponent) as T } }其中值得注意的实现细节映射器按顺序串联args.reduce把多个映射器依次应用到目标组件上前一个映射器的输出作为后一个映射器的输入。这意味着映射器是有顺序的例如connect(Input, mapProps(...), mapReadPretty(...))会先做状态映射再做阅读态替换。最终产物是一个极薄的功能组件connect返回的组件自身几乎不承载逻辑只负责把props / attrs / slots原样透传给加工后的Component从而保证对上层使用者透明——它在渲染树中不会引入多余层级也保留了原组件的name。Vue 2 / Vue 3 差异化适配在 Vue 2 下使用functional: true的函数式组件直接透传context.data与context.children在 Vue 3 下则用setup 渲染函数实现同样的透传最终均通过markRaw标记避免被响应式系统过度代理保证透传性能。透传的是props与attrs两套数据Vue 3 中声明的 props 走props未声明的属性走attrs二者合并后传给映射器加工最终由h渲染进目标组件。这也是为什么connect对第三方组件的 props 透传是无侵入且完整的。从测试用例也可以验证这一点packages/vue/src/tests/field.spec.ts#L419-L439 中分别用函数式组件、带name的BaseComponent、以及Input作为目标组件调用connect验证其都能正常渲染并接收映射后的 props。四、mapProps把 Field 状态映射成组件 propsconnect本身不产生任何状态映射逻辑真正负责Field 状态 → 组件 props转换的是内置的mapProps映射器其类型定义在 packages/vue/src/types/index.ts#L58-L62export type IStateMapperProps | { [key in keyof Field]?: keyof Props | boolean } | ((props: Props, field: GeneralField) Props)正如 mapProps 文档 所说明的它接受两种形式的参数可以同时传多个4.1 对象形式声明式属性映射mapProps({ title: label, // Field.title - 组件 props.label description: extra, // Field.description - 组件 props.extra required: true, // Field.required - 组件 props.requiredkey 相同value 为 true validateStatus: true, // Field.validateStatus - 组件 props.validateStatus })规则为key 是 Field 的属性名value 是目标组件的属性名若 value 为true表示映射后的属性名与 Field 属性名保持一致。4.2 函数形式命令式复杂映射mapProps((props, field) { return { ...props, help: field.selfErrors?.length ? field.selfErrors : undefined, } })函数形式接收当前已累积的 props和当前 Field 实例返回新的 props适合无法用简单键值对表达的映射例如根据field.selfErrors动态计算help内容。4.3 底层实现要点mapProps 源码 展示了其精确的取值逻辑对象形式下通过FormPath.getIn(field, extract)从 Field 中按路径取值当extract value且映射目标名不同时会删除原始props.value避免value与目标属性如list同时存在造成冲突当组件 props 上已有有效值而 Field 状态无效时isValid(originalValue) !isValid(extractValue)保留组件自有值防止映射覆盖外部传入的 props函数形式通过args.reduce依次合并进 props映射完成后返回的组件用observer来自formily/reactive-vue包裹保证 Field 状态变化时组件能响应式更新组件名会被规范为Connected${target.name}。测试 packages/vue/src/tests/field.spec.ts#L428-L433 展示了对象形式与函数形式混合使用的写法connect( { functional: true, props: [list], render(h, context) { return h(div, [context.props.list]) } }, mapProps({ value: list, loading: true }, (props, field) { return { ...props, mounted: field.mounted ? 1 : 2 } }), mapReadPretty({ render(h) { return h(div, read pretty) } }) )这里value: list将 Field 的value映射到组件的list属性loading: true则直接映射同名的loading属性。五、mapReadPretty一键补齐第三方组件的阅读态大多数第三方组件只提供编辑态不提供只读展示能力。mapReadPretty正是为此设计的传入一个阅读态组件当 Field 的pattern为readPretty时自动切换到该组件渲染否则仍渲染原组件。其核心判断逻辑在 mapReadPretty 源码const field fieldRef.value return h( field !isVoidField(field) field.pattern readPretty ? component // 阅读态组件 : target, // 原组件 { attrs: { ...readPrettyProps, ...attrs }, on: listeners }, slots )要点解读仅当字段不是VoidField且pattern readPretty时才切换为阅读态组件阅读态组件名会被规范为Read${target.name}可选的第二参数readPrettyProps会合并进阅读态组件的 attrs用于向阅读态组件传附加配置同样使用observer包裹阅读态切换是响应式的——当字段pattern从编辑态变为阅读态时无需手动刷新即可自动换组件。典型用法是把编辑组件与阅读态组件一并交给connect编排const PreviewInput connect( Input, mapProps({ value: true, placeholder: true }), mapReadPretty(PreviewText.Input) // 例如 Formily 内置的 PreviewText.Input )测试 packages/vue/src/tests/field.spec.ts#L434-L438 验证了mapReadPretty在readPretty模式下会渲染出 read pretty 内容packages/vue/src/tests/schema.json.spec.ts#L707-L739 则在 Schema 场景下同时组合mapProps与mapReadPretty验证 scoped slot 透传仍然正常。六、完整实战用 connect 把 ant-design-vue 接入 Formily原文档指向的官方示例位于 packages/vue/docs/demos/api/shared/connect.vue这里给出完整可运行的接入流程。6.1 定义映射后的组件import { Form, Input, Button } from ant-design-vue import { createForm, setValidateLanguage } from formily/core import { FormProvider, FormConsumer, Field, connect, mapProps, } from formily/vue setValidateLanguage(en) const FormItem connect( Form.Item, mapProps( { title: label, // Field.title - Form.Item 的 label description: extra, // Field.description - Form.Item 的 extra required: true, // 同名映射 validateStatus: true, // 同名映射 }, (props, field) { return { ...props, help: field.selfErrors?.length ? field.selfErrors : undefined, } } ) )注意这里既有对象形式title/description/required/validateStatus的声明式映射又有函数形式根据field.selfErrors动态计算help展示了两种形式的混合用法。Form.Item经过映射后可直接作为Field的decorator使用。6.2 在模板中使用template FormProvider :formform Form layoutvertical Field namename titleName required :decorator[FormItem] :component[Input, { placeholder: Please Input }] / FormConsumer template #default{ form } div stylewhite-space: pre; margin-bottom: 16px {{ JSON.stringify(form.values, null, 2) }} /div Button typeprimary clickform.submit(log) Submit /Button /template /FormConsumer /Form /FormProvider /templateField的component和decorator都支持组件 附加 props 数组的写法如[Input, { placeholder: Please Input }]。Input之所以能直接使用是因为 Formily 的Field内部对常规组件已具备基础的value/onChange协议支持而当第三方组件的 props 命名与 Field 状态不一致时例如把value叫list才需要借助connectmapProps显式映射。6.3 初始化表单并运行export default { components: { FormProvider, FormConsumer, Field, Form, Button }, data() { const form createForm({ validateFirst: true }) return { FormItem, Input, form } }, methods: { log(...args) { console.log(...args) }, }, }创建表单时开启validateFirst: true在required字段上即可体验提交时先校验、失败即中断的行为错误信息会经mapProps的函数形式映射到Form.Item的help中展示。七、connect 在 Schema 场景中的使用除了在 JSX/模板中直接使用connect产物最典型的消费方式是注册进createSchemaField的components中从而让 JSON Schema 的x-component直接引用const { SchemaField } createSchemaField({ components: { ConnectedComponent, // connect(...) 的产物 ConnectedWithMapReadPretty, // 组合 mapProps mapReadPretty 的产物 }, }) const schema new Schema({ type: string, name: ConnectedComponent, x-component: ConnectedComponent, })packages/vue/src/tests/schema.json.spec.ts#L643-L705 中验证了通过 Schema 渲染connect产物时scoped slotx-content仍能正常透传并渲染出预期内容123同文件 L707-L739 进一步验证了组合mapReadPretty后 scoped slot 依然完好。这说明connect的透传是完整无损的——attrs、slots 都会原样穿过映射层这也是它能作为 Schema 组件注册的关键。八、使用建议与注意事项综合文档、源码与测试在实际项目中接入connect时有以下几点值得留意映射器顺序即处理顺序connect(target, mapperA, mapperB)中mapperA先执行、mapperB后执行。若依赖某个映射器的产物务必确认其传入顺序。value: true与改名映射的差异目标组件 props 与 Field 属性同名时用true不同名时写{ 状态名: 组件属性名 }此时底层会自动删除残留的value以避免重复。组件自有 props 优先当外部显式传入的 props 值有效、而 Field 状态无值时mapProps会保留外部值因此不必担心映射逻辑覆盖你手动传入的占位属性。阅读态是响应式的mapReadPretty基于field.pattern readPretty实时判断配合observer自动切换无需手动监听 pattern 变化。不要用connect包装 Formily 自身组件它面向的是第三方组件库的无侵入接入Formily 内置组件如FormItem已经实现了状态协议重复包装反而可能引入不必要的映射开销。运行时依赖formily/reactive-vue与vue-demiconnect产物依赖observer与isVue2判断这意味着使用方环境需同时具备响应式观察与双版本适配能力这正是formily/vue作为统一入口所保证的。总结connect是 Formily Vue 接入第三方组件库的最小且完整的拼图它以目标组件 映射器管道的极简签名实现了对任意 Vue 组件的无侵入接入mapProps负责把 Field 状态声明式或命令式地映射为组件 propsmapReadPretty则一键补齐阅读态最终产物经observer包裹获得响应式能力并通过极薄的透传组件无缝融入普通Field与 JSON Schema 两种渲染场景。掌握connect就等于掌握了 Formily Vue 生态中任意组件皆可表单化的入口。【免费下载链接】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),仅供参考
