移动端可视化引擎 F2 架构设计之: 为什么要选用 JSX

本文涉及的产品
云原生数据库 PolarDB 分布式版,标准版 2核8GB
简介: 移动端可视化引擎 F2 架构设计之: 为什么要选用 JSX

🙋🏻‍♀️ 编者按:本文作者是蚂蚁集团前端工程师索丘,和大家一起聊一聊为什么移动端可视化引擎 F2 要选用 JSX ,这背后的思考是什么?欢迎查阅~

  从图形语法说起

《图形语法》是 F2 数据可视化的核心理论,而图形语法的核心是通过一套抽象的语法来描述任意的统计图表,根据书中的描述,统计图的定义依靠以下几个基础语法:

声明 描述
DATA 从数据集生成视觉编码的数据操作
TRANS 变量转换(比如:rank)
SCALE 度量转换(比如:log)
COORD 定义坐标系(比如:极坐标)
ELEMENT 图形(比如:点)和他们的美学属性(比如:color)
GUIDE 辅助元素(比如:坐标轴,legend)

如果我们用一份数据结构去表达的话,那么可以抽象成如下的数据结构

{
  data: [
    {"category": 1, "value": 4},
    {"category": 2, "value": 6},
    {"category": 3, "value": 10},
    {"category": 4, "value": 3},
    {"category": 5, "value": 7},
    {"category": 6, "value": 8}
  ],
  trans: {
    value: 'rank',
  },
  scale: {
    value: 'linear',
  },
  coord: 'polar',
  element: {
    "theta": {"field": "category"},
    "radius": {"field": "value"},
    "color": {"field": "category"}
  },
  guide: {
    axis: {
     value: {},
    }
  }   
}

下面来看 F2 3.x API 命令式的描述

chart.data([
  {"category": 1, "value": 4},
  {"category": 2, "value": 6},
  {"category": 3, "value": 10},
  {"category": 4, "value": 3},
  {"category": 5, "value": 7},
  {"category": 6, "value": 8}
]);
chart.scale({
  value: 'linear',
});
chart.coord('polar');
chart.interval('category*value').color('category');
chart.axis({
  value: {},
});
chart.render();

从类型上看我们可以把前面的 JSON 描述归类到「声明式」,API 调用归类到 「命令式」,虽然从最后的结果看没有太大区别,但是从灵活性的角度看,下面的这种 API 命令式要远比 JSON 声明式灵活很多,因为我们可以在调用 API 的过程中加很多逻辑代码,来满足我们的个性化业务诉求,这个也是最早 F2 不直接使用 JSON 的一个原因。

虽然 F2 看起来是命令式的,但是实际上在内部,我们也构造了一个结构对象,用来保存图形语法的描述对象,只是对外提供的是 API 命令式的编程模式,也就是说我们最终的目的都是在构造图形语法的结构描述。

那为什么现在要采用 JSX 而不是沿用之前的 API 命令式呢, 在回答这个问题之前,我们先来看看 JSX 和 JSX 的特点。

  什么是 JSX

什么是 JSX, 可以看 React 官网的 JSX 简介,但是不要把 JSX 和 React 等同起来,JSX 可以让我们方便地创建数据结构,它可以和 React 完全没有关系,我们只要通过 Babel jsx transform 定义自己构造函数。

举个例子:假设我们定义了如下的构造函数和 JSX 结构

export function jsx(type, config, key?: string) {
  return {
    key,
    type,
    ...config,
  };
}
<chart
  data={[
    {"category": 1, "value": 4},
    {"category": 2, "value": 6},
    {"category": 3, "value": 10},
    {"category": 4, "value": 3},
    {"category": 5, "value": 7},
    {"category": 6, "value": 8}
  ]}
  scale={{
    value: 'linear',
  }}
  coord='polar'
>
  <interval position="x*y" />
  <axis field="x" />
</chart>

那么转换后的代码就会变成

import { jsx as _jsx } from "./jsx-runtime";
_jsx('chart', {
  data: [],
  coord: 'polar',
  children: [
    _jsx('interval', {
      position: "x*y",
    }),
    _jsx("axis", {
      field: "x",
    }),
  ],
});

执行完后,就会得到如下的结构

{
  type: "chart",
  data: [],
  coord: "polar",
  children: [
    { type: "interval", position: "x*y" },
    { type: "axis",  field: "x" }
  ]
}

通过 JSX 我们也得到了一个类似的数据结构,所以我们也能利用 JSX 来生成我们需要的结构描述,那么相比之下 JSX 有哪些优势呢,我们再来看看 JSX 的优势。

  JSX 的优势

通过前面我看到,我们最终的目的都是为了得到最后的图形语法结构描述,不管是 JSON, API 命令式,还是 JSX 都是为了生成这份结构,那么 JSX 相比之下又具有哪些优点呢?

1. 可编程性

JSX 可以在结构中很方便地嵌入表达式,这个是 JSON  不具备的能力

举个例子

<chart
  data={[
    {"category": 1, "value": 4},
    {"category": 2, "value": 6},
    {"category": 3, "value": 10},
    {"category": 4, "value": 3},
    {"category": 5, "value": 7},
    {"category": 6, "value": 8}
  ]}
  scale={{
    value: 'linear',
  }}
  coord='polar'
  >
  <interval position="x*y" />
  {
    // 类似这样的表达式在 JSON 中是不好描述的
    showAxis ? <axis field="x" formatter={v => v.toFixed(2)}/> : null
  }
</chart>

从上面的例子我们看到 2 个点:

  1. axis 需要通过 showAxis 这个变量来控制是否显示,如果是 JSON 的话,那么就需要 2 份 JSON 描述
  2. formatter 有自定义的格式化的诉求,而 JSON 无法保存方法的

2. 更强的扩展能力

因为 JSON 需要有配套的 Runtime 来处理 JSON 结构,如果需要对 JSON 进行扩展,那么相应的 Runtime 也需要同步升级,这个在多变的业务场景中带来的成本无疑是巨大的,而 JSX 可以通过扩展标签类型来方便地实现

例子:

import { Custom } from './custom';
<chart
  data={[
    {"category": 1, "value": 4},
    {"category": 2, "value": 6},
    {"category": 3, "value": 10},
    {"category": 4, "value": 3},
    {"category": 5, "value": 7},
    {"category": 6, "value": 8}
  ]}
  scale={{
    value: 'linear',
  }}
  coord='polar'
  >
  <interval position="x*y" />
  <axis field="x" />
  { /* 自定义的标签扩展 */ }
  <Custom ... />
</chart>

JSX 里有个默认规则,小写是内置标签(对应的类型为string),大写的是外部引用(可以是任意类型),所以只要约定 Custom 对应的接口(在 React 里就是 Component),就能实现无限扩展,但是这样也会带来一个问题,那就是生成的结构不再能被序列化传输和存储,这个我们后面的篇幅再讨论。

3. 更稳定的树结构

要理解这一点需要对 JSX 生成的结构有更深的理解,我这里只简单提一下,大家有兴趣可以去研究下 JSX 的编译规则 这句话可以这么理解:「不管外部参数如何变化,JSX 返回的结构树是稳定的」,稳定的结构树,是后续 diff 的基础。

例子:

<chart
  data={[
    {"category": 1, "value": 4},
    {"category": 2, "value": 6},
    {"category": 3, "value": 10},
    {"category": 4, "value": 3},
    {"category": 5, "value": 7},
    {"category": 6, "value": 8}
  ]}
  scale={{
    value: 'linear',
  }}
  coord='polar'
  >
  <interval position="x*y" />
  {
    showAxis ? <axis field="x" /> : null
  }
</chart>

我们还是拿这段代码为例,showAxis 为 true 和 false 时,生成的结构对象分别如下所示,这 2 颗树的结构是一致的,并不会因为参数不同而不同。

{
  type: "chart",
  data: [],
  coord: "polar",
  children: [
    { type: "interval", position: "x*y" },
    { type: "axis",  field: "x" }
  ]
}
{
  type: "chart",
  data: [],
  coord: "polar",
  children: [
    { type: "interval", position: "x*y" },
    null,   // 树的节点里还是会保留null
  ]
}

4. 完整的结构描述

JSX 保留了 JSON 结构化描述的特点,这个是 API 命令式不具备的,命令式需要执行完全部代码之后才能得到完整的结构,而且通过转换函数,可以将 JSX 方便地转成 JSON,在面向未来的 nocode、lowcode、甚至是智能化场景,JSON 无疑是一种最好的形式,而且 JSON 机器友好度比较高。

5. 成熟的配套工具

不管是 Babel 还是 TypeScript 都有成熟的 JSX 编译插件,而且配置简单,详情可看 F2 官网的 jsx-transform

6. 小结

JSX 保留了 JSON 结构化描述的特点,,但相比 JSON 又具备更强的灵活性和可编程性,这些在我们面临复杂的业务场景时是很重要的,但是也因为灵活,所以 JSX 的结构是不可被序列化传输和存储的,这个也是 JSX 的局限,但是这个局限我们可以通过上层更进一步的领域解决方案来封装和解决。

  JSX 之下的 JSON 化

前面我们也提到 JSON 有非常好的机器友好度,尤其是面向搭建和智能化场景,这些场景都需要跨端,甚至是跨平台来传输和存储,所以在面向机器友好度的角度来看,我们还是要对 JSX 进行 JSON 化,前面我们也提到过,序列化传输和存储的问题要通过上层的解决方案来解决,那么我们要如何解决 JSON 化的问题呢?

我们就拿上面这个例子的 formatter举例

<chart
  data={[
    {"category": 1, "value": 4},
    {"category": 2, "value": 6},
    {"category": 3, "value": 10},
    {"category": 4, "value": 3},
    {"category": 5, "value": 7},
    {"category": 6, "value": 8}
  ]}
  scale={{
    value: 'linear',
  }}
  coord='polar'
  >
  <interval position="x*y" />
  {
    // 类似这样的表达式在 JSON 中是不好描述的
    showAxis ? <axis field="x" formatter={v => v.toFixed(2)} /> : null
  }
</chart>

这个例子中,当我们面向所有业务场景时 formatter 的格式是各种各样的,比如保留小数点后 0 - n 位,百分比,保留正负号,日期格式等等,因为我们无法穷举,所以通过函数来处理,但是又因为用了函数,这个结构体就不能被序列化了,这个也是我们前面提到 JSX 不可被序列化的原因。

但是,当我们把业务场景局限到某个特定领域时,这个场景的特点和诉求就能被枚举出来,所以当我们面向某个特定领域时,formatter 的格式也是可能被枚举出来,比如某个场景小数点统一保留 2 位,需要百分比,日期处理等等这些具体的规则,这个时候我们就可以定义 formatter 的类型为 'toFixed' | 'percent' | 'date' 等。

一旦可枚举之后,那么我们就可以用如下的方式来表达了

<chart
  data={[
    {"category": 1, "value": 4},
    {"category": 2, "value": 6},
    {"category": 3, "value": 10},
    {"category": 4, "value": 3},
    {"category": 5, "value": 7},
    {"category": 6, "value": 8}
  ]}
  scale={{
    value: 'linear',
  }}
  coord='polar'
  >
  <interval position="x*y" />
  <customAxis visible={ true } field="x" formatter="toFixed" />
</chart>

而这个结构,就完全可以用下面的 JSON 来描述了

{
  type: "chart",
  data: [],
  coord: "polar",
  children: [
    { type: "interval", position: "x*y" },
    { type: "customAxis",  field: "x", visible: true, formatter: 'toFixed' }
  ]
}

这个 JSON 是完全可以被序列化的,而这个转换在代码上仅仅只是对  axis 进行了再次的封装,而这个也是我们提供给业务,让业务可以做领域二次封装的能力,业务的二次封装还能带来业务使用的便利性。

所以,JSX 不仅能有清晰的结构描述,还有良好的编程能力和扩展性,而且通过二次封装,我们还能 JSON 序列化,所以 JSX 的形式无疑是当下最好的选择

  领域解决方案

我们再来聊聊领域解决方案,我们面向的是整个移动端的数据可视化场景,因为需要考虑通用性和灵活性,它的易用性往往是不够的,还是拿我们前面提到的这个 formatter 举例,在面向特定领域时,格式化的差异会非常大。比如金融类的股票、基金场景:formatter 不仅需要格式化数字,还需要根据数字的 正、负、零来显示不同的颜色(俗称红涨绿跌平盘色),而且美股颜色还需要相反(绿涨红跌),如果业务中这些统一的规则要反复处理,那估计是要发疯的,所以就需要领域解决方案来解决这些问题。

这里顺便再剧透一下,在不远的将来,我们还会在 F2, F6 之上,主要从易用性的角度来考虑,并给大家带来更简单易用的移动端可视化解决方案 FCharts,让普通场景使用简单的同时,也让领域解决方案变得更加方便和易用,敬请期待。

  最后

总结下来,我们选用 JSX 就是看中 JSX 的可编程性、易扩展性、完整的结构描述和成熟的配套,不仅是当下对编程友好度、业务复杂度的考虑,还有未来面向搭建和智能化友好的 JSON, 都是一种很好的选择。

最后,如果想了解更多细节欢迎 star 我们的 GitHub 和 官网

  • F2 GitHub
  • F2 官网
  • F6 GitHub
  • F6 官网


相关实践学习
跟我学:如何一键安装部署 PolarDB-X
《PolarDB-X 动手实践》系列第一期,体验如何一键安装部署 PolarDB-X。
相关文章
|
4天前
|
机器学习/深度学习 数据处理 数据安全/隐私保护
|
4天前
|
存储 边缘计算 人工智能
云计算与分布式系统架构:驱动数字化时代的创新引擎
本文将探讨云计算与分布式系统架构在数字化时代中的重要性,介绍其基本概念和原理,并探讨其在推动技术创新、提升企业效率和满足用户需求方面的作用。同时,还将提出未来发展的趋势和挑战,为读者提供对云计算与分布式系统架构的深入理解。
|
7月前
|
安全 持续交付 开发者
Docker 架构解析:多角度解析 Docker 引擎与容器运行时
Docker 架构解析:多角度解析 Docker 引擎与容器运行时
58 0
|
7月前
|
持续交付 虚拟化 Docker
Docker 架构解析:理解 Docker 引擎和容器运行时
Docker 架构解析:理解 Docker 引擎和容器运行时
537 1
|
4天前
|
存储 Dragonfly NoSQL
Tair 对 Redis 引擎架构之争的看法
本文详细讲解了阿里云自研数据库Tair的发展以及介绍。
72961 0
|
4天前
|
SQL 关系型数据库 MySQL
Presto【基础 01】简介+架构+数据源+数据模型+特点(一篇即可入门支持到PB字节的分布式SQL查询引擎Presto)
Presto【基础 01】简介+架构+数据源+数据模型+特点(一篇即可入门支持到PB字节的分布式SQL查询引擎Presto)
61 0
|
9月前
|
人工智能 并行计算 安全
国内首个政务领域 Web 引擎 SIG 成立!龙蜥联合儒特科技打造全新一代 Web 架构
龙蜥社区联合儒特科技成立的 Web 引擎 SIG 组,表明了龙蜥对前沿尖端技术探索的前瞻性。
|
10月前
|
存储 弹性计算 运维
互娱NoSQL架构优化 —— 暨MongoDB“在线换引擎”技术服务指南”
XX工作室是某大客户核心游戏工作室,其核心业务是国内二次元RPG手游,采用实时开放世界对战模式,整体采用阿里云方案,本次专项攻坚主要对于玩家在游戏期间各类游戏属性交互(包含过图、物品、面板、剧情等)的核心业务模块进行优化,其中涉及NoSQL部分由于在专项优化期间存在诸多细节,特此提炼出来给各位有类似互娱业务场景进行参考。
|
资源调度 分布式计算 Kubernetes
给 K8s 装上大数据调度引擎:伏羲架构升级 K8s 统一调度
飞天伏羲作为有着十多年历史的调度团队,在服务好 MaxCompute 大数据平台的过程中,一直在不断通过自我革新赶超业界先进水平,我们经历了 Fuxi 2.0 的这样的大规模升级,今天通过 K8s 统一调度项目又再次实现了系统架构的蜕变,将大数据平台强大的调度能力赋予 K8s 系统,同时去拥抱 K8s 周边丰富的生态。除了集团弹内集群,将来我们在公共云、专有云等多个场景,也会以 K8s 统一调度的方式进行输出,以更好地服务云上的用户,敬请期待!
1468 0
给 K8s 装上大数据调度引擎:伏羲架构升级 K8s 统一调度
|
存储 SQL 缓存
MySQL-InnoDB引擎-架构和事务原理
MySQL-InnoDB引擎-架构和事务原理
MySQL-InnoDB引擎-架构和事务原理