Web
前端由在25年前的技术组成。HTTP
、HTML
、CSS
和 JS
都是在九十年代中期首次标准化的。从那时起,网络发展成为一个无处不在的应用平台。随着网络的发展,用于开发这些应用的架构也在不断演进。如今,有许多用于构建网络应用的核心架构。目前由网络开发人员使用的最流行的架构是单页面应用程序(SPA)
,但我们正在转向一种新的、改进的用于构建网络应用程序的架构。
<a>
和 <form>
元素从一开始就存在。链接用于让浏览器从服务器获取内容,而表单用于让浏览器将内容发送到服务器(并得到返回内容)。有了这种从一开始就包含在规范中的双向通信,就可以在网络上永远创建功能强大的应用程序。
以下是主要的架构(按照流行程度的时间顺序排列):
- 多页面应用程序(MPAs)
- 渐进式增强型多页面应用程序(PEMPAs,又称“JavaScript Sprinkles”)
- 单页面应用程序(SPAs)
- 下一个转变
Web
开发的每种架构都有其优点和痛点。最终,痛点变得足够严重,以至于促使转移到下一个架构,该架构带来了自己的折衷方案。
无论我们如何构建应用程序,我们几乎总是需要在服务器上运行代码(值得注意的例外包括像 Wordle
这样的游戏,它们将游戏状态存储在本地存储中)。区分这些架构的一种方法是代码的存放位置。让我们依次探讨每种架构,并观察代码位置随时间的变化。在讨论每种架构时,我们将特别考虑以下代码用例:
- 持久性 - 从数据库保存和读取数据
- 路由 - 根据 URL 路由流量
- 数据获取 - 从持久性获取数据
- 数据更新 - 在持久性中更改数据
- 渲染逻辑 - 将数据显示给用户
- 用户界面反馈 - 响应用户交互
自然地,Web
应用程序还有更多部分,但这些部分是最常移动和我们作为 Web 开发人员花费大量时间的部分。根据项目规模和团队结构,我们可以在所有这些代码类别中工作,也可以只在其中一部分中工作。
多页面应用程序(MPAs)
在早期,这是基于当时网络浏览器功能的唯一可行的架构。
在多页面应用程序中,我们编写的所有代码都驻留在服务器上。客户端的用户界面反馈代码由用户的浏览器处理。
MPA 架构
文档请求:当用户在地址栏中输入
URL
时,浏览器会向我们的服务器发送请求。我们的路由逻辑将调用一个函数来获取数据,该函数将与持久性代码通信以检索数据。然后,渲染逻辑使用这些数据确定将作为响应发送给客户端的HTML
。与此同时,浏览器会通过某种待定状态(通常在favicon
位置)向用户提供反馈。更新请求:当用户提交表单时,浏览器将表单序列化为发送到我们的服务器的请求。我们的路由逻辑将调用一个函数来变异数据,该函数将与持久性代码通信以进行数据库更新。然后,它将响应重定向,以便浏览器触发
GET
请求以获取新的用户界面(这将触发用户最初输入 URL 时发生的相同情况)。同样,浏览器会通过待定的用户界面向用户提供反馈。
注意:成功的更新应该发送重定向响应,而不仅仅是新的
HTML
。否则,您的历史堆栈中将存在POST
请求,点击返回按钮将再次触发POST
请求(曾经想过为什么应用程序有时会说“不要点击返回按钮!!”是的,就是这个原因。它们应该响应重定向)。
MPA 的优缺点
MPA
的思维模型很简单。我们当时并不太喜欢它。虽然在请求/响应循环的时间内处理了一些状态和复杂流程,但大多数情况下都是在请求/响应周期内完成的。
这种架构的不足之处在于:
- 完整页面刷新 - 使某些事情变得困难(焦点管理),其他事情变得不切实际(想象每次点赞推文都会进行完整页面刷新…),有些事情根本不可能(动画页面转换)。
- 用户界面反馈控制 - 网站图标变成旋转器很好,但通常更好的用户体验是与用户界面更接近的视觉反馈。设计师肯定喜欢根据品牌目的自定义它。还有什么关于不看好UI?
值得注意的是,随着即将推出的页面跳转 API
,MPA
对于更多场景来说将变得更加可行。但对于大多数网络应用程序来说,这还不够。无论如何,在当时,这个问题远未被标准委员会和用户所关注!
逐步增强型多页面应用程序(PEMPAs)
渐进式增强是我们的 Web
应用程序应该在所有 Web
浏览器上都是功能性和可访问的,然后利用浏览器具有的任何额外功能来增强用户体验的理念。该术语由Nick Finck
和 Steve Champeon
在 2003 年提出。说到浏览器的功能…
XMLHttpRequest
最初是由微软的 Outlook Web Access
团队在 1998 年开发的,但直到 2016 年才标准化(你能相信吗!?)。当然,在这之前,这并没有阻止浏览器供应商和 Web 开发人员。AJAX
一词在 2005 年被普及,并且许多人开始在浏览器中进行 HTTP
请求。企业建立在这样一个理念上:我们不必再次返回服务器以获取更新 UI 所需的数据,而只需少量数据即可。有了这个,我们就可以构建逐步增强型多页面应用程序:
“哇!” 你可能会想,“等一下… 这些代码都从哪来的?”现在我们不仅从浏览器那里接管了 UI 反馈的责任,而且还将路由、数据获取、数据变异和渲染逻辑以及我们已经在服务器上拥有的内容都添加到了客户端上。“这是怎么回事?”
好吧,这就是事实。渐进式增强背后的理念是我们的基线应该是一个功能性的应用程序。特别是在 2000 年代初期,我们无法保证用户使用的浏览器能够运行我们新的 AJAX
功能,或者他们是否处于足够快的网络上以在与应用程序交互之前下载我们的 JavaScript
。因此,我们需要保持现有的 MPA 架构不变,并仅使用 JavaScript
来增强用户体验。
也就是说,根据我们所谈论的增强级别,我们可能确实需要在我们几乎所有的类别中编写代码,除了持久性(除非我们需要离线模式支持,这确实很好,但不是行业标准做法,因此没有包含在图表中)。
此外,我们甚至必须在后端添加更多代码来支持客户端将进行的AJAX
请求。所以,在网络的两端都有更多的内容。
这是 jQuery
、MooTools
等的时代。
PEMPA 架构行为
文档请求:当用户首次请求文档时,与
MPA
示例中发生的情况相同。但是,PEMPA
还将加载客户端JavaScript
,通过包含<script>
标签来使用增强功能。客户端导航:当用户单击带有应用程序内
href
的锚元素时,我们的客户端数据获取代码会阻止默认的全页刷新行为,并使用JavaScript
更新URL
。然后,客户端路由逻辑确定UI
需要进行哪些更新,并手动执行这些更新,包括显示任何待定状态(UI
反馈),同时数据获取库进行网络请求到服务器端点。服务器路由逻辑调用数据获取代码从持久性代码中检索数据并将其作为响应发送(作为XML
或JSON
,我们可以选择 😂),然后客户端使用该更新的数据使用其渲染逻辑进行最终UI
更新。更新请求:当用户提交表单时,我们的客户端数据更新逻辑会阻止默认的全页刷新和发布行为,并使用
JavaScript
序列化表单并将数据发送到服务器端点。然后,服务器路由逻辑调用数据变异函数,该函数与持久性代码交互以执行更新,并响应客户端的更新数据。客户端渲染逻辑将使用更新的数据根据需要更新UI
;在某些情况下,客户端路由逻辑将用户发送到另一个位置,从而触发与客户端导航流程类似的流程。
PEMPA 优缺点
通过带上客户端代码和接管 UI
反馈功能,我们确实解决了 MPA
的问题。我们拥有了更多的控制权,可以为用户提供更自定义的应用程序感。
不幸的是,为了给用户提供他们期待的最佳体验,我们必须负责路由、数据获取、变异和渲染逻辑。这其中存在一些问题:
- 阻止默认 - 我们在路由和表单提交方面不如浏览器做得好。保持页面上的数据最新以前从未成为问题,但现在它占据了我们一半以上的客户端代码。此外,竞态条件、表单重新提交和错误处理是隐藏 bug 的绝佳场所。
- 自定义代码 - 我们现在需要管理的代码要多得多,这是以前我们不必编写的。我知道相关性并不意味着因果关系,但我注意到一般来说,我们拥有的代码越多,我们就会有越多的
bug
。 - 代码重复 - 在渲染逻辑方面存在大量的代码重复。客户端代码需要以与后端代码相同的方式更新 UI,以在变异或客户端转换后呈现每个可能的状态。因此,后端具有的相同 UI 必须在前端也可用。而且大多数情况下,它们使用的是完全不同的语言,这使得代码共享不可行。这不仅仅是模板,还包括逻辑。挑战是:“进行客户端交互,然后确保客户端代码更新的 UI 与如果是完整页面刷新时发生的情况相同。”这个操作非常困难(我们开发人员经常使用的一个网站就是一个 PEMPA,并且经常不正确地执行此操作)。
- 代码组织 - 对于
PEMPAs
来说,这是非常困难的。由于没有集中存储数据或渲染 UI 的地方,人们几乎在任何地方手动更新DOM
,这使得跟踪代码变得非常困难,从而减慢了开发速度。 - 服务器/客户端间接性 -
API
路由和客户端数据获取以及使用它们的数据更新代码之间存在间接性。网络的一侧发生变化必然导致另一侧的变化,而这种间接性使得很难知道我们没有因为遵循代码路径而破坏任何东西,因为遵循代码路径需要浏览一系列文件。网络成为障碍,导致了这种间接性,就像狐狸利用河流摆脱猎狗的气味一样。
单页面应用程序(SPAs)
不久之后,我们意识到如果从后端删除 UI 代码,就可以解决重复问题。所以我们就这样做了:
你会注意到这个图形几乎与 PEMPA 的图形完全相同。唯一的区别是渲染逻辑消失了。一些路由代码也消失了,因为我们不再需要为 UI
设置路由。我们剩下的只有 API
路由。这是 Backbone
、Knockout
、Angular
、Ember
、React
、Vue
、Svelte
等行业中大多数使用的策略。
SPA 架构
由于后端不再具有渲染逻辑,所有文档请求(用户在输入我们的 URL
时进行的第一次请求)都由静态文件服务器(通常是 CDN
)提供。在SPA
的早期,HTML
文档几乎总是一个空的HTML
文件,其中 <body>
中有一个 <div id="root"></div>
,用于“挂载”应用程序。然而,如今,框架允许我们使用一种称为“静态站点生成”(SSG
)的技术,在构建时预渲染页面的大部分内容。
此策略中的其他行为与 PEMPA
相同。现在我们主要使用 fetch
而不是 XMLHttpRequest
。
SPA 优缺点
有趣的是,上面架构行为中与PEMPA
的唯一区别是文档请求更糟糕了!那么我们为什么要这样做呢?
到目前为止,这里最大的优势是开发人员体验。这正是从 PEMPA
过渡到SPA
的最初动力。没有代码重复是一个巨大的好处。我们通过各种方式来证明这种变化(DX
毕竟是 UX
的一部分)。不幸的是,改进 DX
对我们来说就是 SPA
唯一做的事情。
我个人记得被说服 SPA
架构有助于交互体验,因为 CDN
可以更快地响应 HTML
文档,而服务器生成HTML
文档的速度则慢一些,但在现实世界的场景中,这似乎并没有什么区别(而且现代基础架构使这一点变得更不真实)。悲伤的现实是,SPA
仍然存在与 PEMPA
完全相同的其他问题,尽管有更现代的工具,这使得处理这些问题变得更容易。
更糟糕的是,SPA 还引入了几个新问题:
- bundle大小 - 它有点爆炸了。
- 瀑布效应 - 因为现在所有获取数据的代码都存储在
JavaScript
包中,所以我们必须等待其下载完成,然后才能获取数据。与此同时,我们需要利用代码拆分和延迟加载这些bundle
包,现在我们面临着类似于这样的关键依赖情况:document → app.js → page.js → component.js → data.json → image.png
。这不太理想,最终导致用户体验大幅下降。对于静态内容,我们可以避免其中的大部分问题,但是对于此问题,存在一系列问题和限制,而静态站点生成策略的提供者正在努力解决并乐于出售他们的特定供应商解决方案。 - 运行时性能 - 由于有太多的客户端
JavaScript
需要运行,一些性能较低的设备无法跟上。过去在我们强大的服务器上运行的内容现在期望在人们手中的小型计算机上运行。 - 状态管理 - 这成为一个巨大的问题。作为证据,我提供解决此问题的库的数量。在此之前,
MPA
将我们的状态呈现在DOM
中,我们只需引用/更新即可。现在我们只是获取JSON
,我们不仅需要让后端知道数据何时已更新,还需要保持该状态的内存表示始终处于最新状态。这具有所有缓存挑战的标志(因为它就是缓存),这是软件中最困难的问题之一。在典型的SPA 中
,状态管理占据了人们工作的30-50%
的代码(这个统计数据需要引用,但你知道它是真的)。 - 创建了一些库来帮助解决这些问题并减少其影响。这非常有帮助,但有些人会将这种变化称为疲劳。这已成为自 2010 年代中期以来构建 Web 应用程序的事实标准方法。我们已经进入了 2020 年代,并且一些新想法正在即将到来。
逐步增强型单页面应用程序(PESPAs)
MPA
有一个简单的思维模型。SPA
具有更强大的功能。经历了 MPA
阶段并在 SPA
中工作的人们真正怀念我们在过去十年中失去的简单性。如果考虑到 SPA
架构的动机主要是为了改善开发人员体验而不是为了 PEMPAs
,那么这就特别有趣了。如果我们可以以某种方式将SPA
和 MPA
合并为单一架构,以获得两者的优势,那么希望
我们将获得既简单又更有能力的东西。这就是逐步增强型单页面应用程序。
考虑到逐步增强,基线是一个功能齐全的应用程序,即使没有客户端 JavaScript
也是如此。因此,如果我们的框架将渐进式增强作为核心原则,并鼓励使用它,那么我们的应用程序基础就有了简单的 MPA 思维模型的坚实基础。具体来说,是在请求/响应循环的上下文中考虑事物的思维模型。这使我们可以在很大程度上消除 SPA
的问题。
这值得强调:渐进增强的主要好处不是“您的应用程序在没有JavaScript
的情况下工作”(尽管这是一个不错的副作用),而是心智模型大幅简化。
为了做到这一点,PESPAs
在阻止默认时需要“模拟浏览器”。因此,无论浏览器是进行请求还是进行基于 JavaScript
的获取请求,服务器代码都以相同的方式工作。因此,虽然我们仍然拥有该代码,但在我们的其余代码中可以保持简单的思维模型。其中一个重要部分是,PESPAs
模拟浏览器在进行更新时重新验证页面上的数据的行为,以保持页面上的数据处于最新状态。对于 MPA
,我们只是进行了全页重新加载。对于 PESPAs
,此重新验证是通过获取请求进行的。
请记住,我们还有一个与 PEMPA
相关的重要问题:代码重复。PESPAs
通过使后端UI
代码和前端UI
代码完全相同来解决这个问题。通过使用能够在服务器上渲染并在客户端上交互/处理更新的 UI
库,我们就不会遇到代码重复的问题。
你会注意到数据获取、突变和渲染有小框。这些部分用于增强。例如,待处理状态、UI 等在服务器上没有实际位置,因此我们将在客户端运行一些代码。但即使这样,借助现代 UI 库的共存性,我们可以处理这些问题。
PESPA 架构
对于 PESPAs,文档请求实际上与 PEMPAs 几乎相同。所需的初始 HTML 将直接从服务器发送,JavaScript 也会加载以增强用户交互体验。
- 客户端导航:当用户点击链接时,我们将阻止默认行为。我们的路由器将确定新路由所需的数据和
UI
,并触发对下一个路由所需数据的获取以及渲染该路由的UI
。 - 更新请求:你注意到那两个图表是一样的吗?是的!这不是巧合!使用
PESPAs
进行更新是通过表单提交完成的。不再使用onClick + fetch
这种无聊的方式(然而,对于像会话超时时重定向到登录屏幕这样的渐进增强的命令式突变来说是可以接受的)。当用户提交表单时,我们将阻止默认行为。我们的突变代码将表单序列化并将其作为请求发送到与表单操作相关联的路由(默认为当前URL
)。后端的路由逻辑调用操作代码,该代码与持久性代码通信以执行更新,并返回成功响应(例如:类似推文)或重定向(例如:创建新的GitHub
存储库)。如果是重定向,则路由器加载该路由的代码/数据/资产(并行加载),然后触发渲染逻辑。如果不是重定向,则路由器会重新验证当前 UI 的数据,并触发渲染逻辑以更新 UI。有趣的是,无论是内联变异还是重定向,路由器都参与其中,使我们开发者对两种类型的变异都有相同的心智模型。
PESPA 优缺点
PESPAs 消除了先前架构中的大量问题。让我们逐一看看它们:
MPA 问题:
- 全页刷新 -
PESPAs
阻止默认行为,而是使用客户端端 JS 模拟浏览器。从我们编写的代码的角度来看,这与MPA
没有任何不同,但从用户的角度来看,这是一种改进的体验。 - UI 反馈控制 -
PESPAs
允许我们完全控制网络请求,因为我们正在阻止默认行为并进行fetch
请求,因此我们可以根据我们的 UI 需要的方式为用户提供反馈。
PEMPA 问题:
- 阻止默认行为 -
PESPAs
的核心方面之一是它们在路由和表单方面应与浏览器的行为基本相同。这是它们实现给我们MPA
心智模型的方式。取消表单重新提交的请求、正确处理无序响应以避免竞争条件问题以及显示错误以避免永远不消失的旋转器,这些都是PESPA
的一部分。这是框架真正帮助我们的地方。 - 自定义代码 - 通过在客户端和服务器之间共享代码,并具有模拟浏览器行为的正确抽象,我们最终大大减少了自己需要编写的代码量。
- 代码重复 -
PESPA
的一个概念是服务器和客户端使用完全相同的代码进行渲染逻辑。因此,几乎没有重复可言。别忘了这个挑战:“进行客户端交互,然后确保客户端更新的 UI 与我们刷新页面后得到的 UI 相同。”在 PESPA 中,这应该是毫不费力或不考虑的事情。 - 代码组织 - 由于
PESPAs
的浏览器模拟提供的心智模型,应用程序状态管理不是一个问题。并且渲染逻辑在网络的两端都处理得一样,因此也没有杂乱的DOM
更新。 - 服务器/客户端间接性 - PESPA 模拟浏览器意味着前端和后端的代码共同存在,从而消除了间接性,并使我们的工作效率更高。
SPA 问题:
- bundle大小 - 转向
PESPA
需要一个服务器,这意味着我们可以将大量代码移至后端。UI
需要的只是一个小型UI
库,该库可以在服务器和客户端上运行,一些用于处理 UI 交互和反馈的代码以及组件的代码。由于基于URL
(基于路由)的代码拆分,我们终于可以告别具有数百KB JS
的网页了。此外,由于渐进增强,大多数应用程序应该在JS
加载完成之前就可以使用。此外,目前正在进行 JS 框架的努力,以进一步减少客户端所需的 JS 量。 - 瀑布 -
PESPAs
的一个重要部分是它们可以在不运行任何代码的情况下了解给定URL
的代码、数据和资产需求。这意味着除了代码拆分外,PESPAs 还可以一次性触发代码、数据和资产的获取,而不是等待一次性依次触发。这也意味着 PESPAs 可以在用户触发导航之前预取这些内容,因此当它们被要求时,浏览器可以立即将它们返回,从而使使用应用程序的整个体验感觉即时。 - 运行时性能 -
PESPAs
有两个优点:1)它们将大量代码移到服务器上,因此设备要执行的代码较少;2)由于渐进增强,UI 在 JS 加载和执行之前就已准备好使用。 - 状态管理 - 由于浏览器模拟使我们具有
MPA
心智模型,因此应用程序状态管理在PESPA
上的环境中根本不成问题。这一事实的证据是,即使没有JavaScript
,应用程序也应该大部分正常工作。PESPAs
在更新完成时自动重新验证页面上的数据(MPAs 由于全页重新加载而免费获得)。
需要指出的是,无论是否使用客户端JavaScript
, PESPA
的工作方式都不会完全相同。只是大多数应用程序应该在没有 JavaScript
的情况下正常工作。而且这不仅仅是因为我们关心无 JavaScript
用户体验。这是因为通过针对渐进增强,我们大大简化了我们的 UI
代码。你会惊讶地发现,即使没有 JS,我们也可以做得很多,但对于一些应用程序来说,让所有UI
元素都在没有客户端 JavaScript
的情况下工作可能并不必要或实际。但即使我们的一些UI
元素确实需要一些 JavaScript
来运行,我们仍然可以获得 PESPAs
的主要优势。
区分 PESPA 的因素:
- 功能性是基线 - JS 用于增强而不是启用
- 延迟加载 + 智能预取(不仅仅是 JS 代码)
- 将代码推向服务器
- 无需手动复制 UI 代码(如
PEMPA
) - 透明的浏览器模拟(
#useThePlatform
)
至于缺点。我们仍在发现其中的内容。但这里有一些想法和初步反馈:
许多习惯于 SPA
和SSG
的人会为现在服务器端代码运行我们的应用程序而感到遗憾。然而,对于任何真实的应用程序,我们都无法避免服务器端代码。当然,有一些用例可以使我们构建整个站点一次并将其放在 CDN
上,但我们白天工作的大多数应用程序并不适用于这种情况。
与此相关的是人们担心服务器成本的问题。这个想法是,SSG
允许我们一次构建我们的应用程序,然后通过 CDN
以极低的成本为几乎无限数量的用户提供服务。这种批评有两个缺陷。
- 1)我们可能在应用程序中使用
API
,因此这些用户仍然会在他们的访问中触发我们最昂贵的大量服务器端代码。 - 2)
CDN
支持HTTP
缓存机制,因此如果我们真的能够使用 SSG,那么我们肯定可以利用它来提供快速的响应并限制渲染服务器正在处理的工作量。
离开 SPA 的另一个常见问题是现在我们必须处理服务器端渲染的挑战。对于习惯于仅在客户端上运行其代码的人来说,这绝对是一个不同的模型,但如果我们使用已经考虑到这一点的工具,那么这几乎不是一个挑战。如果我们没有,那么确实可能是一个挑战,但是我们有合理的解决方法可以强制某些代码仅在客户端运行,同时进行迁移。
正如我所说,我们仍在发现逐步增强的单页面应用程序的缺点,但我认为目前我们可以感知到的好处是值得权衡的。
我还应该提到,即使我们已经具备了使用现有工具的 PESPA
架构的能力,但同时关注渐进增强并共享渲染逻辑代码是新的。本文主要关注展示事实上的标准架构,而不仅仅是平台的能力。
一种 PESPA 实现:Remix
在推动 PESPA
的大潮中,有一个专注于Web
基础知识和现代用户体验的 Web
框架——Remix
。Remix
是第一个提供一切我描述的 PESPA
功能的 Web
框架。其他框架也可以并且正在适应 Remix
的领先地位。我知道 SvelteKit
和SolidStart
都在将PESPA
原则融入它们的实现中。我想会有更多的框架效仿(再次说明,元框架很长时间以来就已经能够实现PESPA
架构,但是 Remix
将这种架构置于前沿,其他框架也在跟随)。当我们使用一个Web
框架来实现 PESPA
时,情况是这样的:
在这种情况下,Remix
充当了网络之间的桥梁。没有 Remix
,我们就必须自己实现这个功能才能完整地实现 PESPA
。Remix
还通过一种组合式的约定和配置路由来处理我们的路由。Remix
还会帮助我们处理渐进增强的数据获取和突变部分(如类似推特的按钮)以及实现诸如待定状态和乐观 UI 之类的 UI 反馈。
由于Remix
内置的嵌套路由,我们也可以获得更好的代码组织(Next.js
也在追求此功能)。虽然嵌套路由对于 PESPA
架构并不是必需的,但基于路由的代码拆分是重要的一部分。此外,我们通过嵌套路由可以获得更细粒度的代码拆分,因此这是一个重要的方面。
Remix
正展示了我们可以通过PESPA
架构更快地构建更好的体验。而且,我们最终会得到这样的情况:
在不费吹灰之力地获得完美的性能 Lighthouse 分数?我要报名参加!
结论
就我个人而言,我对这种转变非常期待。在同时获得更好的用户体验和开发体验方面是一个很好的胜利。我认为这是一个重要的胜利,我对未来对我们的发展感到兴奋。作为对你完成这篇博客文章的奖励,我创建了一个仓库,演示了通过一个 TodoMVC
应用程序的所有这些代码在不同时代之间的移动!你可以在这里找到它:kentcdodds/the-webs-next-transformation。希望这可以帮助一些想法更加具体化。