amis 数据域与数据链:JSON 配置中的数据作用域、查找链路与更新机制完全指南
2026/9/13 11:18:49 网站建设 项目流程

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。这里发生了一次完整的数据闭环:

  1. 组件初始化时,amis 按initApi配置发起请求;
  2. 请求成功后,Page把返回结构中的data字段内容存入当前组件的数据域
  3. 渲染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. 当前数据域没找到时,向上到父组件数据域,重复步骤 1 和 2;
  3. 一直找到顶级节点(通常是page节点),寻找过程结束;
  4. 如果 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!", "...其他字段": "" } }

使用时有几点必须注意:

  1. 并不是所有组件都支持配置初始化接口。对那些不支持初始化接口的组件,一般使用 Service 组件 辅助实现数据域初始化;
  2. statusmsgdata是接口返回的必要字段
  3. 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属性(含defaultDatadataMapping处理后的结果)与远程数据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会根据更新后的数据域显示id1。具有类似"更新数据域"特征的组件还有Formula等。

在 store 层面,数据更新由 packages/amis-core/src/store/iRenderer.ts 的updateData完成:它会基于旧数据构造新对象、记录__prev(修改前的值),并可选携带__changeReason(修改原因),随后将新数据写回self.data

更新数据链:从全量刷新到 trackExpression 精准追踪

通常顶层数据域更新后,所有具备数据域的子组件都会随之更新,否则子组件拿不到最新值。但全量更新的代价很大:比如在顶层更新一个name变量,所有子组件都会被重新刷新一遍,存在明显的性能损耗。

因此 amis 中的具备数据域的组件,默认只检测两层节点的数据是否变化(上层数据域和上上层数据域),来决定当前层数据要不要更新。这种做法会带来两个问题:

  1. 当前组件可能并不关心上层数据是否变化,没必要进行这些刷新操作;
  2. 当前组件关心上上层的数据变化,但默认检测不到最新值(例如放在service中的crudcrudfilter用了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 中createObjectObject.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.typeapi;点击"设置值"时通过事件动作写入amis-demo2__changeReason.typeaction。借助这两个隐藏字段,可以方便地实现"值变化追踪""变更审计"等高级交互。

小结:一张数据流转全景图

把全文串起来,amis 的数据体系可以概括为一条完整的流转链路:

  1. 初始化:通过initApi接口返回值或显式data属性(或两者合并)写入当前组件数据域;
  2. 查找:组件渲染时沿数据链逐级向上查找变量,url参数位于最顶层,__super指针在源码层面串联起整条链(由createObject以原型链实现);
  3. 更新:表单提交、事件动作等交互通过updateData写回数据域,并同步记录__prev__changeReason
  4. 同步:具备数据域的组件默认检测两层上层数据变化,可通过trackExpression精准声明关心的上层变量,把刷新范围收敛到最小。

掌握数据域与数据链,就等于掌握了 amis 页面中"数据从哪里来、到哪里去、何时更新"的全部规则——这也是排查"为什么组件拿不到最新值""为什么某个变量解析为空"这类高频问题的第一把钥匙。相关概念的进一步展开,可继续阅读 模板、表达式、数据映射 与 API 文档。

【免费下载链接】amis前端低代码框架,通过 JSON 配置就能生成各种页面。项目地址: https://gitcode.com/GitHub_Trending/am/amis

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询