amis2/docs/zh-CN/concepts/datascope-and-datachain.md
cmdyu 59e619a6c6
Update datascope-and-datachain.md (#7255)
修复文档中《数据域与数据链》章节下的“更新数据链”部分里将”表达式“写成了”表单时“的错误
2023-06-26 12:01:16 +08:00

459 lines
12 KiB
Markdown
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
title: 数据域与数据链
description:
type: 0
group: 💡 概念
menuName: 数据域与数据链
icon:
order: 10
---
## 基本的数据展示
我们再看之前的简单示例:
```json
{
"type": "page",
"body": "Hello World!"
}
```
目前我们只是在 `Page` 组件中渲染一串固定的文本,如何实现 **通过接口拉取想要的数据,并展示到 `Page` 组件的内容区** 呢?
## 添加初始化接口
```json
{
"type": "page",
"initApi": "/api/mock2/page/initData",
"body": "date is ${date}"
}
```
这个 api 接口返回的数据结构如下:
```json
{
"status": 0,
"msg": "",
"data": {
"title": "Test Page Component",
"date": "2017-10-13"
}
}
```
渲染后页面效果:
```schema
{
"type": "page",
"initApi": "/api/mock2/page/initData",
"body": "date is ${date}"
}
```
## 发生了什么?
我们可以看到,输出结果不变,但是我们这次渲染的是接口返回的数据:
1. 首先我们给 `Page` 组件配置了`initApi`属性,该属性会在组件初始化时,请求所该属性所配置的接口
2. 接口请求成功后,`Page` 会把接口返回的`data`内数据存到当前的数据域中
3. `Page` 在渲染 `body` 所配置的文本时,会解析文本内容,当解析到模板变量`${text}`时amis 会把尝试在当前组件的数据域中获取`text`变量值,并替换掉`${text}`,最后渲染解析后的文本。
> 下一节我们会介绍[模板](./template)`body`属性自身支持模板语法amis 中支持模板语法的组件还有很多
## 数据域
前面我们提到了**数据域**这个概念,它是 amis 中最重要的概念之一。
还是通过最简单的示例进行讲解:
```json
{
"type": "page",
"body": "Hello ${text}"
}
```
上面的配置要做的很简单:使用 `Page` 组件,在内容区内渲染一段模板文字,其中`${text}`是**模板变量**,它会去到当前组件的数据域中,获取`text`变量值。
毫无疑问,`${text}`将会解析为空白文本,最终渲染的文本是 `Hello`
```schema
{
"type": "page",
"body": "Hello ${text}"
}
```
因为当前 `Page` 组件的数据域中是没有任何数据的,相比之前的示例,区别在于前面我们为 `Page` 组件配置了初始化接口,它会将接口返回的数据存入数据域中以供组件使用。
再观察下面这段配置:
```schema
{
"data": {
"text": "World!"
},
"type": "page",
"body": "Hello ${text}"
}
```
再次查看渲染结果,顺利输出了 `Hello World!`
相信你可能已经猜到,**组件的`data`属性值是数据域的一种形式**,实际上当我们没有显式的配置数据域时,可以假想成这样:
```schema
{
"data": {}, // 空的数据域
"type": "page",
"body": "Hello ${text}"
}
```
> 而前面我们知道 amis 的特性之一是基于组件树,因此自然数据域也会形成类似于树型结构,如何来处理这些数据域之间的联系呢,这就是我们马上要介绍到的 **[数据链](./datascope-and-datachain#%E6%95%B0%E6%8D%AE%E9%93%BE)**
## 数据链
相信通过上文,你已经基本掌握了 amis 中数据域的概念,接下来我们会介绍另一个重要概念:**数据链**。
**数据链**的特性是,当前组件在遇到获取变量的场景(例如模板渲染、展示表单数据、渲染列表等等)时:
1. 首先会先尝试在当前组件的数据域中寻找变量,当成功找到变量时,通过数据映射完成渲染,停止寻找过程;
2. 当在当前数据域中没有找到变量时,则向上寻找,在父组件的数据域中,重复步骤`1`和`2`
3. 一直寻找,直到顶级节点,也就是`page`节点,寻找过程结束。
4. 但是如果 url 中有参数,还会继续向上查找这层,所以很多时候配置中可以直接 `${id}` 取地址栏参数。
> 为了方便讲解,我们这一章的例子统一使用的设置组件`data`属性的方式来初始化数据域,请记住,如果组件支持,你永远可以通过接口来进行数据域的初始化
继续来看这个例子:
```schema
{
"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
```
数据链:
```json
{
"name": "zhangsan",
"age": 20,
"__sub": {
"name": "lisi"
}
}
```
> `__sub` 字段只是为了方便理解。
首先,`page`组件下的`tpl`组件,在渲染`my name is ${name}`时,首先会在`page`的数据域中,尝试寻找`name`变量,在当前数据域中,`name`变量为`zhangsan`,因此寻找变量结束,通过数据映射渲染,输出:`my name is zhangsan`,渲染结束;
然后,`service`组件开始渲染,`service`组件内子组件`tpl`,它配置的模板字符串是:`my name is ${name}, I'm ${age} years old`,它会在`service`的数据域中,尝试寻找`name`和`age`变量。
由代码可以看出,`service`数据域中`name`变量为`lisi`,因此停止该变量的寻找,接下来寻找`age`变量。
很明显在`service`数据域中寻找`age`变量会失败,因此向上查找,尝试在`page`数据域中寻找`age`变量,找到为`20`,寻找变量结束,通过数据映射渲染,输出:`my name is lisi, I'm 20 years old`,渲染结束。
> **注意:** 当前例子中,对数据域中数据的获取使用的是 **\${xxx}** 模板语法,但是在不同的组件配置项中,获取数据的语法会有差异,我们会在后续的[模板](./template)和[表达式章节](./expression)中一一介绍。
### 具备数据域的组件
- App
- Page
- Cards
- Chart
- CRUD
- CRUD2
- Dialog
- Drawer
- List
- Page
- PaginationWrapper
- Service
- Wizard
- Combo
- InputArray
- Table
- Table2
有个特殊情况是 CRUD 中 filter实际上是个 form所以 CRUD 中有两层数据域,第一层是 CRUD 本身,同时查询条件表单中也有一层数据域。
### 常见误解
需要注意,只有少数几个容器组件会创建新的数据域,具体查看[具备数据域的组件](#具备数据域的组件)列表。
常见的错误写法是给容器组件加 data 属性,比如:
```schema
{
"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}"
}
}
]
}
```
这样是不会生效的,正确的做法是使用 Service 包裹一层,如下所示
```schema
{
"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}"
}
}
}
]
}
```
## 初始化数据域
通过上面的介绍你可能发现,初始化数据域有两种方式:
### 1. 配置组件初始化接口
想要将自己的服务中的数据保存到某个组件的数据域中,最好的方式就是为当前组件配置初始化接口:
```json
{
"type": "page",
"initApi": "/api/initData",
"body": "Hello ${text}"
}
```
接口必须按照下面的格式返回:
```json
{
"status": 0,
"msg": "",
"data": {
"text": "World!"
...其他字段
}
}
```
**注意:**
1. **并不是所有组件都支持配置初始化接口来实现数据域初始化操作**,对于那些不支持配置初始化接口的组件来说,一般会使用 [**Service 组件**](../../components/service) 来辅助实现数据域初始化;
2. **`status`**、**`msg`** 和 **`data`** 字段为接口返回的必要字段;
3. `data`必须返回一个具有`key-value`结构的对象
```json
{
"status": 0,
"msg": "",
"data": { // 正确
"text": "World!"
}
}
// 直接返回字符串或者数组都是无效的
{
"status": 0,
"msg": "",
"data": "some string" // 错误,使用 key 包装
}
{
"status": 0,
"msg": "",
"data": ["a", "b"] // 错误,使用 key 包装
}
```
> `api` 除了配置字符串格式以外,还可以配置复杂对象结构,更多详情查看[API 文档](../types/api)
### 2. 显式配置 data 属性值
另一种初始化当前数据域的方式是显式的设置组件的`data`属性值:
```schema
{
"data": {
"text": "World!",
"name": "amis"
},
"type": "page",
"body": "Hello ${text}, my name is ${name}."
}
```
### 同时配置
在同时配置 **初始化接口****`data`属性** 时,数据域将会合并`data`属性值和初始化接口返回的数据
## 更新数据域
部分组件的某些交互或行为会对当前组件的数据域进行更新:
```schema
{
"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`,并返回到前端,格式如下;
```json
{
"status": 0,
"msg": "保存成功",
"data": {
"id": 1
}
}
```
这时 amis 将会把`data`数据与当前`form`组件的数据域进行**merge**`form`组件中的`static-tpl`组件会根据更新后的数据域,显示`id`为`1`。
> 具有类似特征的组件还有`Formula`等
## 更新数据链
通常顶层数据域数据更新,孩子中具备数据域的组件都会更新,如果不更新会拿不到最新的值。从功能来看这个更新代价其实是很大的,有性能损耗,比如如果我在顶层更新了个变量 `name`,所有的孩子都会重新刷新一遍。
目前 amis 中,具备数据域的组件,默认会检测两层节点的数据是否发生变化(上层数据域和上上层数据域),来决定当前层的数据要不要更新。存在两个问题:
1. 当前组件也许并不关心上层数据是否变化,没必要进行这些刷新操作
2. 当前组件关系上上层的数据变化,但是在此拿不到最新的。(比如:放在 service 中的 crudcrud 中 filter 用了 service 的接口返回数据,但是拿不到最新的)
amis 从 3.2.0 版本开始针对[具备数据域的组件](#具备数据域的组件)新增了 `trackExpression` 属性,用来主动配置当前组件需要关心的上层数据。
针对以上问题,则可以通过这样配置来解决
1. `trackExpression` 配置成 `"none"` 也就是说不追踪任何数据。
2. `trackExpression` 配置成 `"${xxxVariable}"` 这样 xxxVariable 变化了更新当前组件的数据链。
关于 `trackExpression` 的语法,请查看表达式篇章,可以监听多个变量比如: `"${xxx1},${xxx2}"`,还可以写表达式如 `"${ xxx ? xxx : yyy}"`
amis 内部是通过运算这个表达式的结果来判断。所以表达式中千万不要用随机函数,或者用当前时间等,否则每次都会更新数据链。另外如果变量是数组,或者对象,会转成统一的字符串 `[object Array]` 或者 `[object Object]` 这个其实会影响检测的,所以建议转成 json 字符串如。 `${xxxObject | json}`。还有就是既然是监控上层数据,表达式中不要写当前层数据变量,是取不到的。
```schema
{
"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}"
]
}
}
]
}
```
## URL 参数
url 中的参数会进入顶层数据域,比如下面的例子,可以点击[这里](./datascope-and-datachain?word=myquery#url-参数)看效果。
```schema
{
"type": "page",
"body": "${word}"
}
```