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}, I'm ${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 zhangsan; service内的tpl渲染my name is ${name}, I'm ${age} years old:- 先在
service数据域中找name,命中lisi,该变量查找结束; - 找
age时在service数据域中失败,于是沿数据链向上,在page数据域中命中age = 20; - 最终输出
my name is lisi, I'm 20 years old。
- 先在
注意:本例中获取数据使用的是
${xxx}模板语法。不同组件配置项中获取数据的语法会有差异,后续可在模板与表达式章节中逐一了解。
具备数据域的组件
只有以下组件会创建新的数据域:
- App
- Page
- Cards
- Chart
- CRUD
- CRUD2
- Dialog
- Drawer
- List
- Form
- PaginationWrapper
- Service
- Wizard
- Combo
- InputArray
- Table
- Table2
其中有一个特殊情况: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组件的数据域进行merge,form内的static-tpl会根据更新后的数据域显示id为1。具有类似"更新数据域"特征的组件还有Formula等。
在 store 层面,数据更新由 packages/amis-core/src/store/iRenderer.ts 的updateData完成:它会基于旧数据构造新对象、记录__prev(修改前的值),并可选携带__changeReason(修改原因),随后将新数据写回self.data。
更新数据链:从全量刷新到 trackExpression 精准追踪
通常顶层数据域更新后,所有具备数据域的子组件都会随之更新,否则子组件拿不到最新值。但全量更新的代价很大:比如在顶层更新一个name变量,所有子组件都会被重新刷新一遍,存在明显的性能损耗。
因此 amis 中的具备数据域的组件,默认只检测两层节点的数据是否变化(上层数据域和上上层数据域),来决定当前层数据要不要更新。这种做法会带来两个问题:
- 当前组件可能并不关心上层数据是否变化,没必要进行这些刷新操作;
- 当前组件关心上上层的数据变化,但默认检测不到最新值(例如放在
service中的crud,crud的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.tsx:componentDidUpdate中通过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参数(例如?word=myquery)时,body会直接渲染出该参数值。这也是数据链查找的第 4 条规则——即使到了page顶层节点,url 参数层仍会被继续查找,所以很多场景下可以直接用${id}一类写法取地址栏参数。
隐藏数据:不被枚举但可读取的特殊字段
数据域中还有一类不会被枚举到、但可以读取的特殊字段:
__prev:修改前的值__changeReason:修改原因(从 amis 6.9.0 版本开始支持)__changeReason.type:修改原因类型input:用户输入api:api 接口返回触发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, ...})记录修改前数据,并以同样方式写入不可枚举的__changeReason; - packages/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),仅供参考