F2 Architecture Design: Why Choose JSX

Start with the grammar of graphics

"Graphics Grammar" is the core theory of F2 data visualization, and the core of graph grammar is to describe arbitrary statistical charts through a set of abstract grammars. According to the description in the book, the definition of statistical charts relies on the following basic grammars:

statement describe
DATA Data manipulation to generate visual codes from datasets
TRANS Variable transformation (eg: rank)
SCALE Metric transformation (eg: log)
COORD Define the coordinate system (eg polar coordinates)
ELEMENT Shapes (eg: points) and their aesthetic properties (eg: color)
GUIDE Auxiliary elements (eg: axes, legend)

If we use a data structure to express, then it can be abstracted into the following data structure

{
  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: {},
    }
  }   
}
复制代码

Let's take a look at the description of the F2 3.x API imperative

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();
复制代码

In terms of type, we can classify the previous JSON description into "declarative" and API calls into "imperative". Although there is not much difference from the final result, from the perspective of flexibility, the following This API imperative is much more flexible than the JSON declarative, because we can add a lot of logic code in the process of calling the API to meet our personalized business requirements, which is also one of the reasons why F2 did not use JSON directly.

Although F2 seems to be imperative, in fact, internally, we also construct a structure object to save the description object of the graphics grammar, but only provide the API imperative programming mode, that is to say, our final The purpose is to construct the structural description of the graph grammar

So why use JSX now instead of using the previous API imperative? Before answering this question, let's take a look at the characteristics of JSX and JSX.

What is JSX

What is JSX, you can see the introduction of JSX on React's official website , but don't equate JSX with React, JSX allows us to easily create data structures, it can have nothing to do with React, we just need to define our own constructor through Babel jsx transform

for example:

Suppose we define the following constructor and JSX structure

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>
复制代码

Then the converted code becomes

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>
复制代码

我们还是拿这段代码为例,showAxistruefalse 时,生成的结构对象分别如下所示,这 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 和 官网

Guess you like

Origin juejin.im/post/7085740448295157774