Javascript 框架的新浪潮

语言: CN / TW / HK

前言

通过深入研究大规模应用所面临的问题和最近的技术创新的演变历程,帮助我们理解新的 Javascript Web 框架激增背后的原因。

正文

保持对 Javascript 生态系统技术革新浪潮的跟进,并承受住它对你心智的冲击,这不是一般人胆小的人所能做到的。

对于进入该行业的人来说,要了解正在发生的新类库、新框架、新概念和包含强烈个人色彩的技术取向是一项挑战。

这是一个很好的提醒,如果你处于流血的边缘,你通常是流血的人。默认使用您熟悉的“无聊”技术,并且选择成为新技术后期采用者通常是一个不错的选择。 话虽如此,这篇文章将使我们了解 Javascript 生态系统中框架的前沿思潮。

我们将通过审视过去构建大型 Web 应用程序时的痛点来了解当前所发生的事情。

我们将深入研究当前存在的问题,而不是关注当前解决方案激增的现况。针对当前存在的问题,每个新框架给出不同的答案并做出不同的权衡。

到最后,我们将塑造适配于 React、Svelte、Vue、Solid、Astro、Marko、Fresh、Next、Remix、Qwik 等流行框架的高层级的心智模型,以及适合当今环境的“元框架”。

了解过去有助于理解现在。我们将从一段记忆之旅开始,看看我们曾经走过的路。这个故事以前讲过。这一次,我们将专注于那些曾经催生别样解决方案的大型项目问题和过程中的所思所想。

网页简史(A handwavy history of web pages)

网络最初是链接在一起的静态文档。有人可以提前准备一份文件,然后放到电脑上。

现在最酷的是其他人可以访问它——无需将他们的身体移动到相同的地理位置。挺厉害的。

在后来的某个时间点,我们认为让这些文档动态化会很酷。

我们获得了像 CGI 这样的技术,使我们能够根据请求提供不同的内容。

然后我们得到了像 Perl 这样富有表现力的语言来编写这些脚本。Perl 影响了第一种为专为 Web 构建的语言 - PHP。

PHP 的一个很好的创新是将 HTML 直接连接到这个后端代码。它使得以编程方式创建嵌入动态值的文档变得容易。

网络最重要的突破之一就是由此而来: html <html> <body> This document has been prepared ahead of time. Regards. </body> </html>

要想轻松嵌入动态值,这么干: <html> <body> Y2K? <?php echo time(); ?> </body> </html>

潘多拉的盒子打开了

这些动态页面很受欢迎。我们现在可以轻松自定义发送给用户的内容,包括启用会话的 cookie。

基于服务器的模板框架出现在现在与数据库对话的语言生态系统中。这些框架使得从静态页面开始并扩展到动态页面变得容易。

网络发展迅速,我们想要更多的互动体验。为此,我们使用了 Flash 等浏览器插件。对于界面上其他所有内容,我们会在后端提供的 HTML 上插入 Javascript 片段来实现界面的交互性。

像 jQuery 和 Prototype 这样的工具突然出现并磨平了 Web API 的许多粗糙简陋之处和竞争浏览器之间的各自为政。

科技的变革变得越来越快。科技公司变得越来越大,随着项目和团队的成长,更多的业务逻辑被塞进这些模板里面,这在当时是很常见的。

编写服务器代码将数据塞入到服务器模板语言中。这导致了模板最终演变成可以访问全局变量的杂乱业务逻辑所在之处。另外,安全性正在成为一个问题,SQL 注入等攻击司空见惯。

最终我们迎来了“Ajax:一种 Web 应用程序的新方法”

您现在可以做的新事情是异步更新页面,而不是同步刷新(所谓的同步,就是指全量刷新整个界面)。

这种模式在 Google maps 和 Google docs等第一个大型客户端应用程序中得到普及。

我们开始看到网络分发类似桌面应用风格软件的力量。相比于只能在桌面系统的应用商店上去购买软件相比,这是向前迈出的重要一步。

Javascript 的体量越来越大

当 nodeJS 出现时,它启用的新功能是使得前端开发人员能用具有自己熟悉的异步优先模型的,与前端相同的语言,即 javascript,编写后端程序。

这曾经(并且现在)令人信服。随着越来越多的企业级应用上线,javascript 的竞争优势在于能够快速部署和迭代。

Node 的生态系统强调重用小型的,用途单一的软件包,你可以 npm 仓库即下即用,快速地完成你的工作。

前后端分离模式

我们对可以与台式机和移动设备相媲美的网络的需求持续增长。我们现在拥有了一系列可重用的“小部件”库和实用程序,例如 jQuery UI、Dojo、Mootools、ExtJs 和 YUI 等。

我们对在后端插入的 javascript 越来越重视,与此同时,前端需要做的东西也越来越多。这通常会导致在前端和后端存在重复模板。

诸如 Backbone 和 Knockout 之类的框架以及许多其他框架就在这个时候出现了。他们通过 MVC、MVVM 等将关注点分离添加到前端。前端开始引入架构,并且与我们收集的所有小部件和 jQuery 插件兼容。

代码结构化有助于扩展所有这些前端代码,并加速模板从后端转移到前端。

我们仍在编写微调型(fine-tuned) DOM 操作代码来更新页面并保持组件同步。这个问题很重要,并且界面与数据不同步的相关错误开始常见。

由谷歌支持的 Angular 登上了现场。它通过增强 HTML 的动态性来提高生产力。它带有双向数据绑定,以及受 spreadsheets 启发的响应式系统。

这些声明性的双向绑定删除了强制更新数据的大部分样板。这很好,让我们更有效率。

随着应用规模的扩大,开发者发现很难追踪发生了什么变化,并且经常导致遭遇性能不佳。主要表现在生命周期相关的代码执行时,总是占用主线程太久(如今,像 Svelte 这样的库在减轻其缺点的同时保持双向绑定)。

除了移动网络的兴起,这些提高生产力的框架也加速了前后端分离。这为探索强调这种分离模式的不同架构铺平了道路。

这是 JAMstack 哲学的主要部分,它强调提前准备 HTML 并从 CDN 提供它。当时来看,这是对提供静态文档的一种回归。

但是现在我们有了一个基于 git 的工作流程、强大的 CDN 基础架构,它不依赖于远程的集中式服务器,以及将前端解耦出来,使得前端能单独跟 API 服务进行通讯。在 CDN 上抓取静态资产的运营成本远低于运营服务器。

今天,像 Gatsby、Next 和许多其他工具都利用了这些想法。

超新星 - ReactJS 的崛起

扬手一挥,快速进入大科技时代。我们正在努力快速变革,摧毁旧事物。

对于那些进入这个行业的人来说,Javascript 很重要,构建一个由分离的后端服务支持的解耦 SPA 正在成为现状。

React 在 Facebook 诞生之初面临一些挑战:

  1. 数据频繁更改时所产生的一致性问题:保持许多小组件彼此同步仍然是一项重大挑战。数据流缺乏可预测性使这在规模上成为问题。

  2. 在组织上可扩展性问题:上市时间和速度是优先考虑的。让刚入职的新开发人员能够快速上手并做出富有成效的工作成绩是至关重要的。

React 诞生了,你可以做的很酷的新事情就是以「声明方式」编写前端代码。

众所周知,如何实现前端侧的关注点分离是被重新论证过的,以前的 MVC 架构无法扩展

如此一来,我们就从「模板」升级到 「Javascript 驱动的 JSX」。 JSX 最初是被讨厌的。但我们大多数人都挺过来了,用多了也就习惯了。

组件模型允许前端团队之间彼此解耦,彼此可以更轻松地并行开发独立组件。

作为一种架构,它允许对组件进行分层 - 从共享的原子组件到多个原子组件组成的“有机体”组件,它们向上组合,共同组成以根组件为代表的完整页面。

单向数据流使数据流更易于理解、跟踪和调试。它增加了以前很难找到的可预测性。

虚拟 DOM 意味着我们可以编写返回 UI 描述的函数,并让 React 找出不同点,根据不同点来做最小的 DOM 更新。

这解决了数据频繁更改时的一致性问题,并使模板的组合更加符合人体工程学。

大规模的 React 应用触碰到 CPU 和 网络的瓶颈

React 很受欢迎,并已成为行业标准——通常甚至对于不需要其功能的网站也是如此。在大规模应用中,我们开始看到一些它的一些不足点。

对抗 CPU(Running up against the CPU)

DOM 是 React 渲染模型的一个问题。浏览器并不是为了在同一个时间段内频繁地创建和销毁 DOM 节点而创建的。

就像任何可以通过引入新的软件层来解决的问题一样,React 在用户代码和 DOM 之间加入了一个抽象层 - 虚拟 DOM。

人们需要在 100 毫秒以内感知到反馈,才觉得事物是顺畅。在做滚动之类的事情时这个时间范围还要小得多。

结合单线程环境,这种优化成为高交互应用的新瓶颈。

当虚拟 DOM 和真实 DOM 之间发生协调时,大型交互式应用程序对用户输入变得无响应。耗时任务(long task)等术语开始出现。

这导致了react 团队在 2017 年对 React 进行全面的重写,这次重写引入并发模式的基础 - fiber reconciler。

运行时成本不断增加

与此同时,移动网络的速度更快了,这意味着通过网络能够传输更多代码。在典型的 SPA 应用中,由于浏览器必须先下载和执行 Javascript,应用首屏渲染速度慢越发明显。

我们开始注意到所有隐含的运行时成本,不仅是 HTML 和虚拟 DOM,还有我们编写 CSS 的方式。

组件模型改进了我们使用 CSS 的经验。我们可以将样式与组件放在一起,从而提高可移除性。对于之前害怕删除 CSS 代码的人来说,这是一个很棒的特性。

我们一直在遭遇的级联覆盖和所有它的特殊性问题都被 css-in-js 类库所抽象和解决掉了。

初生代的 css-in-js 类库通常伴随着看不见的运行时成本。在将这些样式注入页面之前,我们需要等到组件被渲染。这导致样式相关的 concern 被纳入 Javascript 包中。

在大规模应用上,糟糕的性能通常会导致一千次应用削减的死亡,我们注意到了这些成本。这导致了新一代的 css-in-js 类库专注于通过使用智能预编译器来提取出样式表,从而来减少运行时间成本。

网络效率低下和渲染阻塞型组件

当浏览器渲染 HTML 时,诸如 CSS 或脚本之类的渲染阻塞型资源会阻止 HTML 的其余部分显示。

父组件经常会阻塞组件层次结构中子组件的渲染。在实践中,许多组件依赖于来自数据库的数据和来自 CDN 的代码(通过代码拆分)。

这通常会导致同步的,阻塞型网络请求的 waterfall。某个组件在渲染后获取数据之后,解锁异步子组件的渲染。然后改子组件获取他们需要的数据,再解锁自己的子组件的渲染,一层一层地在重复这个过程。

通常会看到 「spinner hell」或「cumulative layout shifts」,其中一些 UI 在加载时会弹出到屏幕中。

React 已经发布了 Suspense 来帮助优化页面在加载阶段的加载体验。但默认情况下,它不会阻止同步的 waterfall。 用于data fetching 的 Suspense 允许“render as you fetch”的模式。

那么,Facebook 是怎么处理这些问题呢?

我们将继续曲线救国,以了解 React 是如何通过一些权衡取舍来缓解其在大规模应用所暴露的问题。这将有助于理解新框架所采用的新构建模式。

  • 优化运行时成本 - 在 React 中,虚拟 DOM 的运行时成本是不能不谈的。Concurrent mode 是在高频交互的界面体验中保持页面的可响应性的解决方案。在 css-in-js 领域,使用了一个名为 Stylex 的内部库。当呈现数千个组件时,这可以保持符合人体工程学的开发人员体验,而不会产生运行时成本。

  • 优化网络 - Facebook 使用 Relay 避免了顺序网络 waterfall 问题。对于给定的入口点,静态分析准确地确定需要加载哪些代码和数据。 这意味着代码和数据都可以在优化的 graphQL 查询中并行加载。 对于首屏页面加载和 SPA 的路由跳转等场景,这比顺序网络瀑布要快得多。

  • 优化 Javascript bundle - 这里的一个基本问题是发布与特定用户无关的 Javascript。 当有针对特定类型和用户群的 A/B 测试、功能标记体验和代码时,这很难。还有语言和本地化设置。

当有许多代码分支时,静态依赖图无法看到在实践中针对特定用户群一起使用的模块。

Facebook 使用人工智能赋能的动态打包系统。这利用其紧密的客户端-服务器集成来根据运行时的请求计算最佳依赖关系图。这与基于优先级分阶段加载代码包的框架相结合。

生态系统的其他参与者是什么情况?

Facebook 拥有复杂的基础设施和多年来建立的内部类库。如果你是一家大型科技公司,你可以投入大量资金和资源来优化这些权衡取舍。

这为前端产品开发人员在保持性能的同时完成工作创造了一个成功的坑。

我们中的大多数人都没有构建一套类似于 Facebook 规模的应用程序需求。尽管如此,在许多大型组织中,性能仍然是热门话题。我们可以从这些模式中学习——比如尽可能早地获取数据、并行化网络请求以及使用「行内导入」等。

大型科技公司经常在内部推出自己的应用程序框架。将许多解决方案分散地分布到社区中的各个用户代码中。这导致许多人犯上了「javascript 生态和框架疲劳症」。

Javascript 的世界:分散、分裂、群龙无首

跟随这混乱分裂的 javascript 社区前行,你还能坚持住吗?如今,我们正处于 SPA 时代。这就是进入这个行业的人的现状。

React 是无可争议的冠军,但是我们也看到了它在大规模应用中的权衡取舍。

React 提供了一层。将其他必要的层留给生态系统,导致开发 react 应用的其他重要方面都没有官方定论,处于百家齐放状态:路由、状态管理、数据获取等,社区中的每个解决方案都有自己的概念和 API。

不可变性 VS 可变性,OOP 范式 VS 函数式范式,无休止的辩论和新类库层出不穷。

时至今日,许多开发人员都受困在关于如何做选择以及如何架构应用等类似的不确定性中。

冉冉升起的 React 的替代方案

组件是构建页面应用的基础单元,这是一个业界公认的理念了。但是运行时成本、Javascript 驱动的 JSX 和技术复杂性都有待商榷。

许多并非来自大型科技公司的“草根替代品”已经获得了广泛的关注。让我们对它们进行一个超高级的概述:

Vue

当人们评估迁移到 Angular 2 还是 React 时,Vue 填补了入门门槛低的空白。

你不必触碰复杂的 webpack 配置。您可以从 CDN 中直接下载它,并使用对许多开发人员来说直观的「模板」来构建组件。

Vue 的核心团队推出了官方的路由类库和 css-in -js 类库等核心组件,从而减少决策疲劳。

它还通过对模板使用静态分析来优化 React 协调算法(reconciliation algorithm)的某些方面,从而实现更快的运行时间。此称为: compiler-informed virtual DOM

Svelte

Svelte 开创了预编译方法,消除了我们在运行时看到的复杂性和开销。 这个想法是创建一个拥有可以自行编译的框架,并具有最小的原生 JavaScript 的打包输出。同时保持基于声明式组件的现代前端应用编写体验和熟悉的 Javascript mutable 风格。

它完全避开了虚拟 DOM,因此不受只能使用不可变的 Javascript 编写风格来执行状态更新的约束。对于许多人来说,这是一种在网络上构建东西的更简单和更理智的模型。

solid

受 Knockout 的启发,Solid 带有一个简单且可预测的响应式模型。与 React 采用 JSX 一样,它避开了模板以便于利用函数的可组合性。

而与 React 采用不断地 re-render 的实现不同, Solid 只渲染一次组件,然后使用流线型的(streamlined)响应式系统进行细粒度更新,没有采用虚拟 DOM, 故没有虚拟 DOM 运行时的开销。

Solid 看起来像我们许多 React 开发人员希望我们在使用 hook 的时候应该表现的样子。它的 API 可能更符合人体工程学,可以抹平很多东西,比如 hook 的依赖数组,它专注于细粒度的响应式和可组合的基础语义(fine-grained reactivity and composable primitives)。

相互借鉴

关于这些框架中的每一个,还有很多要说的点。每个框架都根据其基础模型和偏好做出不同的权衡取舍。

实际上,进化通常来自于人们随心所欲地进行进化。尝试针对当前痛点去尝试使用不同解决方案,每个框架都相互学习。

一个大主题是精简和简化。将事务从运行时移到编译时是这些主题之一,启发了“React forget”这种编译器,这是一个可能消除对 memoization 的需求的功能。

它们的共同点是解决与 DOM 的交互部分。正如我们所看到的,要以一种易于扩展的方式解决,这是一个具有挑战性的方面。

同时,我们看到了纯客户端渲染的利弊。加载页面时的空白屏幕需要更长的时间。在移动设备和网络上,这有点像一场灾难。

对于许多网站来说,快速迭代中保持高性能成为主要的竞争优势。

我们迈出了一步,正在探索通过首先在服务器上渲染来加速首页的加载速度的方法(需要意识到的一点是,这也是一种权衡取舍)。

这个类似于回退的思路为许多“元”框架(“meta” frameworks)和新一波 HTML 优先的前端框架开辟了道路。

Javascript Web 框架的新思潮

We shall not cease from exploration. And the end of all our exploring will be to arrive where we started. And to know the place for the first time.

受 PHP 的启发,Next 加快了创建推送到 CDN 的静态页面的流程。它还抹平了在 React 应用程序中使用 SSR 的麻烦部分。

它给社区带来一些类似于「基于文件的路由系统」和「如何组织应用代码」等非常需要的技术方案。还有许多其他不错的功能。

从那时起,一大波其他“元”框架被创造出来。对于 Vue,我们有 Nuxt。 Svelte 有 Sveltekit,以及 Solid 有即将到来的 SolidStart。

这些框架都是服务器优先的,旨在集成 Web 框架的所有部分和人体工程学,而不仅仅是长期以来备受关注的界面交互元素。

社区的技术话题开始变成关于如何同时改善「用户体验」和「开发人员体验」,而不是二选一。

MPA 们的反击

从服务器提供 HTML的多页面架构应用的一个突出的特性就是 - 服务端路由 + 页面重载。

首屏的加载速度对于许多站点来说至关重要,尤其是那些没有登录的站点。它与搜索排名和跳出率等直接相关。

对于许多交互性低的网站和应用程序来说,使用像 React 这样的客户端渲染库来渲染页面是大材小用了。

对于许多人来说,这意味着降低 JS 在渲染工作中的比重。选择HTML 优先而不是 Javascript 优先,MPA 优于 SPA,并且将首屏的 javascript 零加载视为明智之选。

MarkoAstroFreshRocketEnhance 等框架都采用这种方法。

与某些元框架相(meta framework)比,以上这些框架把路由器保留在服务器上,而不是在第一次加载后让客户端路由器接管。

在 Javascript 生态系统中,这是在 Node.js 之后不久回归到服务端模板的阶段。

这一轮 MPA 框架与前几代是不同的,具体表现为两个方面: - 实现网页交互性的 js 代码是基于 component-based 来编写的,在此基础上通常还使用 islands pattern。 - 在前端和后端代码中使用相同的语言。通常同一个文件的代码可以运行到客户端和服务端。当添加一些交互性时,这消除了在前端和后端以不同方式构建模板所带来的重复性代码问题。

渐进增强理念的回归

Remix 给 React 生态系统带来了「渐进增强」理念的回归。

从技术角度来看,它是 React Router 的编译器,与其他新兴的元框架一样,是一个边缘技术兼容的运行时。

它通过嵌套布局和数据获取 API 解决了 Facebook 使用 Relay 来解决的大规模应用所面临的相同挑战。

这允许提前对代码和数据的并行获取。这是 Suspense 的“渲染时获取(fetch as you render)”模式的一个很好的先决条件。

对渐进增强的支持意味着它拥有基于 Web 标准的 API 和基于原生 HTML 表单的表单操作体验。

在 remix 中,你不需要通过事件处理程序来发出命令式的获取请求。您将用于提交数据的表单渲染到 action 函数中,最终是在服务器上去处理它们(通常在同一个文件中 )- 受 PHP 启发。

与 Next 类似,应用程序可以降级到没有 Javascript 的情况下工作,就像传统的服务器渲染 MPA 一样。而在 javascript 可用的情况下,可以让应用中的每一个页面升级到基于 react 的 SPA。

Remix 还提供了许多 API 和模式来处理诸如乐观 UI 更新(optimistic UI updates)、处理竞争条件(race conditions)和优雅降级和一些您希望一个专注于最终用户体验的深思熟虑的框架能够提供之类的特性。

一个混杂的未来

不要与 Quic 协议混淆。 QwikJS 框架就是为了尽量减少不必要的 Javascript而生的。

虽然它的 API 看起来像 React,但它的方法与其他元框架的不同之处在于它专注于 hydration 过程。

就像您可以暂停虚拟机并将其移动到不同的物理机一样。 Qwik 将这个想法应用于服务器和浏览器之间发生的工作。

「“可恢复”hydration」(resumable hydration)的想法意味着您可以在服务器上启动某些东西并在客户端上继续执行来恢复它而无需任何重复执行一遍。

这与「局部 hydration」 形成对比,局部 hydration在水合作用工作发生时动态变化,Qwik 一开始就试图避免这样做。

这是一组有趣的想法,利用了服务端和客户端的同构能力来实现了动态打包和服务。

这些概念开始模糊 MPA 和 SPA 之间的界限,其中应用程序可以从 MPA 的形态开始,并根据用户交互动态过渡到 SPA。有时(在更多流行语中)被称为“可变换的应用程序”(transitional apps)。

边缘技术的现状

与此同时,后端基础设施和托管技术也在不断改进。

位于边缘的 CDN 使我们的 SPA 静态资产服务变得轻松快捷。现在将运行时和数据移动到边缘也变得可行。

这是在浏览器之外创建一个新的运行时层,但仍尽可能靠近用户。这使得将当前在浏览器中完成的许多事情移回服务器变得更加容易。同时在一定程度上减轻了以往这样做所带来的网络延迟的代价。

像 React 服务器组件(react server component)这样的想法正在探索将服务器组件从这一层输出到浏览器的技术的可行性。

新的 Javascript 运行时间(如 Deno 和 Bun)正在出现,以简化和提高 Javascript 生态系统的生产力,并且这是为这个新的边缘运行时世界而构建的,目的是优化访问速度和首屏加载时间。

有了先行探索出的 serverless functions 和流式架构,让一些标准规范里面的 web API 运行在这一层成为了可能。

Streaming 是这里的一个大主题。它提前输出 HTML,因此浏览器可以边接收边渲染。在获取服务端数据的同时,开始消费任何渲染阻塞型的资源,如 CSS 和 JS。这有助于并行化许多其他顺序加载的 waterfall 行为。

总结

我们覆盖了很多领域,但是几乎都没有深入,都只是点到即止。

对于这篇文章中提到的最好的框架、架构或模式以及我们没有提到的无数其他问题,没有一个通用的答案。

每个解决方案都始终是针对特定指标和场景的的权衡取舍。想要知道需要做出什么权衡取舍完成取决于你的应用场景(正在构建什么,您的用户是谁,以及他们的使用模式)和关心围绕关键用户体验(如性能预算)去指定什么样的指标。

对于我们大多数人来说,真相也许会横跨在几个方案之间。新一波框架和创新的伟大之处在于,它们提供了适用于各种规模应用(从小规模到大规模,从静态应用到动态应用)的杠杆。

对于那些刚刚踏进这个行业的人和那些经验丰富的人来说,投资基本面总是一个不错的选择。

框架的演变慢慢地推动了原生 web 技术,消除了框架之前为了兼容各种环境所需要做的工作,并减轻了之前我们需要做出的权衡取舍 - 现在我们可以越来越多地使用一些原生 web 支持的特性了。

资料