最近,我对对比框架和普通的 JavaScript 产生了浓厚的兴趣。这始于我在一些自由职业项目中使用 React 时遇到的一些挫折,以及我最近作为规范编辑,对 Web 标准有了更多的认识。
我希望了解一下这些框架的共性和差异,Web 平台作为一种更精简的选择,能提供什么,以及它是否足够。我的目标并非要抨击这些框架,而是要了解成本和效益,找出有没有其他选择,甚至当我们决定采用框架时,我们也能从中吸取教训。
在本系列文章的第一部分中,我将深入探讨一些框架的共性技术特性,并介绍几种不同的框架是怎样实现这些特性的。我还要看一下使用这些框架的成本。
框架
我选取四种架构进行研究。React 是当今的主流框架,还有三个较新的竞争者,它们声称自己的工作方式与 React 不同。
- React:“React 使创建交互式用户界面变得不费力。声明性视图使你的代码更可预测,更容易调试。”
- SolidJS:“Solid 遵循与 React 相同的理念……但它的实现方式完全不同,放弃了使用虚拟 DOM。”
- Svelte:“Svelte 是一种全新的构建用户界面的方式……是一个在你构建应用时发生的编译步骤。Svelte 不使用虚拟 DOM diffing 之类的技术,而是编写代码,当你的应用程序的状态发生变化时,外科手术式地更新 DOM。”
- Lit:“在 Web Components 标准的基础上,Lit 增加了……反应性、声明性模板,以及一些深思熟虑的特性。”
总结一下这些框架对其差异化的说法:
- React 通过声明式视图使构建 UI 更容易。
- SolidJS 遵循 React 的理念,但是采用了另一种技术。
- Svelte 处理用户界面采用了一种编译时的方式。
- Lit 使用现有的标准,并增加了一些轻量级的特性。
框架能解决什么问题?
框架自身也提及了诸如声明性、反应性和虚拟 DOM 等词。让我们深入了解它们的含义。
声明性编程
声明性编程是一种范式,在这种范式中,逻辑被定义,而没有指定控制流。我们描述需要的结果是什么,而不是我们会采取什么步骤。
在 2010 年左右,声明性框架的早期,DOM 的 API 更加简单,更加冗长。而使用命令式的 JavaScript 编写 Web 应用程序则需要大量的模板代码。这时,“模型 - 视图 - 视图模型”(model-view-viewmodel,MVVM)的概念开始盛行,当时具有划时代意义的 Knockout 和 AngularJS 框架,提供了一个 JavaScript 声明层,在库内处理这种复杂性。
今天,MVVM 并不是一个广泛使用的术语,它在某种程度上是旧术语“数据绑定”的变种。
数据绑定
数据绑定是一种声明性的方式,用来表示数据如何在模型和用户界面之间同步。所有流行的 UI 框架都提供了某种形式的数据绑定,它们的教程都以数据绑定的例子开始。
以下是 JSX(SolidJS 和 React)中的数据绑定:
function HelloWorld() {
const name = "Solid or React";
return (
<div>Hello {name}!</div>
)
}
Lit 中的数据绑定:
class HelloWorld extends LitElement {
@property()
name = 'lit';
render() {
return html`<p>Hello ${this.name}!</p>`;
}
}
Svelte 中的数据绑定:
<script>
let name = 'world';
</script>
<h1>Hello {name}!</h1>
反应性
反应性是一种声明性的方式来表达更改的传播。
如果我们能够用一种声明的方式来表示数据绑定,那么我们就必须要有一个使框架能够传播更改的高效方法。
- React 引擎会把渲染的结果与之前的结果相比较,并将差异应用于 DOM 本身。这种处理更改传播的方式,被称为虚拟 DOM。
- 在 SolidJS 中,这是以其存储和内置元素更明确地完成的。例如,Show 元素将跟踪内部的变化,而不是虚拟 DOM。
- 在 Svelte 中,生成“active”代码。Svelte 知道哪些事件会导致变化,它会生成直接的代码,区分事件和 DOM 更改。
- 在 Lit 中,反应性是通过元素属性来实现的,基本上是依赖 HTML 自定义元素的内置反应性。
逻辑
如果框架为数据绑定提供了声明性的接口,并且能够实现反应性,那么就必须提供一些方法来表达一些传统意义上的逻辑,这些逻辑是以命令的方式写的。逻辑的基本构件是 “if” 和 “for”,而所有的主流框架都提供了这些构件的一些表达。
(1) 条件句
除了绑定数字和字符串等基本数据外,每个框架都提供了一个“条件”原语。在 React 中,它看起来如下所示:
const [hasError, setHasError] = useState(false);
return hasError ? <label>Message</label> : null;
…
setHasError(true);
SolidJS 提供了内置的条件组件。
<Show when={state.error}>
<label>Message</label>
</Show>
Svelte 提供了 #if 指令:
{#if state.error}
<label>Message</label>
{/if}
在 Lit 中,你将在 render 函数中使用显式三元运算:
render() {
return this.error ? html`<label>Message</label>`: null;
}
(2) 列表
另一个常见的框架基元是列表处理。列表是用户界面的一个关键部分——如联系人列表、通知等——要想高效工作,就必须有反应性,而不是在一个数据项发生变化时,对整个列表进行更新。
在 React 中,列表处理看起来像这样:
contacts.map((contact, index) =>
<li key={index}>
{contact.name}
</li>)
React 使用特殊的 key 属性来区分列表项,它确保整个列表不会在每次渲染时被替换。
在 SolidJS 中,使用了 for 和 index 内置元素:
<For each={state.contacts}>
{contact => <DIV>{contact.name}</DIV> }
</For>
在内部,SolidJS 将自身的存储与 for 和 index 相结合,以确定在项目发生个更改时要更新哪些元素。它比 React 更清晰,使我们能够避免虚拟 DOM 的复杂性。
Svelte 使用 each 指令,该指令根据其更新器被转译:
{#each contacts as contact}
<div>{contact.name}</div>
{/each}
Lit 提供了一个 repeat 函数,它的工作原理类似于 React 的基于键的列表映射:
repeat(contacts, contact => contact.id,
(contact, index) => html`<div>${contact.name}</div>`
组件模型
有一件事超出了本文的范围,那就是不同框架中的组件模型,以及如何使用自定义 HTML 元素来处理它。
注意:这是一个很大的主题,我想在以后的文章里讨论这个主题,因为这个主题会让这篇文章变得太长。
成本
框架提供了声明性的数据绑定、控制流原语(条件和列表),以及传播更改的反应性机制。它们还提供了其他重要的东西,比如重用组件的方法,但这就是另一篇文章的主题了。
框架有用吗?是的。它们带给了我们所有这些方便的特性。但这是一个正确的问题吗?使用框架需要付出一定的成本。让我们来看一下这些成本。
包大小
在查看包大小时,我更愿意看到非 Gzip 的缩减大小。这个尺寸与 JavaScript 的 CPU 开销有很大关系:
- ReactDOM 大约是 120 KB。
- SolidJS 大约是 18KB。
- Lit 大约是 16KB。
- Svelte 约为 2KB,但生成的代码大小不同。
现在看来,在保持包大小上,现在的框架要优于 React。虚拟 DOM 要求使用很多 JavaScript。
构建
不知何故,我们习惯了“构建” Web 应用。如果不设置 Node.js 和 Webpack 这样的捆绑器,不处理 Babel-TypeScript 启动包中最近的一些配置更改,以及所有这些事情,就不可能启动一个前端项目。
越是有表达力的框架,包大小就会变得更小,但构建工具和转译时间的负担就越大。
Svelte 宣称,虚拟 DOM 完全是一种开销。我同意,但是可能像 Svelte 和 SolidJS 这样的“构建”以及像 Lit 这样的自定义客户端模板引擎都只是单纯的开销吗?
调试
在构建和转译过程中,需要付出的成本也是不同的。
我们在使用和调试 Web 应用程序时,所见到的代码和我们所编写的完全不一样。我们现在依靠同样品质的调试工具,逆向设计出一个站点,并把它和我们自己的代码中的 bug 相关联。
在 React 中,调用栈从来不是“你的”事情——React 会为你处理调度。这一特性在没有 bug 的时候非常好用。但是,如果你试图找出无限循环重现的原因,你将会陷入痛苦的境地。
在 Svelte 中,库本身的包大小很小,但你要传输和调试一大堆神秘的生成代码,这些代码是 Svelte 对反应性的实现,根据你的应用需求定制。
Lit 并不需要进行大量的构建,但是要想有效地进行调试,你就必须熟悉其模板引擎。这也许是我对框架持怀疑态度的最大原因。
当你寻求自定义的声明式解决方案时,你将面对更加困难的命令调试。本文中的示例采用了 TypeScript 来对 API 进行规范,但是该代码本身并不需要转译。
升级
在本文中,我讨论了四个框架,但是还有许多其他的框架,多得数不清(AngularJS、Ember.js 和 Vue.js,仅举几例)。你能指望框架、它的开发者、它的思想和它的生态系统在开发过程中为你工作?
除了修补自己的 bug 之外,还有一个更让人沮丧的事情,就是必须为框架的错误找到变通方法。而且,还有一个更加令人沮丧的事情,那就是在没有修改你的代码的情况下,将框架升级为新的版本,会出现 bug。
诚然,浏览器中也有这样的问题,但是这种问题一旦出现,就会影响到所有人,而且在大多数情况下,修复或者发布一个解决方案,都是迫在眉睫的。此外,本文提到的大部分模式都建立在成熟的 Web 平台 API 之上,并不一定都需要采用尖端技术。
总结
我们对框架所要处理的核心问题有了更深刻的理解,并且着重于数据绑定、反应性、条件和列表。我们也对成本进行了讨论。
在本系列的第二部分中,我们将会了解到,在没有框架的情况下,我们是怎样处理这些问题的,以及我们可以从中学习到什么。敬请关注!
原文链接:https://www.smashingmagazine.com/2022/01/web-frameworks-guide-part1/