
前端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点击查看免费下载联动计算器Linkage Calculator是表单开发中的高频需求在填写表单的过程中根据已填字段实时计算、求值并汇总结果。本文以 Formily 2.x 为背景完整讲解如何借助 Schema 的x-reactions声明式联动能力实现「单价 × 数量 小计、各小计求和 总额」的表格计算器并深入 json-schema 包源码剖析dependencies、when、fulfill等字段的底层执行机制。读完本文你将掌握 Markup Schema 与 JSON Schema 两种写法的完整方案并能举一反三实现任意联动求值场景。为什么选择 x-reactions 实现联动计算器在 Formily 1.x 中实现这类「边填写边求值汇总」的需求成本非常高往往需要手动维护大量字段监听、副作用与状态同步代码。而在 Formily 2.x 中我们可以借助x-reactions轻松实现它把字段间的联动逻辑声明式地写进 Schema由框架自动建立依赖追踪、执行求值并回写字段状态无需手写任何事件监听与手动赋值代码。从 types.ts 中的SchemaReaction类型定义可以看到一条完整的 reaction 声明支持以下核心字段字段类型作用dependenciesArraystring \| object或Recordstring, string声明需要依赖的其他字段路径其当前值会被收集进$deps数组或具名对象whenstring \| boolean条件表达式为真时执行fulfill为假时执行otherwisetargetstring联动目标字段路径不声明时作用于当前字段自身effects生命周期类型数组触发时机默认[onFieldInit, onFieldValueChange]fulfill{ state?, schema?, run? }条件成立时的操作更新字段状态、打补丁 Schema 或执行脚本otherwise同fulfill条件不成立时的操作runstring一段可执行的函数体脚本运行时作用域包含$target等变量其中fulfill.state内以{{表达式}}形式书写的字符串会被编译为真正的 JS 表达式执行这正是计算器求值逻辑的载体。计算器核心机制dependencies 与 $deps实现计算器的第一步是声明依赖。从 transformer.ts 的getDependencies实现可以看到当dependencies是数组时每个字符串路径如.price、.count会通过field.query(target).getIn(...)取出对应字段的当前值按顺序放入$deps数组因此$deps[0]、$deps[1]与声明顺序一一对应依赖项也支持{ name, source, property }对象形式将取到的值挂到具名属性上当dependencies是对象时Recordstring, string会按 key 收集为具名依赖对象。随后在getUserReactions中框架会把$deps、$dependencies、$target、$self、$form、$values、$record、$index等变量合并进求值作用域再依次编译when与fulfill/otherwise。整个求值过程可概括为dependencies 收集依赖值 → 注入 $deps 作用域 → 编译 when 得到布尔条件 → 条件为真走 fulfill / 为假走 otherwise → setSchemaFieldState 回写状态表达式编译由 compiler.ts 完成字符串若匹配^\s*\{\{([\s\S]*)\}\}\s*$正则则提取内部表达式通过new Function($root, with($root) { return (expression); })在注入的作用域中求值非{{}}包裹的普通字符串则原样返回。这就是计算器公式{{$deps[0] * $deps[1]}}能够被真正执行的根本原因。另外shared.ts 中的SchemaStateMap展示了 Schema 属性到字段状态的映射关系x-pattern → pattern、x-value → value、x-read-pretty → readPretty等。因此fulfill.state中既可以写value也可以写visible、disabled、pattern等任意字段状态联动能力远不止求值。Markup Schema 案例单价 × 数量 小计汇总为总额先看 Markup Schema组件式 JSX Schema写法。该案例构建了一张项目明细表ArrayTable每一行包含可编辑的「价格 price」与「数量 count」通过x-reactions实时计算「小计 total」表格下方再对全部小计求和得到「总额 total」import React from react import { Form, FormItem, NumberPicker, ArrayTable, Editable, Input, FormButtonGroup, Submit, } from formily/antd import { createForm } from formily/core import { createSchemaField } from formily/react const SchemaField createSchemaField({ components: { FormItem, Editable, Input, NumberPicker, ArrayTable, }, }) const form createForm() export default () { return ( Form form{form} layoutvertical SchemaField SchemaField.Array nameprojects titleProjects x-decoratorFormItem x-componentArrayTable SchemaField.Object SchemaField.Void x-componentArrayTable.Column x-component-props{{ width: 50, title: Sort, align: center }} SchemaField.Void x-decoratorFormItem x-componentArrayTable.SortHandle / /SchemaField.Void SchemaField.Void x-componentArrayTable.Column x-component-props{{ width: 80, title: Index, align: center }} SchemaField.String x-decoratorFormItem x-componentArrayTable.Index / /SchemaField.Void SchemaField.Void x-componentArrayTable.Column x-component-props{{ title: Price }} SchemaField.Number nameprice x-decoratorEditable required x-componentNumberPicker x-component-props{{ addonAfter: $, }} default{0} / /SchemaField.Void SchemaField.Void x-componentArrayTable.Column x-component-props{{ title: Count }} SchemaField.Number namecount x-decoratorEditable required x-componentNumberPicker default{0} / /SchemaField.Void SchemaField.Void x-componentArrayTable.Column x-component-props{{ title: Total }} SchemaField.Number x-decoratorFormItem nametotal x-componentNumberPicker x-patternreadPretty x-component-props{{ addonAfter: $, }} x-reactions{{ dependencies: [.price, .count], when: {{$deps[0] $deps[1]}}, fulfill: { state: { value: {{$deps[0] * $deps[1]}}, }, }, }} / /SchemaField.Void SchemaField.Void x-componentArrayTable.Column x-component-props{{ title: Operations, dataIndex: operations, width: 200, fixed: right, }} SchemaField.Void x-componentFormItem SchemaField.Void x-componentArrayTable.Remove / SchemaField.Void x-componentArrayTable.MoveDown / SchemaField.Void x-componentArrayTable.MoveUp / /SchemaField.Void /SchemaField.Void /SchemaField.Object SchemaField.Void x-componentArrayTable.Addition titleAdd / /SchemaField.Array SchemaField.Number nametotal titleTotal x-decoratorFormItem x-componentNumberPicker x-component-props{{ addonAfter: $, }} x-patternreadPretty x-reactions{{ dependencies: [.projects], when: {{$deps[0].length 0}}, fulfill: { state: { value: {{$deps[0].reduce((total,item)item.total ? totalitem.total : total,0)}}, }, }, }} / /SchemaField FormButtonGroup Submit onSubmit{console.log}提交/Submit /FormButtonGroup /Form ) }关键点逐段拆解行内小计total的联动dependencies: [.price, .count].price是相对当前字段projects[].total的相对路径指代同一条记录中的price字段.count同理。两值分别注入$deps[0]与$deps[1]。相对路径写法以.开头非常适合表格行内联动它让每条数据记录拥有各自独立的依赖关系互不串行when: {{$deps[0] $deps[1]}}仅当价格与数量均为真值非 0、非空时才触发计算避免无效求值fulfill.state.value: {{$deps[0] * $deps[1]}}价格 × 数量写入当前字段 valuex-patternreadPretty小计字段只读展示渲染为纯文本不允许用户手工编辑防止计算结果被覆盖。表格汇总total的联动dependencies: [.projects]依赖整个projects数组$deps[0]即全部项目记录when: {{$deps[0].length 0}}数组非空才执行求和fulfill.state.value中通过原生Array.prototype.reduce累加每条记录的小计item.total{{$deps[0].reduce((total,item)item.total ? totalitem.total : total,0)}}。由于total字段的值由行内 reaction 保证汇总表达式可以直接消费item.total实现两级联动的天然衔接。两个联动都未声明target因此默认作用于自身字段符合「自己算自己」的语义若想「改了 A 联动 B」只需在 reaction 中声明target: 路径并配合fulfill.schema或fulfill.state即可。JSON Schema 案例同一能力的纯数据化表达如果表单 Schema 需要从服务端下发、存入数据库或由低代码平台动态生成则应使用纯 JSON Schema 写法。它与 Markup Schema 表达的是完全相同的计算逻辑仅将 JSX 结构转换为type: object / array / void与x-*属性组合import React from react import { Form, FormItem, NumberPicker, ArrayTable, Editable, Input, FormButtonGroup, Submit, } from formily/antd import { createForm } from formily/core import { createSchemaField } from formily/react const SchemaField createSchemaField({ components: { FormItem, Editable, Input, NumberPicker, ArrayTable, }, }) const form createForm() const schema { type: object, properties: { projects: { type: array, title: Projects, x-decorator: FormItem, x-component: ArrayTable, items: { type: object, properties: { column_1: { type: void, x-component: ArrayTable.Column, x-component-props: { width: 50, title: Sort, align: center, }, properties: { sortable: { type: void, x-component: ArrayTable.SortHandle, }, }, }, column_2: { type: void, x-component: ArrayTable.Column, x-component-props: { width: 50, title: Index, align: center, }, properties: { index: { type: void, x-component: ArrayTable.Index, }, }, }, column_3: { type: void, x-component: ArrayTable.Column, x-component-props: { title: Price, }, properties: { price: { type: number, default: 0, x-decorator: Editable, x-component: NumberPicker, x-component-props: { addonAfter: $, }, }, }, }, column_4: { type: void, x-component: ArrayTable.Column, x-component-props: { title: Count, }, properties: { count: { type: number, default: 0, x-decorator: Editable, x-component: NumberPicker, x-component-props: { addonAfter: $, }, }, }, }, column_5: { type: void, x-component: ArrayTable.Column, x-component-props: { title: Total, }, properties: { total: { type: number, x-read-pretty: true, x-decorator: FormItem, x-component: NumberPicker, x-component-props: { addonAfter: $, }, x-reactions: { dependencies: [.price, .count], when: {{$deps[0] $deps[1]}}, fulfill: { state: { value: {{$deps[0] * $deps[1]}}, }, }, }, }, }, }, column_6: { type: void, x-component: ArrayTable.Column, x-component-props: { title: Operations, }, properties: { item: { type: void, x-component: FormItem, properties: { remove: { type: void, x-component: ArrayTable.Remove, }, moveDown: { type: void, x-component: ArrayTable.MoveDown, }, moveUp: { type: void, x-component: ArrayTable.MoveUp, }, }, }, }, }, }, }, properties: { add: { type: void, title: Add, x-component: ArrayTable.Addition, }, }, }, total: { type: number, title: Total, x-decorator: FormItem, x-component: NumberPicker, x-component-props: { addonAfter: $, }, x-pattern: readPretty, x-reactions: { dependencies: [.projects], when: {{$deps[0].length 0}}, fulfill: { state: { value: {{$deps[0].reduce((total,item)item.total ? totalitem.total : total,0)}}, }, }, }, }, }, } export default () { return ( Form form{form} layoutvertical SchemaField schema{schema} / FormButtonGroup Submit onSubmit{console.log}submit/Submit /FormButtonGroup /Form ) }与 Markup Schema 写法的对应关系SchemaField.Array↔type: arrayx-component: ArrayTable其items对应SchemaField.Object每个表格列对应一个type: void的x-component: ArrayTable.Column节点列内容以properties嵌套表达x-patternreadPretty在 JSON 中写作x-pattern: readPretty或等价的x-read-pretty: true行内 total 用的就是后者x-reactions对象结构完全一致dependencies、when、fulfill.state.value均原样保留求值表达式无需任何改动。两种写法由 transformer.ts 统一转换为字段的reactions数组getBaseReactions打底 getUserReactions注入用户 reaction因此运行时行为完全等价你可以在团队中按「Schema 是否需要序列化」自由选择。源码级执行流程一条 reaction 如何跑起来结合 transformer.ts 的getUserReactions可以完整还原本例的运行时链路注册createSchemaField解析 Schema 时x-reactions会被toArr包装为 reaction 数组每个元素生成一个以field为入参的回调触发时机未显式声明effects时默认注册[onFieldInit, onFieldValueChange]对应DefaultFieldEffects即字段初始化时执行一次、依赖字段值每次变化时执行一次声明了target时还会采用同样的默认效果收集依赖getDependencies(field, reaction.dependencies)按声明路径查询字段并组装$deps合并作用域将$deps、$dependencies、$target此时为null与getBaseScope提供的$self、$form、$values、$record、$index等合并条件编译shallowCompile(when, scope)编译when当when未声明时条件恒为true选择分支条件为真取fulfill为假取otherwise状态回写setSchemaFieldState依据target是否存在选择调用field.form.setFieldState(target, ...)修改目标字段或field.setState(...)修改自身state通过patchCompile深度编译{{}}表达式后写入对应状态。值得注意的细节是shared.ts 中的SchemaStateMap决定了fulfill.state中可写的字段范围value、visible、display、pattern、disabled、editable、dataSource、validator、componentProps等均可通过fulfill.state动态设置。这意味着同一个机制不仅能做数值计算还能实现「满足条件才显示字段」「切换数据源」「动态禁用/只读」等更丰富的联动场景。实战注意事项相对路径依赖表格行内计算务必使用以.开头的相对路径如.price这样每条记录都会按自身数据求值写绝对路径如projects.0.price则只能命中固定下标无法适配动态增删行。默认值与空值保护when: {{$deps[0] $deps[1]}}这类守卫条件可以避免价格或数量为空时把total算成NaN数组汇总同理用$deps[0].length 0做保护reduce 回调中也应保留item.total ? ... : ...的容错写法。只读展示计算结果字段用x-patternreadPretty或x-read-pretty: true设为只读配合ArrayTable使用时可直接在表格内以纯文本渲染防止用户覆盖计算结果。依赖收集是响应式的getDependencies读取依赖字段值的过程发生在响应式追踪上下文内因此后续依赖字段任何一次值变化都会自动重新触发整条 reaction 链这正是「边填写边汇总」无需手动刷新的原理相关机制可参考 reactive 包 的autorun与reaction实现。组件与装饰器注册案例中Editable、NumberPicker、ArrayTable等必须通过createSchemaField的components注册否则 Schema 渲染会因找不到组件而报错ArrayTable的列、排序、索引、增删按钮等能力均来自 array-table 的实现。小结联动计算器是 Formily 2.x 声明式联动能力的典型应用。本文基于 calculator.md 给出的完整案例分别以 Markup Schema 与 JSON Schema 两种方式实现了「单价 × 数量 小计 → 汇总总额」的表格计算器并从 transformer.ts、compiler.ts 源码层面解释了dependencies、$deps、when、fulfill的执行链路。掌握这一模式后你可以将同一套 reaction 语法迁移到字段显隐控制、动态数据源、跨表汇总、审批流联动等任意表单联动场景中。赞分享前端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点击查看免费下载相关推荐Cerbero-7b与其他意大利语模型对比Camoscio、Fauno全面评测 Cerbero 7b与其他意大利语模型对比Camoscio、Fauno全面评测 在意大利语人工智能领域 Cerbero 7b 作为首个完全开源免费的意Formily 2.x 卡片数组组件 ArrayCards 实战指南基于 formily/next 的动态列表表单开发Formily 2.x 卡片数组组件 ArrayCards 实战指南基于 formily/next 的动态列表表单开发 本指南围绕 formily/nex前端UI组件vCluster 端到端测试体系深度解析基于 Ginkgo v2 的 e2e 测试套件架构与实战指南vCluster 端到端测试体系深度解析基于 Ginkgo v2 的 e2e 测试套件架构与实战指南 vCluster 是一个 CNCF 认证的 Kubern云原生集群管理虚拟化多集群创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考