ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Formily 路径解构(Path Destructor):前后端数据差异兼容方案实战与原理解析

Formily 路径解构(Path Destructor):前后端数据差异兼容方案实战与原理解析 Formily 路径解构Path Destructor前后端数据差异兼容方案实战与原理解析【免费下载链接】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/formilyFormily 是阿里巴巴开源的高性能表单解决方案其内置的**路径解构Path Destructor**能力允许开发者把一条字段路径直接写成[startDate,endDate]或{aa,bb}这样的解构表达式从而让表单数据的存取自动完成数组 ↔ 扁平对象之间的重组。本篇文章以官方高级指南《前后端数据差异兼容方案》为骨架结合formily/path的解析器与存取器源码、formily/core的表单模型集成以及对应测试用例帮助你彻底掌握这一能力并能在 Markup Schema、JSON Schema、纯 JSX 三种写法中直接落地。痛点前端组件结构与后端领域模型的结构差异前后端联调中前端数据结构与后端数据结构不匹配是出现频率最高的问题之一最典型的场景就是日期范围组件前端日期范围组件如 DatePicker.RangePicker输出的天然是数组结构例如[2020-11-20, 2021-12-30]后端领域模型为了查询、索引与落库方便往往要求拆分后的扁平结构例如{ startDate: 2020-11-20, endDate: 2021-12-30 }。从后端模型设计角度看拆分扁平结构是最佳方案从前端组件化角度看数组结构又是最佳方案。两边各有其道理但以往只能由前端在提交前手动消化这种不平等条约——写一堆拆装、拼接、回填的胶水代码。Formily 给出的解法是提供路径解构的能力在字段路径层面直接声明这个数组要拆成哪几个键从而让拆分与重组在数据存取链路中自动完成业务代码零胶水。这一能力的完整定义与示例见 docs/guide/advanced/destructor.md及其中文版 docs/guide/advanced/destructor.zh-CN.md。核心思路把解构表达式当作一个字段路径节点理解路径解构只需抓住一条规则解构表达式是点路径a.b.c中的一个普通节点只不过在数据操作读取/写入时会额外生效。官方 FormPath 文档 明确指出解构表达式会被当作点路径的一个节点可以像普通字符串节点一样参与匹配语法在setIn中使用解构路径数据会被解构从复合结构拆成扁平结构在getIn中使用解构路径数据会被重组从扁平结构拼回复合结构。因此在表单字段上只需要把name写成name[startDate,endDate]字段组件仍然输出数组[2020-11-20, 2021-12-30]但当 Formily 把该字段值写入form.values时[startDate,endDate]会被解析成两条规则startDate ← 数组第 0 位、endDate ← 数组第 1 位最终提交给后端的数据就自动变成了扁平对象{ startDate: 2020-11-20, endDate: 2021-12-30 }在 FormPath 文档 中同样给出了最简洁的存取示例import { FormPath } from formily/core const target {} FormPath.setIn(target, parent.[aa,bb], [11, 22]) console.log(target) //{parent:{aa:11,bb:22}} console.log(FormPath.getIn(target, parent.[aa,bb])) //[11,22] console.log(FormPath.parse(parent.[aa,bb]).toString()) //parent.[aa,bb]案例一Markup Schema 中的路径解构官方指南的第一个案例采用 Markup Schema 写法完整代码如下来自 destructor.mdimport React from react import { Form, FormItem, DatePicker, FormButtonGroup, Radio, Submit, } from formily/antd import { createForm, onFieldValueChange } from formily/core import { createSchemaField, FormConsumer } from formily/react const SchemaField createSchemaField({ components: { FormItem, DatePicker, Radio, }, }) const form createForm({ effects() { onFieldValueChange(visible_destructor, (field) { form.setFieldState([startDate,endDate], (state) { state.visible !!field.value }) }) }, }) export default () { return ( Form form{form} layoutvertical SchemaField SchemaField.Boolean namevisible_destructor title是否显示解构字段 default{true} enum{[ { label: 是, value: true }, { label: 否, value: false }, ]} x-decoratorFormItem x-componentRadio.Group / SchemaField.String nameundestructor title解构前 x-decoratorFormItem x-componentDatePicker.RangePicker / SchemaField.String name[startDate,endDate] title解构后 default{[2020-11-20, 2021-12-30]} x-decoratorFormItem x-componentDatePicker.RangePicker / /SchemaField code pre FormConsumer {(form) JSON.stringify(form.values, null, 2)} /FormConsumer /pre /code FormButtonGroup Submit onSubmit{console.log}提交/Submit /FormButtonGroup /Form ) }这个案例值得拆解的关键点对照组设计undestructor解构前与[startDate,endDate]解构后都使用x-componentDatePicker.RangePicker渲染同样的日期范围组件唯一区别是字段name。通过下方FormConsumer实时渲染的form.valuesJSON可以直观看到前者在 values 中保留数组undestructor: [2020-11-20, 2021-12-30]后者则被拆成startDate与endDate两个扁平键。字段联动visible_destructor布尔字段用于控制解构字段的显示/隐藏。effects()中通过onFieldValueChange(visible_destructor, ...)监听变化再用form.setFieldState([startDate,endDate], ...)直接以解构路径命中目标字段并切换visible状态——解构路径在字段查询/匹配语法中同样作为普通节点生效。默认值default{[2020-11-20, 2021-12-30]}以数组形式提供说明解构发生的位置在值写入表单模型这一层与组件本身接受数组输入完全不冲突。提交验证Submit onSubmit{console.log}会在提交时打印form.values可直接确认后端拿到的就是扁平结构。提示真实项目中 RangePicker 输出的往往是 dayjs/Moment 对象而非日期字符串文档为便于演示直接使用字符串。解构机制只关心数组的第 N 位映射到哪个键与元素的具体类型无关。案例二JSON Schema 中的路径解构如果表单结构以纯 JSON 数据驱动例如由后端下发或设计器产出可以改用 JSON Schema 写法import React from react import { Form, FormItem, DatePicker, FormButtonGroup, Radio, Submit, } from formily/antd import { createForm } from formily/core import { createSchemaField, FormConsumer } from formily/react const SchemaField createSchemaField({ components: { FormItem, DatePicker, Radio, }, }) const form createForm() const schema { type: object, properties: { visible_destructor: { type: boolean, title: 是否显示解构字段, default: true, enum: [ { label: 是, value: true }, { label: 否, value: false }, ], x-decorator: FormItem, x-component: Radio.Group, }, undestructor: { type: string, title: 解构前, x-decorator: FormItem, x-component: DatePicker.RangePicker, }, [startDate,endDate]: { type: string, title: 解构后, default: [2020-11-20, 2021-12-30], x-decorator: FormItem, x-component: DatePicker.RangePicker, x-reactions: { dependencies: [visible_destructor], fulfill: { state: { visible: {{!!$deps[0]}}, }, }, }, }, }, } export default () { return ( Form form{form} layoutvertical SchemaField schema{schema} / code pre FormConsumer {(form) JSON.stringify(form.values, null, 2)} /FormConsumer /pre /code FormButtonGroup Submit onSubmit{console.log}提交/Submit /FormButtonGroup /Form ) }与案例一相比本案例的差异点解构字段名直接出现在properties的键上[startDate,endDate]作为 schema 属性名。JSON Schema 的对象键可以是任意字符串因此完全合法且无需任何转义。联动改用x-reactions声明式表达dependencies: [visible_destructor]声明依赖fulfill.state.visible: {{!!$deps[0]}}用表达式把依赖值取反转为显隐布尔值。这比在effects()中手写监听更内聚也更适合可序列化的 schema 场景。其余部分default数组默认值、FormConsumer预览、Submit提交与案例一完全一致验证结论相同。案例三纯 JSX 中的路径解构不依赖 Schema 时直接用Field组件也可以获得完全一致的解构能力import React from react import { Form, FormItem, DatePicker, FormButtonGroup, Radio, Submit, } from formily/antd import { createForm } from formily/core import { Field, FormConsumer } from formily/react const form createForm() export default () { return ( Form form{form} layoutvertical Field namevisible_destructor title是否显示解构字段 initialValue{true} dataSource{[ { label: 是, value: true }, { label: 否, value: false }, ]} decorator{[FormItem]} component{[Radio.Group]} / Field nameundestructor title解构前 decorator{[FormItem]} component{[DatePicker.RangePicker]} / Field name[startDate,endDate] title解构后 initialValue{[2020-11-20, 2021-12-30]} decorator{[FormItem]} component{[DatePicker.RangePicker]} reactions{(field) { field.visible !!field.query(visible_destructor).value() }} / code pre FormConsumer {(form) JSON.stringify(form.values, null, 2)} /FormConsumer /pre /code FormButtonGroup Submit onSubmit{console.log}提交/Submit /FormButtonGroup /Form ) }要点说明name[startDate,endDate]是唯一解构入口decorator、component、initialValue等属性与其他字段无任何差别。联动改回命令式reactionsfield.query(visible_destructor).value()读取依赖字段值再赋值field.visible。这展示了同一联动逻辑在三种写法effects / x-reactions / reactions中的等价表达。三种写法的form.values结果完全一致证明路径解构属于 Formily 表单模型/路径引擎层面的通用能力与上层写法Schema 或 JSX无关。解构表达式的语法能力全景官方指南只演示了[startDate,endDate]这一种数组模式但 FormPath 文档 与 路径包测试用例 展示了更完整的语法整理如下语法含义示例与结果[aa,bb]数组解构模式按下标位置映射键名Path.setIn({}, aa.bb.ddd.[aa,bb], [123, 444])→{ aa: { bb: { ddd: { aa: 123, bb: 444 } } } }{aa,bb}对象解构模式按同名键重组Path.setIn({}, a.b.c.{aaa,bbb}, { aaa: 123, bbb: 321 })→{ a: { b: { c: { aaa: 123, bbb: 321 } } } }{c:kk,d:mm}对象模式 键别名源数据kk/mm重组为c/dPath.getIn({ a: { b: { kk: 2, mm: 333 } } }, a.b.{c:kk,d:mm})→{ c: 2, d: 333 }复杂嵌套对象/数组模式可任意嵌套{aa:{bb:{cc:destructor1,dd:[destructor2,destructor3],ee}}}可将深层嵌套结构一次性摊平为四个扁平键路径前缀解构节点可挂在任意点路径末端a.b.c.[aaa,bbb]、aa.bb.ddd.[{cc:aa,bb}]等保留多余字段写入时不会清空目标对象中未声明的键源对象含kkk: ddd时解构写入后kkk依然保留数组下标数组模式的键可混合对象模式a.b.c.[{ddd,kkk:mmm},bbb]把第 0 位对象拆开后与bbb一起摊平几点边界说明均有源码与测试支撑不支持...展开解构表达式语法上类似 ES6 解构但不支持 rest/spread见 FormPath 文档。deleteIn同样支持解构路径Path.deleteIn({ a: 1, b: 2 }, { a })→{ b: 2 }Path.deleteIn([1, 2], [0])→[undefined, 2]。existIn支持解构路径仅当解构表达式声明到的所有键都存在时才返回true见 accessor.spec.ts 中existIn相关用例。匹配语法无需转义FormPath.parse(target.[aa,bb]).match(target.[aa,bb])直接返回true解构表达式在 match 测试 中也有专门的组合匹配用例。源码原理从词法解析到数据存取的完整链路路径解构的底层实现位于formily/path包理解它可以帮你判断什么场景能用、什么写法会出问题。1. 解析阶段把解构表达式编译为规则并缓存入口在 parser.ts 的parseDestructorExpressionparser.ts当解析器遇到{/[起始的 token 时进入解构上下文destructorContext分别调用parseObjectPatternparser.ts或parseArrayPatternparser.ts生成 AST随后把整个解构表达式的规范化字符串作为一个单独的 path segment压入segments。真正产出拆装规则的是 destructor.ts 中的parseDestructorRulesdestructor.ts。它递归遍历 AST为每个叶子生成一条{ key, path }规则对象模式{aa,bb}key取自属性名若值是标识符如{c:kk}中的kkkey会被替换为别名kk同时path记录真实读取位置数组模式[aa,bb]key先取下标再被标识符名覆盖为aa、bbpath记录0、1嵌套子规则会与父路径basePath.concat(rule.path)拼接形成完整读取路径。解析完成后规则被写入DestructorCachedestructor.ts后续同字符串的路径直接命中缓存避免重复解析。2. 存取阶段getIn / setIn 的分流路径引擎 index.ts 中getInindex.ts与setInindex.ts遍历 segments 时会先getDestructor(index)检查当前节点是否为解构规则命中则转交解构器setInByDestructordestructor.ts对每条规则执行mutators.setIn([key], source, mutators.getIn(path, value))——即先从待写入值中按path取出对应位置的数据再写到目标对象以key命名的键上。这就是[startDate,endDate]把数组第 0/1 位拆成两个键的过程。getInByDestructordestructor.ts反向操作先根据首条规则首段是否为数字决定重组目标是数组还是对象再对每条规则执行mutators.setIn(path, response, source[key])——把扁平键重新拼回数组/对象。deleteInByDestructordestructor.ts与existInByDestructordestructor.ts同理前者按key逐个删除后者要求所有key都存在才返回true。四种操作的 mutatorgetIn/setIn/deleteIn/existIn由 index.ts 递归提供因此解构节点与普通点路径节点可以无缝混排、任意嵌套。3. 与表单模型的集成form.values 的读写全部走路径引擎formily/core的表单模型把对form.values的所有存取都委托给FormPathForm.ts 中的setValuesIn/getValuesIn/setInitialValuesIn/getInitialValuesIn等方法Form.ts直接调用FormPath.setIn/FormPath.getIn字段模型的取值与回写最终都落在 Field.ts 的this.form.getValuesIn(this.path)Field.ts与this.form.setValuesIn(this.path, value)Field.ts上。由于this.path就是字段的name所以只要name带解构表达式字段值的写入/读取就会被自动拆装。核心模型对此有专门测试field.spec.ts 中的field destructor path with display none用例field.spec.ts创建了name: [aa,bb]的数组字段并执行setDisplay(none)断言form.values为{}、字段值为[]证明解构路径在字段生命周期创建、显隐、值收集中全程生效。4. 测试佐证解构行为的行为契约accessor.spec.ts 是理解解构行为的最佳活文档以下关键断言均可作为行为契约// 解构读取把扁平对象重组为数组 const value { array: [{ aa: 123, bb: 321 }] } expect(getIn(value, array.0.[aa,bb])).toEqual([123, 321]) // 解构写入把数组拆成扁平对象 expect(setIn({}, [aa,bb], [123, 444])).toEqual({ aa: 123, bb: 444 }) // 对象模式 别名 expect(getIn({ a: { b: { kk: 2, mm: 333 } } }, a.b.{c:kk,d:mm})).toEqual({ c: 2, d: 333, }) // 写入时保留源对象多余字段 expect( Path.setIn({ a: { b: { c: { kkk: ddd } } } }, a.b.c.{aaa,bbb}, { aaa: 123, bbb: 321, }) ).toEqual({ a: { b: { c: { aaa: 123, bbb: 321, kkk: ddd } } } }) // 复杂嵌套深层结构一次摊平 expect( Path.setIn( {}, {aa:{bb:{cc:destructor1,dd:[destructor2,destructor3],ee}}}, { aa: { bb: { cc: 123, dd: [333, 444], ee: abcde } } } ) ).toEqual({ destructor1: 123, destructor2: 333, destructor3: 444, ee: abcde })实践建议与常见注意事项明确拆装方向提交给后端需要扁平结构、而组件输出数组时字段name用数组模式[key1,key2]需要把扁平对象重组为对象/数组再回显时读取方用{a,b}或[a,b]模式即可。getIn重组与setIn解构互为逆操作这一对称性是整个设计的精髓。解构节点是单一 path segment[startDate,endDate]被视为一个节点而非两个因此字段联动、查询field.query(...)、setFieldState等所有基于路径的功能都可以直接使用无需额外转义。三种写法等价Markup Schema 的name、JSON Schema 的properties键、纯 JSX 的Field.name是同一入口联动逻辑可选用effects/x-reactions/reactions任意一种不影响解构结果。不要在解构路径中使用...当前实现不支持展开语法复杂嵌套请使用显式的对象/数组模式组合。结合 FormPath 学习路径解构只是 FormPath 能力的一部分点路径、下标路径、相对路径如[1]表示当前下标 1、通配匹配、分组/范围/反向匹配等语法详见 FormPath 文档阅读时注意区分数据操作路径与匹配路径两类语法。结语路径解构是 Formily 处理前后端数据差异的零成本方案不写胶水代码、不引入额外依赖只需在字段name中声明解构表达式数据在进出form.values的瞬间即完成拆装。其实现横跨 parser.ts 的语法解析、destructor.ts 的规则编译与四类存取操作、index.ts 的路径引擎以及 Form.ts / Field.ts 的表单模型委托链路并有 accessor.spec.ts 与 field.spec.ts 两层测试兜底。掌握了本文的三段式案例与源码链路你就能在真实项目中自信地把前后端数据结构不一致从痛点清单里划掉。【免费下载链接】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),仅供参考
返回列表