cover
· 原发布于 muspimerol.site

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


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

1
1