amis2/docs/zh-CN/concepts/datascope-and-datachain.md

14 KiB
Executable File
Raw Permalink Blame History

title description type group menuName icon order
数据域与数据链 0 💡 概念 数据域与数据链 10

基本的数据展示

我们再看之前的简单示例:

{
  "type": "page",
  "body": "Hello World!"
}

目前我们只是在 Page 组件中渲染一串固定的文本,如何实现 通过接口拉取想要的数据,并展示到 Page 组件的内容区 呢?

添加初始化接口

{
  "type": "page",
  "initApi": "/api/mock2/page/initData",
  "body": "date is ${date}"
}

这个 api 接口返回的数据结构如下:

{
  "status": 0,
  "msg": "",
  "data": {
    "title": "Test Page Component",
    "date": "2017-10-13"
  }
}

渲染后页面效果:

{
  "type": "page",
  "initApi": "/api/mock2/page/initData",
  "body": "date is ${date}"
}

发生了什么?

我们可以看到,输出结果不变,但是我们这次渲染的是接口返回的数据:

  1. 首先我们给 Page 组件配置了initApi属性,该属性会在组件初始化时,请求所该属性所配置的接口
  2. 接口请求成功后,Page 会把接口返回的data内数据存到当前的数据域中
  3. Page 在渲染 body 所配置的文本时,会解析文本内容,当解析到模板变量${text}amis 会把尝试在当前组件的数据域中获取text变量值,并替换掉${text},最后渲染解析后的文本。

下一节我们会介绍模板body属性自身支持模板语法amis 中支持模板语法的组件还有很多

数据域

前面我们提到了数据域这个概念,它是 amis 中最重要的概念之一。

还是通过最简单的示例进行讲解:

{
  "type": "page",
  "body": "Hello ${text}"
}

上面的配置要做的很简单:使用 Page 组件,在内容区内渲染一段模板文字,其中${text}模板变量,它会去到当前组件的数据域中,获取text变量值。

毫无疑问,${text}将会解析为空白文本,最终渲染的文本是 Hello

{
  "type": "page",
  "body": "Hello ${text}"
}

因为当前 Page 组件的数据域中是没有任何数据的,相比之前的示例,区别在于前面我们为 Page 组件配置了初始化接口,它会将接口返回的数据存入数据域中以供组件使用。

再观察下面这段配置:

{
  "data": {
    "text": "World!"
  },
  "type": "page",
  "body": "Hello ${text}"
}

再次查看渲染结果,顺利输出了 Hello World!

相信你可能已经猜到,组件的data属性值是数据域的一种形式,实际上当我们没有显式的配置数据域时,可以假想成这样:

{
  "data": {}, // 空的数据域
  "type": "page",
  "body": "Hello ${text}"
}

而前面我们知道 amis 的特性之一是基于组件树,因此自然数据域也会形成类似于树型结构,如何来处理这些数据域之间的联系呢,这就是我们马上要介绍到的 数据链

数据链

相信通过上文,你已经基本掌握了 amis 中数据域的概念,接下来我们会介绍另一个重要概念:数据链

数据链的特性是,当前组件在遇到获取变量的场景(例如模板渲染、展示表单数据、渲染列表等等)时:

  1. 首先会先尝试在当前组件的数据域中寻找变量,当成功找到变量时,通过数据映射完成渲染,停止寻找过程;
  2. 当在当前数据域中没有找到变量时,则向上寻找,在父组件的数据域中,重复步骤12
  3. 一直寻找,直到顶级节点,也就是page节点,寻找过程结束。
  4. 但是如果 url 中有参数,还会继续向上查找这层,所以很多时候配置中可以直接 ${id} 取地址栏参数。

为了方便讲解,我们这一章的例子统一使用的设置组件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

数据链:

{
  "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的数据域中,尝试寻找nameage变量。

由代码可以看出,service数据域中name变量为lisi,因此停止该变量的寻找,接下来寻找age变量。

很明显在service数据域中寻找age变量会失败,因此向上查找,尝试在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}"
      }
    }
  ]
}

这样是不会生效的,正确的做法是使用 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}"
        }
      }
    }
  ]
}

初始化数据域

通过上面的介绍你可能发现,初始化数据域有两种方式:

1. 配置组件初始化接口

想要将自己的服务中的数据保存到某个组件的数据域中,最好的方式就是为当前组件配置初始化接口:

{
  "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 除了配置字符串格式以外,还可以配置复杂对象结构,更多详情查看API 文档

2. 显式配置 data 属性值

另一种初始化当前数据域的方式是显式的设置组件的data属性值:

{
  "data": {
    "text": "World!",
    "name": "amis"
  },
  "type": "page",
  "body": "Hello ${text}, my name is ${name}."
}

同时配置

在同时配置 初始化接口data属性 时,数据域将会合并data属性值和初始化接口返回的数据

更新数据域

部分组件的某些交互或行为会对当前组件的数据域进行更新:

{
  "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

更新数据链

通常顶层数据域数据更新,孩子中具备数据域的组件都会更新,如果不更新会拿不到最新的值。从功能来看这个更新代价其实是很大的,有性能损耗,比如如果我在顶层更新了个变量 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}。还有就是既然是监控上层数据,表达式中不要写当前层数据变量,是取不到的。

{
  "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 中的参数会进入顶层数据域,比如下面的例子,可以点击这里看效果。

{
  "type": "page",
  "body": "${word}"
}

隐藏数据

数据中还有以下字段不会被枚举到,但是可以读取:

  • __prev 修改前的值
  • __changeReason 修改原因
  • __changeReason.type 修改原因类型
    • input 用户输入
    • api api 接口返回触发
    • formula 公式计算触发
    • hide 隐藏属性变化触发
    • init 表单项初始化触发
    • action 事件动作触发
  • __super 数据链的上一级

__changeReason 字段在 amis 6.9.0 版本开始支持

{
  "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}"
    }
  ]
}