驾驭前端未来

了解现代前端元框架。将新旧基本概念之间的点连接起来。

前端生态系统正处于转型期。崭露头角的前端人员在充满竞争框架、概念、倡导者、偏好和最佳实践的复杂环境中游刃有余。

今天,网络和网络技术运行着许多最常用的软件。从 Java 小程序到 Flash,再到 Javascript 的快速发展,已经构建了无数工具来管理各种浏览器、屏幕尺寸、设备功能、网络条件和不断增长的用户期望。但是,在追求成熟的软件分发平台的过程中,网络的基本限制并没有改变。在这篇文章中,我们将探讨现代 Javascript 元框架如何驾驭这些约束。我们将建立一个高级心智模型,了解编译、路由、数据加载和突变、缓存和失效等基本部分是如何组合在一起的。这将帮助我们理解新兴的架构模式,包括 React Server 组件提供的功能。尽管魔鬼在细节中,但我们将看到框架之间的融合比我们看到的要多。到最后,我们将掌握超越框架和表面 API 兴衰的基本概念,并更好地了解前端生态系统的发展方向。

框架的兴衰

网络平台是用户在其上构建的工具之下的一个缓慢发展的综合层。随着时间的推移,平台不断完善,不再需要更高层次的抽象。但是平台并不总是提供必要的能力。Flash消亡后,JavaScript生态系统填补了空白,成为构建丰富互动体验的首选技术。

过去十年间,科技行业的蓬勃发展促使许多组织将网络作为主要的直接面向客户的分销渠道,与以前的网络开发人员相比,这导致前端开发专业化,类似于富客户端桌面开发。

当今流行的框架,如 Angular、React、Vue、Svelte、Solid、Qwik 等,解决了客户端交互问题,同时能够组合在数据变化时一致呈现的组件。

这些细节导致各种权衡,因此它们仍然是有争议的,比如处理反应性的最佳方式、模板与函数的偏好,或对复杂性的渴望等。但是,从高层次上看,这些技术都在相似的概念上趋于一致,并提供了类似的能力。

一个重大的主题是元框架复杂性的日益增强,这些元框架在应用程序级框架之上构建。我们先从理解两个通用周期开始调查。

能力与适用性

工具的能力是表达想法的能力。随着它获得新功能,工具变得更加复杂。

工具的适用性在于它是否适合特定情况的用途。在组织环境中,这意味着让尽可能多的人满意,而不会经常遇到陷阱或脚步。

提高能力的创新周期也会导致复杂性和混乱。这导致了构建抽象的循环,这些抽象支配着功能,或者使它们更易于使用。反趋势经常出现在支持更直接方法的回应中。

Bundling the un-bundled

Javascript 疲劳现象来自于必须在多种技术的不同功能之间做出决定,并让它们协同工作。

Bundling 是指我们将这些功能连接到一个产品中,从而使使用它们更易于访问。

Bundling 抽象必须同时解决许多问题。因此,它们往往会变得很大,并且具有抽象和魔法的层次,这往往会让开发人员感到不舒服。

当这些被认为太慢或太有限时,一个新的循环就开始了,在这个循环中,各个部分被打破并孤立地进行创新。

正是通过这种持续的迭代和随着时间的推移经历陷阱的痛苦,我们才知道什么是重要的,什么是不重要的。

经过几年的客户端组件框架迭代,每个框架都有自己的“元”框架,捆绑了不同的工具和最佳实践。React 的许多新 API 都是非捆绑功能,旨在集成到这些更高级别的应用程序框架之一中。

返璞归真

计算机系统的两个主要组成部分是计算和存储。

系统的性能受到计算操作和 I/O(输入、输出)操作(如从存储中获取数据)的限制。

在 Web 上,这些基本约束表现为 Javascript 的单线程以及用户和服务器之间的网络延迟。

在 Web 上的所有连接都涉及遵循请求-响应模式的客户端和服务器。服务器始终是首选的调用端口(即使它正在从CDN提供静态文件)。我们在服务器和客户端之间分配这些操作的方式会导致不同的权衡。

客户端上的计算能够实现快速交互,但计算过多,主线程就会失去响应。服务器不受此限制,但要求其执行任务会引发网络延迟。在网页交互式体验中平衡这些限制至关重要。

阅读和写作

另一个重要因素是读取和写入数据的能力。

Web最初只是只读的静态文档。最终,我们开始持久保存数据并动态生成HTML。有了表单元素这个得力的工具,我们现在可以进行写操作,扩展Web对新型应用的能力。

在此模型中,客户端上的用户交互转换为同步 HTTP 请求。写入后对体验的更新是粗粒度的,因为服务器会使用新生成的文档进行响应。浏览器重新加载自身以显示它的位置。

最终,我们得到了 XMLHttpRequest,它开放了更多功能。用户操作现在可以是异步的,其中更新是细粒度的,其中只有页面的相关部分更新。

在这两种情况下,呈现的内容和由服务器驱动的应用程序状态的真实来源。

到现在为止,我们已经很了解这个故事了。随着时间的流逝,模板和应用程序状态越来越多地转移到客户端,其中应用程序状态变得由客户端驱动,从而允许快速乐观地写入,从而掩盖底层网络。

这是过去十年中许多进入该行业的人所采用的主导模式。在一个速度为王的行业中,随着功能的增长,所有代码只能放在一个地方。

当使用单机的方法受到性能限制时,我们可以放弃它;否则,我们就进入了分布式系统的领域。

分布式前端

纯客户端方法的心智模型就像一个长时间运行的桌面应用程序,它与后端异步同步。

向服务器驱动的应用程序状态的转变是最重要的心智模型变化之一,因为大多数“前端”代码都移回了服务器。

不同生态中其他服务器优先应用框架的区别在于,保持了丰富的客户端交互性和稳定导航的能力。

这种混淆源于知道何时以及如何在同一框架和产品中利用服务器驱动模式的性能特征和客户端驱动方法的功能。

React Server 组件更进一步,追求跨服务器和客户端交织的可组合组件的统一创作体验。对于该行业最主要的框架来说,这是一个重大转变。

其他语言生态系统也在探索类似的概念。例如,C# 的 Blazor、Clojure 的 Electric 和 Rust 的 Leptos 也追求类似的想法。

在我们迷失于细枝末节之前,让我们退后一步,理解为什么现在会出现这种情况?

除了更高的性能之外,让我们了解一些关键因素,这些因素使我们在 Javascript 生态系统中找到了 Web 开发的新方向。

一统天下的语言

作为 Web 的通用语言,没有其他语言像 JavaScript 一样通用

当Node.js出现时,我们可以编写在客户端和服务器上运行的同构代码。像 Meteor 这样的早期开创性全栈框架采用了这些功能。

Meteor 是具有全栈响应性和 RPC 的同构 Javascript 框架的早期示例,它抽象了浏览器和服务器是截然不同的环境这一事实。

当时,这种一体化的方法失去了行业市场份额,被不那么固执和更自由的方法所替代,比如 React,作为一个最小的视图库。

自那时起,TypeScript 就产生了巨大的影响,成为许多开发人员和组织的事实标准。

像tPRC和T3堆栈这样的工具使用同构的TypeScript,通过统一代码、类型、模式和执行模型,在相同的存储库中提供端到端的类型安全性。

下一代编译器和打包器

我们可以将编译器视为转换、准备和优化我们编写的代码以便以后执行的程序。我们在“大规模构建和交付前端”中讨论了它在网络上的具体作用。

打包器和编译器技术的稳步发展导致了从头开始重写的快速下一代打包器的迭代,这些打包器非常擅长管理 Javascript 模块图。

这些功能允许框架为在不同运行时执行的客户端和服务器分离模块图。

这种代码提取的想法为统一的客户端-服务器创作体验提供了动力,以及需要在幕后发生的许多魔力。

Suspense

了解 Suspense 解锁的功能是掌握 React 框架中出现的服务器优先心智模型的关键。SolidVuePreact 和 Astro 等其他框架正在探索变体。

用户体验的角度来看,关键的见解是,我们可以更有意地设计数据密集型体验的加载阶段。

性能角度来看,一个关键的见解是 Suspense 提供了资源加载和渲染的并行化。

受 Facebook BigPipe 概念的启发,它减轻了服务器在浏览器闲置时获取数据和渲染 HTML 的同步等待时间。

客户端可以开始下载字体、CSS 和 JS 等资源,因为它在浏览器解析 HTML 时遇到这些标签。同时服务器并行加载数据。

这降低了TTFB和纯服务器驱动“先获取后渲染”模型中较慢的最大内容绘制的影响。

但是,与简单地<head>早期并从客户端异步加载所有内容相比,这可以通过对分阶段加载阶段的细粒度控制来完成。与此相反,随着数据和代码在瀑布中加载,大量不请自来的悸动在页面内外“爆米花”进出,从而导致累积的布局偏移

除了最初的页面加载外,它还允许RSCs(远程组件)为原地过渡提供序列化的虚拟DOM流。从技术角度来看,这里的关键在于,Suspense 解决了异步渲染和可无序流式传输时的一致渲染问题。

框架的反应式系统解决的一个核心问题是如何确保用户界面在数据随时间变化时呈现一致。

一致性意味着显示的内容准确反映了当前的真理来源。它确保 UI 不会显示过时的数据或与其他使用相同数据的元素显示的数据不同。

虚拟DOM和信号都是解决这个问题的方法。简单来说,这两种方法的不同之处在于,虚拟DOM是粗粒度的,因为它“对比视图”,而信号则是细粒度的,并且“对比模型”。每种方法都有其不同的权衡。

Suspense 从另一个角度解决了渲染一致性问题,当组件树通过网络加载时,资源作为 I/O 绑定操作异步加载。

这意味着我们可以从不同的数据源流式传输响应,而不必手动管理竞争条件,也不必在网络上无序到达时将占位符 DOM 内容交换为最终的 DOM 内容。

它还可以巧妙地与构建时编译器结合使用,以创建新的渲染方法,例如部分预渲染

基础设施的进步

虽然这些功能已经在 JavaScript 生态系统中酝酿,但为 Web 提供支持的云基础设施也在迅速发展。从服务器运行时向浏览器进行流式传输等能力需要后端基础设施支持。

许多服务器基础结构提供商现在都支持这种功能。随着无服务器和边缘计算的普及,我们看到新的运行时的创建如DenoBuntxki.jsLLRT的出现,这些运行时能够在边缘和无服务器环境中快速启动和运行,并实现 fetch 和 ReadableStream 等 Web 标准 API。随着精品“前端云”提供商的兴起,其解决方案抽象出所有底层基础设施的复杂性

路由器一路向上

路由是堆栈上下的基础。互联网和网络可以看作是一系列路由器。路由器也是任何框架的骨干。它是编译器在生成时的第一个入口点,用于初始请求,以及之后许多用户交互的目标。

URL(和二维码)的便利性和可共享性是网络作为软件分发机制成功的基础。路由器是 URL 与需要加载的代码和数据之间的连接器。

将路由器视为 URL 的状态管理器,将路由视为应用程序中的可共享目标。这是一项必不可少的工作,因为 URL 是要显示哪些布局以及需要加载哪些代码和数据的主要输入。

路由器与数据获取和缓存、突变和重新验证等重要操作相关联。正因为如此,应用路由器所在的位置和工作是前端架构的基础。

客户端与服务器

在传统的服务器驱动方法中,路由器将请求映射到获取数据和呈现 HTML 模板的 URL。URL 之间的转换会导致生成新文档,并且需要刷新浏览器。

在客户端驱动的方法中,路由器的代码必须下载到浏览器,在一切启动后,它开始侦听浏览器历史记录更改:例如链接点击和后退导航事件。

从这里开始,它不会请求一个全新的文档,而是将对 URL 的更改映射到重新呈现现有文档的客户端组件代码。

客户端路由是 SPA 体系结构的核心。路由转换保留客户端的当前状态,其中不需要重新评估现有的 JS、CSS 和其他资源。在进行转换之前,可以预加载代码和数据。它还支持路线转换之间的动画等体验(类似的 UX 模式现在正在平台中烘焙)。

大多数元框架都提供服务器驱动的应用程序状态组合,同时保留了客户端路由的整体功能。随着 Qwik 和 RSC 架构所采用的方法,客户端和服务器之间的区别开始变得模糊。

瀑布的历史

很长一段时间里,动态客户端路由是一个常见的模式。也就是说,路由器将路由作为树中任何位置的组件进行渲染。

这种设计允许在运行时提供高度灵活和动态的功能,例如渲染 <Redirect /> 组件。但是,要知道要加载哪些代码和数据,我们必须渲染组件树来确定路由。

在实践中,这意味着许多使用此模式的客户端驱动组件体系结构会遇到相当数量的客户端-服务器网络瀑布。

瀑布是一系列连续的网络请求,其中每个请求都依赖于前一个请求的完成。瀑布是潜伏在网络选项卡中的性能的无声杀手。元框架路由器收敛于静态定义的路由定义

一种常见的表现形式是基于文件系统的路由 - 一种将文件夹结构映射到 URL,然后将 URL 映射到这些文件夹中的特定文件的直观方法。编译器通过遍历文件系统来生成路由。

配置文件定义所有路由是另一种简单且类型安全的方法。

这些路由定义自然地形成了分层树结构。大多数路由器会创建一个嵌套路由树,将 URL 段映射到相应的组件子树。接下来,我们将看到为什么这一点很重要,以及利用 URL 如何成为许多服务器优先数据加载模式的关键。

前端的新后端

将 URL 映射到组件树后,我们需要加载代码和数据。正如我们所看到的,元框架的一大主题是协调服务器驱动的应用程序状态的性能优势,同时又不放弃基于组件的富客户端方法的功能。

主机托管是组件模型的重要组成部分,它能够轻松组合。这里有一个权衡,即管理自己的数据依赖关系的组件的可移植性,即通过获取自己的数据。但是,在组合时可能会产生不必要的瀑布。与接受该数据作为道具(或承诺)相比,提取被提升到路由级别。

我们已经讨论过 Relay 是一个功能强大的“前端后端”客户端库的例子。允许数据与组件共置,但具有提升的提取。此功能需要复杂且捆绑包大小的成本,并且需要 GraphQL。让我们了解在迁移到服务器时如何进行这些权衡,而无需捆绑客户端获取库或使用 GraphQL。

永远最好的朋友

前端后端 (BFF) 是面向服务的后端环境中熟悉的一种设计模式

其基本思想是,定制的后端服务直接位于每个客户端平台(Web、移动、CLI 等)的后面,并满足该前端应用程序的特定需求。

例如,使用 HTMX 等服务器驱动的方法,后端使用 HTML 部分响应执行 AJAX 样式更新的瘦客户端。

就 RSC 而言,它是一个定制的后端,可根据经验将序列化的组件树返回给瘦客户端或胖客户端。

让我们了解一下与在服务器上运行此层相比的一些好处。

  • 通过将大多数数据提取和数据转换逻辑保留在服务器上,包括繁重的转换库(日期格式化、国际化等)以及发送到浏览器的代码之外的任何令牌或机密,从而简化客户端捆绑包。
  • 编写多个数据需求并修剪数据,避免过度获取。正如我们在 Suspense 中看到的那样,较慢的 API 调用可以向式传输到 Suspense 边界,而不是块渲染。

  • 通过允许产品开发人员指定体验的精确数据要求,为前端提供与 GraphQL 解决方案类似的 DX。

  • 利用 URL 状态 - 反应式系统中状态管理的黄金法则是存储状态的最小表示形式,并使用它来派生其他状态。

我们可以将此原则应用于 URL,其中各个 URL 段映射到组件子树,以及其中组件的当前状态。例如,使用映射到当前搜索筛选器或当前选定选项的查询参数。

从性能的角度来看,以这种方式管理状态允许此应用程序层预先获取所有代码和数据,靠近数据所在的服务器。

因此,在大多数情况下,当我们在客户端上运行时,我们提前获得了所需的所有信息,而无需从客户端请求返回服务器。对于初始负载和后续转换来说,这是一个很好的位置。

这也意味着我们正在充分利用网络作为分发机制的力量——通过 URL 提供可共享的链接,这些链接提供您所看到的内容的一致性,并确保当您共享它时,其他人也看到诸如选定的过滤器、开放模型等内容。

考虑到这一点,如果 URL 允许您提前获取代码或数据,则它是存储某些类型的客户端状态的好地方。

缓存统治着我周围的一切

将这些层移出客户端意味着我们可以在服务器上执行更多操作并缓存更多内容。绩效的一个基本原则是少做事。一种方法是尽可能多地提前完成工作,并将结果存储在缓存中。

有多种类型的缓存(以及缓存中的更深层),在较高层次上了解这些缓存非常重要。

公共缓存存储数据不敏感或不个性化的工作结果。一个示例是公共 CDN,其中缓存了服务器构建的 HTML 输出。

专用缓存仅供单个用户(或单独的用户群组)访问。例如,远程数据的内存中客户端缓存。或者浏览器的本机 HTTP 缓存。

任何系统中复杂性的一个主要来源是状态管理。在前端,其中很大一部分是管理前端与它交互的远程数据的同步,这实际上是一种缓存管理

新的远程数据缓存

正如我们所看到的,在浏览器中拥有内存缓存作为视图的真实来源,可以为快速交互提供乐观的写入。每当我们有缓存时,我们都需要了解它们是如何失效的。让我们来看看与客户端缓存交互的不同方式。

  • 手动缓存管理:这涉及使用 Redux 等状态管理工具手动管理规范化缓存。对于乐观更新,它需要命令性的直接缓存更新,这些更新通常会在响应返回时再次更新。
  • 基于密钥的失效消除了手动管理的需要。React Query 是一流的工具的一个例子,它可以处理许多其他棘手的缓存管理问题。而 Apollo 或 Relay 采用类似的方法,即一切都在引擎盖下为您处理。

将此层移动到服务器意味着移动视图的主要事实来源。在了解了如何在客户端模型中完成缓存管理之后,让我们了解一下如何在服务器优先模型中完成缓存管理。

缓存失效和服务器操作

在“传统”请求-响应模型中,写入更新服务器状态与导航相关联,因为浏览器需要在更新后呈现新文档。典型的模式是 POST、重定向、GET 请求流。

<!-- browser sends form data to the url passed to "action" -->
<form action="form_action.php" method="post">
  <!-- fields -->
</form>

大多数框架都收敛于此模式,作为执行写入的初始默认值。这使得 SPA (PESPA) 更容易逐步增强。

表单的 action 属性采用接收浏览器发送的表单数据的终结点的 URL。像 Remix 和 Sveltekit 这样的框架会发送带有表单数据的写入,以路由级别的服务器操作。而 Next 和 SolidStart 允许在组件树中的任何位置调用服务器操作,使它们更类似于 RPC。

一旦我们写入服务器状态(数据库和任何服务器缓存),客户端框架就不会返回一个全新的文档,而是使用其反应式系统来区分响应并就地更新页面。

返回编码到视图中的数据(而不仅仅是数据)的一个好处是,响应可以在单个服务器往返中返回更新的 UI,而浏览器在收到更新视图的重定向后必须执行另一个 GET;这是 React Server 组件的好处,我们接下来会看到。

与手动管理客户端缓存相比,此方法要简单得多,并且也不需要捆绑数据获取库。但正如我们之前所看到的,请求-响应模型在路由(或嵌套路由)级别具有粗粒度更新。

对于大部分体验来说,这是一个很好的默认设置。但是,对于某些功能,我们可能仍然需要细粒度缓存管理和客户端数据加载的好处。

例如,在轮询时,或者当粗粒度的请求-响应流不能很好地映射到您正在构建的内容时,并且您希望避免在写入时重新运行服务器组件或loader函数。

可以在模块图中的任何位置使用的服务器操作的好处是,您可以混合和匹配有意义的方法。例如,您可以使用服务器操作的结果来冻结客户端缓存。

// client fetching and caching with RPC-style server actions
useQuery({
  queryKey: ['cool-beans'],
  // any function that returns a promise
  queryFn: () => myServerActionThatReturnsJson(),
})

在这个领域还有更多细微差别,我们需要更多时间来探索。最后,让我们了解 React Server 组件与服务器操作相结合提供的一些新功能,以及它们如何与其他新兴技术相交。

多维组件

React 服务器组件是一个重大的范式转变。在它们的萌芽阶段,它们很难遵循,因为有许多不同的方法可以将它们概念化。

孤岛架构的角度来看,不同于 React 的各种服务器组件也正在其他框架生态系统中探索,例如 Nuxt 和 Deno's Fresh

React 所做的所有权衡都是为了保留组件模型和随之而来的组合能力。在架构级别理解它们的另一种方法是作为组件化的 BFF

从客户端的角度来看,RSC 是提前运行的组件,例如,在静态构建期间,或在客户端运行之前在服务器上运行。

一个简单的心智模型是将它们视为序列化的组件。通过序列化组件的输出来运行 React 的想法已经酝酿了一段时间。

这个新功能允许 React 表达多种架构风格:提前构建的静态站点、具有 HTMX 样式的 AJAX 更新的服务器驱动体系结构、渐进式增强的 SPA 或具有单个入口点的纯客户端呈现的 SPA。或者全部在同一个应用程序中,具体取决于具体体验。除了潜在的性能优势之外,让我们来探讨一下这种流体架构的一些有趣的潜在优势。

网络合成

服务器组件提供了共享和组合全栈功能切片的能力,以及一种新的前端创作体验。

在组织内部,这是对前端和后端独立团队模型的新尝试,以更加与在全栈垂直切片或钢线程中工作的团队保持一致。

对于具有标准化基础架构的大型组织来说,拥有可由产品团队使用和组成的全栈平台组件的用例非常引人注目。在联合模型中组合 RSC 输出的能力是另一种新兴功能。

目前尚不清楚这将如何在生态系统层面发挥作用,但它无疑会给组件 API 设计带来有趣的变化。例如,软件包还可以导出预加载函数,这些函数可以提升到路由级别,以避免服务器瀑布。因为这是一种新的范式,所以许多最佳实践仍然需要探索,并发现陷阱。

服务器驱动的 UI

AirBnb 和 Uber 等一些大型组织利用这一概念来更精细地控制其原生移动前端的服务器驱动渲染。

react-strict-dom 的引入提供了 React Native 和 RSC 的有趣组合,可以更轻松地跨 Web 以外的平台利用这些想法,包括 AR 和空间用户界面等新兴平台。

生成式 UI

很难预测生成式人工智能的未来将如何在这个领域发挥作用。但它将继续存在。此模型中的一项新兴功能是动态生成高度个性化、丰富的交互体验的能力。

一个更实际的例子是,在知道要渲染哪些组件之前,您需要数据。在这种情况下,您需要提前捆绑多个不同类型的交互组件。由于 UI 组件的数量可以无限增长(例如 CMS 内容类型),因此这种类型的组件动态呈现将需要将所有代码向下发送到客户端,或者在从客户端延迟加载不同的组件类型时引入延迟。

拥有端到端的组件意味着我们可以在不增加大量捆绑包的情况下流式传输组件。这里一个有趣的探索是使用 AI 函数调用以及服务器操作的灵活性来返回序列化的交互式组件。

前端的未来

在这篇文章中,我们再次涵盖了很多领域,几乎没有触及 Web 应用程序框架中一些基本层的表面。更不用说像 WebAssembly 和 WebGPU 这样的技术如何以意想不到的方式发挥作用。或者大型 Javascript 框架之外的其他生态系统如何与有状态服务器方法本地优先开发的出现做出不同的权衡。

站在所有这些技术的最前沿是令人兴奋的。但是,也很容易不知所措。

要培养的一项基本技能是识别问题的内在复杂性,以及该问题的解决方案所产生的偶然复杂性。对于前端的年轻人来说,这意味着将你的注意力缩小到基础和不变的概念上。

工程(和生活)的很大一部分是做出决定并致力于一个方向。您对用户和团队的需求了解得越多,您做出的权衡就越好,您对自己的决策就越有信心。

理解技术发展的一个关键见解是,它并不总是意味着所有人的进步。

处于创新阶段的能力或方法可能是您正在创造的确切最佳点;如果是这样的话,那就继续建设吧,愿原力与你同在。

  • 21
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值