muspi_merol / blog / alel05e9ibefc91r

最后更新于:3月22日

谈谈数据驱动的UI框架


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

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


内容、逻辑、样式

根据我自己的理论,一个文档可以分为三部分:

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

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

内容 = 格式 + 数据

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

WEB做对了什么

传统的GUI程序一般只会用它本身的编程语言来写,比较像MVC架构:模型层执行后端逻辑,视图层就是前端的格式和样式,控制层负责相应前端的事件。它没有区分格式和样式,所以在创建GUI代码中经常见到(用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就是提供了一种更理想的编程方式:

描述内容就用描述格式的语言,用代表数据的变量占位:

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

样式则用键值对表示,而且可以表达级联的属性(例子中未体现):

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

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


数据驱动

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

在现代的WEB系统中:

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

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

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

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

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的那个例子一样方式了

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

<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描述的方式

回到最开头的

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的运行时开销,各有取舍(也可以兼而有之)


受启发于:《浅谈前端框架原理》 🔗腾讯云开发者社区 🔗微信公众号