# 谈谈数据驱动的UI框架 (February 4, 2023)

<img src="https://s2.loli.net/2023/02/04/xfZHc47UQJrVsDt.png" width="3000" title="" crop="0,0,1,1" id="FHc9o" class="ne-image">

```python
import framework  # 前端框架
renderer = framework.compile(template)  # 模板，即UI的描述
UI = renderer.render(state)  # UI仅仅是数据的状态函数
```

抽象来看，这就是**数据驱动**的前端框架做的全部事情

---

### 内容、逻辑、样式
根据我自己的理论，一个**文档**可以分为三部分：

1. 内容 —— 将数据按一定格式呈现。比如一个`markdown`文档。这决定了它基本上得是**线性**的结构
2. 逻辑 —— 描述内容中元素间**非线性**的关系。比如预录制的ppt动作
3. 样式 —— 内容呈现的外观等形式。比如`markdown`的主题

而一个**应用程序**，它与文档不同的地方就在于，它是动态的，尤其是内容是动态的。然而动态中依然有静态的成分，这就是**格式**。

```plain
内容 = 格式 + 数据
```



而格式与样式有一定联系，有时候也会写在一起。比如html中inline的style，本质上就是样式写在格式里

#### WEB做对了什么
传统的`GUI`程序一般只会用它本身的编程语言来写，比较像`MVC`架构：**模型层**执行后端逻辑，**视图层**就是前端的格式和样式，**控制层**负责相应前端的事件。它没有区分格式和样式，所以在创建GUI代码中经常见到（用`Java`语法高亮的伪代码）：

```java
void greet(String name) {
    var vBox = new VBox(10, 20, 30, 40, Align.LEFT);
    this.root.appendChild(vBox);
    var label = new Label();
    label.setFontFamily("mono");
    label.setFontColor(Color.BLACK);
    label.setValue("Hello " + name + "!");
    vBox.appendChild(label);
}
```

可以认为，“加一段格式化后的文字”这一目标，是由数据生成内容；然而实现它的这些代码却要和样式混在一起，这没必要。而WEB就是提供了一种更理想的编程方式：

描述**内容**就用描述**格式**的语言，用代表**数据**的变量占位：

```jsx
<>
  <div>
    Hello, {name}!
  </div>
</>
```

**样式**则用键值对表示，而且可以表达级联的属性（例子中未体现）：

```css
div {
  font-family: monospace;
  color: black;
}
```

同样是9行，后者不比前者清晰明了多了吗

---

#### 数据驱动
相比之下，`MVVM`让开发者不需要关注底层的前端事件，**让框架自动实现数据与内容的同步**

在现代的WEB系统中：

+ 数据用键值对的方式表示，并用跨语言的编码方式传输（比如`JSON`，或者用可读性换效率的`msgpack`等）
+ 格式用基于`XML`的`HTML`语言描述
+ 样式用灵活的`CSS`描述，既可以内嵌于格式中也可以分离出来
+ 逻辑用动态类型的脚本语言`JavaScript`实现

`html`是UI的一种抽象表达了。与写GUI相比起来，WEB项目其实把渲染过程中排版的细节从应用程序中剥离出去了，开发者从编写UI变成编写html，真正由html渲染出图形的工作由浏览器来完成

> 我前段时间还在想，为什么网页要是`html`而不是`json`，毕竟写前端时真的有很多功夫都花在由`json`转`html`上，也不见得`html`比`json`可读性高。后来发现，其实`html`或者说`xml`是一种更面向对象的语言。我觉得更准确的说法是“面向类”，因为`json`其实就是一种描述对象的编码语言，但是它没有保留“类”的信息，而`xml`就有。
>

然而渲染html还得用户自己来搞，这还是挺讨厌的。下面以我在一个不用任何前端框架写的百科网站 [bnu120](https://bnu120.space/) 为例，下面是其中动态加载数据渲染视差字云效果的代码（略加修改，代码有点久远，可能写的比较幼稚）：

```javascript
const scene = document.getElementById("scene");
const depthScale = 100;

// 添加一个人物结点
function create_person_node(name) {
  let span = document.createElement("span");
  span.innerHTML = name;
  scene.appendChild(span);
  
  // 设置span元素随机深度，并将深度保存为元素的data-depth属性
  // Parallax插件会用这个属性制造视差效果
  let depth = Math.random();
  span.setAttribute("data-depth", (depth * depthScale).toString());
}

// 设置结点的位置并设置不透明度
function decorate_person_node(span) {
  let x = Math.random() - 0.5;
  let y = Math.random() - 0.5;
  span.style.left = `${x * window.innerWidth}pt`;
  span.style.top = `${y * window.innerHeight}pt`;
  // 设置元素的不透明度，形成先后出现的动画效果
  setTimeout(() => {
    span.style.setProperty("--opacity", Math.ceil(Math.random() * 100) + "%");
  }, (Math.random() ** 2) * 2000);
}

// 从api获取people列表
fetch("/api/people/list").then(response => response.json()).then(people => {
  people.forEach(create_person_node);
  new Parallax(scene); // 初始化Parallax插件
  scene.childNodes.forEach(decorate_person_node);
});
```

> 这段代码实现了一种类似视差效果的动画，通过获取API中的people列表，将每个人的信息和url添加到scene中，并且设置每个span标签的data-depth属性，从而实现视差动画。然后在scene中添加随机偏移量，最后设置每个span标签的各种CSS属性，实现动画效果。
>
> —— 一个类ChatGPT的AI生成的描述
>

可以看到，其实创建html标签的过程又回到前面Java的那个例子一样方式了

而用前端框架的模板语言要怎么实现这个呢

```html
<script>
	import { onMount } from "svelte";
  
  $: nodes = []; // 一个响应式变量

  onMount(
    async () => {
      let width = window.innerWidth;
      let height = window.innerHeight;
      let rand = () => Math.random() - 0.5;

      let response = await fetch("/api/people/list");
      let people = await response.json();
      nodes = people.map((name) => (
        { name, x: rand() * width, y: rand() * width }
      ));
    }
  );
</script>

<div>
	{#each nodes as { name, x, y }}
		<span style:x style:y> {name} </span>
	{/each}
</div>
```

清晰明了。`<script>`里获得并格式化数据，在底下用模板语言表达格式。这才是数据驱动的开发。完全不用关注与`dom`交互的细节

---

总之，在应用程序的语境下，**内容**就是**数据**和**格式**，**逻辑**就是**脚本**

这三种不同的东西可以用三种不同的语言来写，但不一定要写在三个文件里

而最开头提到的“UI描述”就是一类语言，专为描述UI而设计（废话），在其中可以用三种（子）语言来写UI的这四个部分

### UI描述的方式
回到最开头的

```python
renderer = framework.compile(template)
```

这里的`template`一般是html的扩展，例如上一个例子那样。它相当于把

描述UI有两种主流的语言大类：

1. 在内容中插入脚本 —— template
2. 在脚本中插入内容 —— jsx

虽然现在两边都很火，老牌有`React`属于第1类，有`Vue`属于第2类，新秀有`Svelte`属于第1类，有`Solid`属于第2类

在内容为重的应用中，一定是第1种更方便；而在逻辑为重的应用中可能第2种会略方便些（个人认为这种使用场景应该比较少）

---

不同框架实现上也有优劣。我们看起来只是在声明变量，实际上框架在帮我们修改DOM。修改的粒度越小运行时开销就越少。细化修改粒度有两种方向，可以在编译时找出会变化的量，运行时不再判断其它量是否更新；也可以在运行时将前后渲染的UI描述作`diff`。前者的唯一坏处就是会损失一些动态语言的灵活性，后者会带来`diff`的运行时开销，各有取舍（也可以兼而有之）

---

> 受启发于：《浅谈前端框架原理》 [🔗腾讯云开发者社区](https://cloud.tencent.com/developer/article/2211649) [🔗微信公众号](https://mp.weixin.qq.com/s/DDGAL0n0-RwVfrxzEELYgQ)
>