ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

amis 数据域与数据链:JSON 配置中的数据作用域、查找链路与更新机制完全指南

amis 数据域与数据链:JSON 配置中的数据作用域、查找链路与更新机制完全指南 amis 数据域与数据链JSON 配置中的数据作用域、查找链路与更新机制完全指南【免费下载链接】amis前端低代码框架通过 JSON 配置就能生成各种页面。项目地址: https://gitcode.com/GitHub_Trending/am/amis数据域Data Scope与数据链Data Chain是 amis 低代码框架数据体系的两大基石数据域决定某个组件能读到哪些数据数据链决定读不到时该向谁要。本文以官方概念文档 docs/zh-CN/concepts/datascope-and-datachain.md 为骨架结合 amis-core 源码与仓库内可运行示例系统讲解数据域初始化、数据链查找规则、trackExpression精细追踪、隐藏字段与 URL 参数等机制帮助你在 JSON 配置中准确控制数据来源与刷新范围。从一个问题开始固定文本如何变成接口数据绝大多数 amis 页面都从这样一段最小配置起步{ type: page, body: Hello World! }它只是在Page组件的内容区渲染了一串固定文本。真正的问题是如何通过接口拉取数据并展示到Page组件的内容区答案是给Page配置initApi让组件在初始化时自动请求接口{ type: page, initApi: /api/mock2/page/initData, body: date is ${date} }接口返回的数据结构约定如下/api/mock2/page/initData在仓库 mock 服务 mock/cfc/mock/page 目录下可找到对应实现{ status: 0, msg: , data: { title: Test Page Component, date: 2017-10-13 } }渲染后页面上会显示date is 2017-10-13。这里发生了一次完整的数据闭环组件初始化时amis 按initApi配置发起请求请求成功后Page把返回结构中的data字段内容存入当前组件的数据域渲染body时amis 解析模板字符串发现${date}模板变量到当前组件数据域中取出date的值2017-10-13替换后完成渲染。body属性自身支持模板语法下一节可继续阅读模板。在 amis 中支持模板语法的组件还有很多数据域正是这些模板变量取值的统一来源。数据域组件树上的数据作用域数据域是 amis 中最重要的概念之一。它本质上是一份key-value形式的数据集合隶属于某个组件实例决定了该组件及其后代组件在渲染时能看见哪些变量。通过一个最简单的例子来建立直觉{ type: page, body: Hello ${text} }${text}是模板变量渲染时 amis 会到当前Page组件的数据域中查找text变量。由于当前数据域没有任何数据${text}会被解析为空白文本最终渲染结果是Hello。对比之前配置了initApi的示例两者差别仅在于初始化接口会把返回数据写入数据域供组件使用。再看下面这段配置——通过data属性显式声明数据域{ data: { text: World! }, type: page, body: Hello ${text} }渲染结果顺利输出Hello World!。由此可以得到一个关键结论组件的data属性值是数据域的一种形式。即使不显式配置也可以假想每个组件都带有一个空数据域{ data: {}, type: page, body: Hello ${text} }从源码实现看数据域并非简单对象。amis-core 中的createObject见 packages/amis-core/src/utils/object.ts通过Object.create(superProps)让子数据域以父数据域为原型并挂载一个不可枚举的__super指针指向上一级数据域同时将本级props的键值直接拷贝到对象上。这意味着取值时可以沿原型链向上取到父级数据枚举对象时又不会一次性把所有上层数据全部拉出来。这正是数据链在代码层面最直接的体现。数据链取不到变量时的向上查找规则amis 基于组件树构建页面因此数据域天然形成树型结构。数据链描述的就是这些数据域之间的联系以及当前组件在遇到获取变量场景模板渲染、展示表单数据、渲染列表等时的查找规则首先在当前组件的数据域中寻找变量找到后通过数据映射完成渲染停止寻找当前数据域没找到时向上到父组件数据域重复步骤 1 和 2一直找到顶级节点通常是page节点寻找过程结束如果 URL 中有参数还会继续向上查找这一层——所以很多时候可以直接用${id}取地址栏参数详见下文URL 参数小节。本章示例统一使用data属性来初始化数据域。请记住只要组件支持你永远可以通过接口来进行数据域的初始化二者是等价的两种手段。一个完整的查找过程演示以下配置形成了如下的组件树与数据链{ type: page, data: { name: zhangsan, age: 20 }, body: [ { type: tpl, tpl: my name is ${name} }, { type: service, data: { name: lisi }, body: { type: tpl, tpl: my name is ${name}, Im ${age} years old } } ] }组件树page ├─ tpl └─ service └─ tpl数据链__sub字段只是为了方便理解并非真实 API{ name: zhangsan, age: 20, __sub: { name: lisi } }渲染过程分两条路径第一个tpl渲染my name is ${name}在page数据域中找到name zhangsan查找结束输出my name is zhangsanservice内的tpl渲染my name is ${name}, Im ${age} years old先在service数据域中找name命中lisi该变量查找结束找age时在service数据域中失败于是沿数据链向上在page数据域中命中age 20最终输出my name is lisi, Im 20 years old。注意本例中获取数据使用的是${xxx}模板语法。不同组件配置项中获取数据的语法会有差异后续可在模板与表达式章节中逐一了解。具备数据域的组件只有以下组件会创建新的数据域AppPageCardsChartCRUDCRUD2DialogDrawerListFormPaginationWrapperServiceWizardComboInputArrayTableTable2其中有一个特殊情况CRUD 中的filter本质上是一个 form所以CRUD 内部有两层数据域——第一层是 CRUD 本身第二层是查询条件表单。常见误解容器组件不一定有数据域只有少数几个容器组件会创建新的数据域见上面的列表。给不支持数据域的容器组件加data属性是常见错误{ type: page, data: { name: zhangsan }, body: [ { type: tpl, tpl: my name is ${name} }, { type: container, data: { name: lisi }, body: { type: tpl, tpl: my name is ${name} } } ] }这段配置不会生效——container不会创建数据域data属性被直接忽略内层tpl仍然沿数据链向上取到zhangsan。正确的做法是用Service包裹一层由Service承担数据域的职责{ type: page, data: { name: zhangsan }, body: [ { type: tpl, tpl: my name is ${name} }, { type: service, data: { name: lisi }, body: { type: container, body: { type: tpl, tpl: my name is ${name} } } } ] }此时内层tpl会输出my name is lisi。初始化数据域两种方式与合并规则初始化数据域共有两种方式。方式一配置组件初始化接口把服务端数据保存到某个组件数据域的最佳方式就是为组件配置初始化接口{ type: page, initApi: /api/initData, body: Hello ${text} }接口必须按照下面的格式返回{ status: 0, msg: , data: { text: World!, ...其他字段: } }使用时有几点必须注意并不是所有组件都支持配置初始化接口。对那些不支持初始化接口的组件一般使用 Service 组件 辅助实现数据域初始化status、msg、data是接口返回的必要字段data必须返回一个具有key-value结构的对象{ status: 0, msg: , data: { text: World! } // 正确对象 } { status: 0, msg: , data: some string // 错误需用 key 包装 } { status: 0, msg: , data: [a, b] // 错误需用 key 包装 }api除了配置字符串格式外还可以配置复杂对象结构method、headers、data 等详情参见 API 文档。方式二显式配置 data 属性值直接在 schema 上声明data即可{ data: { text: World!, name: amis }, type: page, body: Hello ${text}, my name is ${name}. }同时配置时的合并行为当初始化接口与data属性同时配置时数据域会合并data属性值与初始化接口返回的数据。从源码看这一逻辑发生在 packages/amis-core/src/WithStore.tsx 的store.initData调用中data属性含defaultData经dataMapping处理后的结果与远程数据store.hasRemoteData ? store.data : null一起被合并进新的数据域。更新数据域交互如何写回数据部分组件的交互行为会更新自身数据域。以表单提交为例{ type: page, body: { type: form, api: /api/mock2/form/saveForm, body: [ { type: input-text, name: name, label: 姓名 }, { type: input-text, name: age, label: 年龄 }, { type: static-tpl, tpl: 生成的id为${id} } ] } }/api/saveForm保存表单提交的数据并返回后端生成的id{ status: 0, msg: 保存成功, data: { id: 1 } }此时 amis 会把data与当前form组件的数据域进行mergeform内的static-tpl会根据更新后的数据域显示id为1。具有类似更新数据域特征的组件还有Formula等。在 store 层面数据更新由 packages/amis-core/src/store/iRenderer.ts 的updateData完成它会基于旧数据构造新对象、记录__prev修改前的值并可选携带__changeReason修改原因随后将新数据写回self.data。更新数据链从全量刷新到 trackExpression 精准追踪通常顶层数据域更新后所有具备数据域的子组件都会随之更新否则子组件拿不到最新值。但全量更新的代价很大比如在顶层更新一个name变量所有子组件都会被重新刷新一遍存在明显的性能损耗。因此 amis 中的具备数据域的组件默认只检测两层节点的数据是否变化上层数据域和上上层数据域来决定当前层数据要不要更新。这种做法会带来两个问题当前组件可能并不关心上层数据是否变化没必要进行这些刷新操作当前组件关心上上层的数据变化但默认检测不到最新值例如放在service中的crudcrud的filter用了service接口返回的数据却拿不到最新值。从amis 3.2.0版本开始针对具备数据域的组件新增了trackExpression属性用于主动声明当前组件需要关心的上层数据配置成none不追踪任何数据彻底关闭该组件因上层数据变化而触发的刷新配置成${xxxVariable}仅当xxxVariable变化时更新当前组件的数据链。trackExpression语法遵循表达式篇章支持同时监听多个变量如${xxx1},${xxx2}也支持写三元表达式如${ xxx ? xxx : yyy}。使用时有几个重要约束amis 内部通过运算该表达式的结果来判断是否变化因此不要使用随机函数、当前时间等每次结果都不同的内容否则每次都会更新数据链如果变量是数组或对象会被转成统一字符串[object Array]或[object Object]从而影响变化检测建议用管道符转成 JSON 字符串如${xxxObject | json}因为监控的是上层数据表达式中不要写当前层数据变量那是取不到的。从源码看trackExpression生效于 packages/amis-core/src/WithStore.tsxcomponentDidUpdate中通过tokenize(props.trackExpression, props.data!) ! tokenize(props.trackExpression, prevProps.data!)比较表达式两次渲染的结果只有结果变化时才重新store.initData同步数据链未配置trackExpression时才回退到isObjectShallowModified浅比较与isSuperDataModified超层检测等默认逻辑。下面是一个完整示例开关打开时同步刷新 CRUD 的数据链关闭时不追踪{ data: { name: amis }, type: page, body: [ { label: 请修改输入框, type: input-text, name: name}, { type: switch, label: 同步更新, name: syncSwitch }, { type: crud, filter: { trackExpression: ${syncSwitch ? name : }, body: [ my name is ${name} ] } } ] }当syncSwitch为真时trackExpression的结果随name变化而变化CRUD 的数据链随之更新开关关闭后表达式结果恒为空串CRUD 不再随name变化而刷新。URL 参数进入顶层数据域的地址栏数据URL 中的参数会自动进入顶层数据域因此组件可以直接通过模板变量引用。例如配置{ type: page, body: ${word} }当页面 URL 携带word参数例如?wordmyquery时body会直接渲染出该参数值。这也是数据链查找的第 4 条规则——即使到了page顶层节点url 参数层仍会被继续查找所以很多场景下可以直接用${id}一类写法取地址栏参数。隐藏数据不被枚举但可读取的特殊字段数据域中还有一类不会被枚举到、但可以读取的特殊字段__prev修改前的值__changeReason修改原因从 amis 6.9.0 版本开始支持__changeReason.type修改原因类型input用户输入apiapi 接口返回触发formula公式计算触发hide隐藏属性变化触发init表单项初始化触发action事件动作触发__super数据链的上一级这些字段在源码中有明确印证packages/amis-core/src/types.ts 定义了DataChangeReason接口type的取值正是上述六种input | api | formula | hide | init | action并附带name变化的字段名与value变化的值两个可选字段packages/amis-core/src/store/iRenderer.ts 中updateData通过Object.defineProperty(newData, __prev, {value: {...prev}, enumerable: false, ...})记录修改前数据并以同样方式写入不可枚举的__changeReasonpackages/amis-core/src/utils/object.ts 中createObject以Object.create(superProps)建立原型链并把上一级数据域挂到不可枚举的__super上。正因为这些字段都是enumerable: false所以常规枚举如Object.keys不会看到它们但通过${__prev.name}、${__changeReason|json}这类模板/表达式读取时完全可用。下面是一个同时演示__prev与__changeReason的完整示例表单中点击接口获取或设置值后模板会实时展示当前值、修改前的值与变化原因{ data: { name: amis }, type: form, id: form_data, actions: [ { type: button, label: 接口获取, actionType: ajax, api: { method: get, url: /api/mock2/form/saveForm, mockResponse: { status: 200, data: { name: amis-demo } } } }, { type: button, label: 设置值, onEvent: { click: { actions: [ { actionType: setValue, componentId: form_data, args: { value: { name: amis-demo2 } } } ] } } } ], body: [ { type: input-text, name: name, label: 姓名 }, { type: tpl, tpl: 当前值${name}br /修改前的值${__prev.name}br /变化原因${__changeReason|json} } ] }点击接口获取时name会被 API 返回的amis-demo覆盖__changeReason.type为api点击设置值时通过事件动作写入amis-demo2__changeReason.type为action。借助这两个隐藏字段可以方便地实现值变化追踪变更审计等高级交互。小结一张数据流转全景图把全文串起来amis 的数据体系可以概括为一条完整的流转链路初始化通过initApi接口返回值或显式data属性或两者合并写入当前组件数据域查找组件渲染时沿数据链逐级向上查找变量url参数位于最顶层__super指针在源码层面串联起整条链由createObject以原型链实现更新表单提交、事件动作等交互通过updateData写回数据域并同步记录__prev与__changeReason同步具备数据域的组件默认检测两层上层数据变化可通过trackExpression精准声明关心的上层变量把刷新范围收敛到最小。掌握数据域与数据链就等于掌握了 amis 页面中数据从哪里来、到哪里去、何时更新的全部规则——这也是排查为什么组件拿不到最新值为什么某个变量解析为空这类高频问题的第一把钥匙。相关概念的进一步展开可继续阅读 模板、表达式、数据映射 与 API 文档。【免费下载链接】amis前端低代码框架通过 JSON 配置就能生成各种页面。项目地址: https://gitcode.com/GitHub_Trending/am/amis创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表