Web 框架解决了什么问题以及如何不使用它们(第 1 部分)
已发表: 2022-03-10我最近对将框架与普通 JavaScript 进行比较变得非常感兴趣。 它是在我在一些自由职业项目中使用 React 感到沮丧之后开始的,并且我最近更熟悉作为规范编辑器的 Web 标准。
我很想知道这些框架之间的共性和差异是什么,作为更精简的替代方案,Web 平台必须提供什么,以及它是否足够。 我的目标不是抨击框架,而是了解成本和收益,确定是否存在替代方案,看看我们是否可以从中学习,即使我们决定使用框架。
在第一部分中,我将深入探讨跨框架的一些常见技术特性,以及一些不同的框架如何实现它们。 我还将研究使用这些框架的成本。
框架
我选择了四个框架来研究:React,是当今占主导地位的框架,以及三个声称做事与 React 不同的新竞争者。
- 反应
“React 让创建交互式 UI 变得轻松。 声明式视图使您的代码更可预测且更易于调试。” - SolidJS
“Solid 遵循与 React 相同的理念……但是它有一个完全不同的实现,它放弃了使用虚拟 DOM。” - 苗条
“Svelte 是一种构建用户界面的全新方法……构建应用程序时发生的编译步骤。 Svelte 没有使用虚拟 DOM 差异等技术,而是编写了在您的应用程序状态发生变化时通过手术更新 DOM 的代码。” - 点亮
“在 Web Components 标准的基础上,Lit 添加了……反应性、声明性模板和一些深思熟虑的功能。”
总结一下这些框架对它们的区别的看法:
- React 使用声明式视图让构建 UI 变得更容易。
- SolidJS 遵循 React 的理念,但使用了不同的技术。
- Svelte 对 UI 使用编译时方法。
- Lit 使用现有标准,并添加了一些轻量级功能。
框架解决什么
框架本身提到了声明性、反应性和虚拟 DOM 等词。 让我们深入了解这些含义。
声明式编程
声明式编程是一种在不指定控制流的情况下定义逻辑的范例。 我们描述了结果需要是什么,而不是我们需要采取哪些步骤。
在声明式框架的早期,大约在 2010 年,DOM API 更加裸露和冗长,使用命令式 JavaScript 编写 Web 应用程序需要大量样板代码。 那时“模型-视图-视图模型”(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 中,会生成“反应式”代码。 Svelte 知道哪些事件会导致更改,它会生成简单的代码,在事件和 DOM 更改之间划清界限。
在 Lit 中,反应性是使用元素属性完成的,本质上依赖于 HTML 自定义元素的内置反应性。
逻辑
当一个框架为数据绑定提供一个声明式接口时,通过它的响应性实现,它还需要提供某种方式来表达一些传统上以命令式编写的逻辑。 逻辑的基本构建块是“if”和“for”,所有主要框架都提供了这些构建块的一些表达。
条件句
除了绑定数字和字符串等基本数据外,每个框架都提供“条件”原语。 在 React 中,它看起来像这样:
const [hasError, setHasError] = useState(false); return hasError ? <label>Message</label> : null; … setHasError(true);
SolidJS 提供了一个内置的条件组件Show
:
<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; }
列表
另一个常见的框架原语是列表处理。 列表是 UI 的关键部分——联系人列表、通知等——为了有效地工作,它们需要具有响应性,而不是在一个数据项更改时更新整个列表。
在 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 使用自己的 store 以及for
和index
来决定在项目更改时更新哪些元素。 它比 React 更明确,允许我们避免虚拟 DOM 的复杂性。
Svelte 使用each
指令,该指令根据其更新程序进行转译:
{#each contacts as contact} <div>{contact.name}</div> {/each}
Lit 提供了一个repeat
函数,其工作方式类似于 React 的基于key
的列表映射:
repeat(contacts, contact => contact.id, (contact, index) => html`<div>${contact.name}</div>`
组件模型
超出本文范围的一件事是不同框架中的组件模型以及如何使用自定义 HTML 元素处理它。
注意:这是一个很大的主题,我希望在以后的文章中介绍它,因为这篇文章会太长。 :)
成本
框架提供声明性数据绑定、控制流原语(条件和列表)以及传播更改的反应机制。
它们还提供其他主要内容,例如重用组件的方法,但这是另一篇文章的主题。
框架有用吗? 是的。 它们为我们提供了所有这些方便的功能。 但这是正确的问题吗? 使用框架是有代价的。 让我们看看这些成本是多少。
捆绑大小
在查看捆绑包大小时,我喜欢查看缩小的非 Gzip 大小。 这是与 JavaScript 执行的 CPU 成本最相关的大小。
- ReactDOM 大约 120 KB。
- SolidJS 大约 18 KB。
- 点亮约为 16 KB。
- Svelte 大约 2 KB,但生成的代码大小不同。
似乎今天的框架在保持包大小方面比 React 做得更好。 虚拟 DOM 需要大量的 JavaScript。
构建
不知何故,我们习惯了“构建”我们的网络应用程序。 如果不设置 Node.js 和 Webpack 之类的捆绑器,处理 Babel-TypeScript 启动包中最近的一些配置更改,以及所有这些爵士乐,就不可能启动前端项目。
框架的表现力越强,包大小越小,构建工具和编译时间的负担就越大。
Svelte 声称虚拟 DOM 是纯粹的开销。 我同意,但也许“构建”(如 Svelte 和 SolidJS)和自定义客户端模板引擎(如 Lit)也是一种不同类型的纯开销?
调试
建造和转译带来了不同的成本。
我们在使用或调试 Web 应用程序时看到的代码与我们编写的完全不同。 我们现在依靠不同质量的特殊调试工具对网站上发生的事情进行逆向工程,并将其与我们自己代码中的错误联系起来。
在 React 中,调用堆栈永远不是“你的”——React 为你处理调度。 当没有错误时,这很有效。 但是尝试确定无限循环重新渲染的原因,您将陷入痛苦的世界。
在 Svelte 中,库本身的包大小很小,但是您将发布和调试一大堆神秘的生成代码,这些代码是 Svelte 的响应性实现,根据您的应用程序的需求进行定制。
使用 Lit,它与构建无关,但要有效地调试它,您必须了解它的模板引擎。 这可能是我对框架持怀疑态度的最大原因。
当您寻找自定义的声明式解决方案时,您最终会遇到更痛苦的命令式调试。 本文档中的示例使用 Typescript 进行 API 规范,但代码本身不需要转译。
升级
在本文档中,我查看了四个框架,但框架的数量我数不胜数(AngularJS、Ember.js 和 Vue.js 等等)。 你能指望框架、它的开发人员、它的思想共享和它的生态系统随着它的发展而为你工作吗?
比修复自己的错误更令人沮丧的一件事是必须为框架错误找到解决方法。 比框架错误更令人沮丧的一件事是当您将框架升级到新版本而不修改代码时发生的错误。
诚然,这个问题也存在于浏览器中,但是当它发生时,它会发生在每个人身上,并且在大多数情况下,修复或发布的解决方法迫在眉睫。 此外,本文档中的大部分模式都基于成熟的 Web 平台 API; 并不总是需要紧跟前沿。
概括
我们更深入地了解了框架试图解决的核心问题以及它们如何解决这些问题,重点关注数据绑定、反应性、条件和列表。 我们还查看了成本。
在第 2 部分中,我们将了解如何在不使用框架的情况下解决这些问题,以及我们可以从中学到什么。 敬请关注!
特别感谢以下人员的技术审查:Yehonatan Daniv、Tom Bigelajzen、Benjamin Greenbaum、Nick Ribal 和 Louis Lazaris。