Web 应用架构的下一个转变
大家好,我是 ConardLi。
Web
技术大概 25
年前开始萌芽,HTTP、HTML、CSS 和 JS 都是在九十年代中期首次被标准化的。直到如今,Web
演变成一个无处不在的应用平台。随着 Web
的发展,Web 应用程序的开发架构也在不断发展。现在有许多用于构建 Web
应用程序的核心架构,目前最流行的是单页应用 (SPA
),但我们正在逐渐过渡到一种新的改进架构来构建 Web
应用程序。
下面是一些主要的架构模式:
多页应用 (MPA) 渐进增强的多页应用(PEMPA) 单页应用 (SPA) 渐进增强的单页应用 (PESPA)
每种架构都有它的优点和痛点,但是往往架构的痛点会成为一个足以促使人们转向下一个架构的核心动力,在技术选型时,我们需要综合考虑。
无论我们怎么构建我们的应用程序,总绕不过需要在服务器上运行代码。其实这些架构的最大区别就是代码所在的位置。下面我们就依次来看一下,并观察代码的位置是如何随时间演进的。在分析每种架构时,我们会从以下几个角度考虑:
持久化( Persistence
) - 从数据库中保存和读取数据路由( Routing
) - 根据 URL 切换模块数据获取( Data fetching
) - 从持久化存储中检索数据数据变更( Data mutation
)- 持久化中的数据变化渲染逻辑( Rendering logic
) - 向用户显示数据UI 反馈( UI Feedback
) - 响应用户交互
注意:在后面的架构图中我们都会使用英文
当然,Web
应用程序的组成部分远不止这些,但这些部分是变化最多的部分,也是我们作为 Web
开发者花费大量时间的地方。根据不同的项目规模和团队结构,我们可能会处理所有这些类别的代码,也可能只处理其中的一部分。
多页应用 (MPA)
在早期,浏览器的功能比较简单,这是当时在 Web
上运行的唯一架构。
我们编写的所有代码都存在于服务器上,只有客户端上的 UI反馈
代码由用户的浏览器处理。
MPA 架构
文档请求
当用户在地址栏中输入 URL
时,浏览器会向我们的服务器发送请求。我们的路由逻辑将调用一个函数来获取数据,该函数会与数据库通信来检索数据。然后,渲染逻辑会使用此数据来生成将作为响应发送给客户端的 HTML
。一般来讲,浏览器都会向用户提供一些处理中状态的反馈(比如 favicon
位置的 loading
)。
变更请求
当用户提交表单时,浏览器会将表单内容序列化为发送到我们服务器的请求,我们的路由逻辑会调用一个函数来更新数据库。然后它就会通知浏览器进行重定向,浏览器会触发一个新的 GET
请求来获取新的 UI
(然后就和上一步用户输入 URL
的结果一样了)。
注意:成功的变更会发送一个重定向的响应,而不仅仅是发送一个新的 HTML
,这一点很重要。浏览器的历史堆栈中会有一个 POST
请求,点击后退按钮会再次触发这个 POST
请求(想知道为什么应用程序有时会显示:“不要点击后退按钮!!” 是的,就是这个原因。)。
MPA 的优缺点
MPA
的心智模型很简单,但当时我们对它并不看好。虽然在请求中主要由 Cookie
处理一些状态和复杂的流程,但在大多数情况下,一切都发生在请求/响应周期的时间内。
缺点:让诸如焦点管理之类的操作变得苦难,具有动画效果的页面切换几乎不太可能,用户体验很差。
值得注意的是,随着即将推出的 page transitions API
,Web
平台不断改进,给 MPA
架构带来了更多可能性。
https://developer.chrome.com/blog/shared-element-transitions-for-spas/
渐进增强的多页应用 (PEMPA)
渐进式增强的理念是:我们的 Web
应用程序应该是功能性的,对所有 Web
浏览器都应该是可访问的,然后利用浏览器的任何额外功能来增强体验。该术语由 Nick Finck
和 Steve Champeon
于 2003
年创造。
渐进增强是我们的 Web
应用程序应该是功能性的并且所有 Web 浏览器都可以访问的想法,然后利用浏览器具有的任何额外功能来增强体验。该术语由 Nick Finck 和 Steve Champeon 于 2003 年创造。
XMLHttpRequest
最初由 Microsoft
的 Outlook Web Access
团队于 1998
年开发,但直到 2016
年才标准化(你相信吗!?)。当然,这从未阻止过浏览器供应商和 Web
开发者。AJAX
作为一个术语在 2005
年流行起来,很多人开始在浏览器中发出 HTTP
请求。
业务建立在这样一个理念上,即我们不必回到服务器去获取更多的数据来更新适当的 UI
。这样,我们就可以构建逐步增强的多页面应用了:
“哇!“ 你可能会想, “等一下… 这些代码是从哪里来的?” 因此,现在我们不仅要负责来自浏览器的UI反馈,我们还需要向客户端提供路由、数据获取、数据变更和渲染逻辑,而不仅仅是在服务器上已有的这些逻辑。“到底发生了什么事?”
好吧,是这样的。渐进增强背后的理念是,我们的基线应该是一个功能性的应用程序。特别是在 21
世纪初,我们不能保证用户使用的浏览器能够运行像 AJAX
这样花哨的新东西,或者他们在与应用程序交互之前能够在足够快的网络上下载我们的 JavaScript
。所以我们需要保持现有的 MPA
架构,只使用 JavaScript
来增强体验。
也就是说,根据我们所讨论的增强级别,我们可能确实需要编写几乎所有类别的代码,数据持久化除外(除非我们想支持离线模式)。
另外,我们还必须向后端添加更多代码,来支持客户端发出的 AJAX
请求。所以在两端都需要有更多的开发者。
这是 jQuery、MooTools
等的时代。
PEMPA 架构
文档请求
当用户第一次请求 HTML
文档时,发生的事情和 MPA
示例中的一样。但是,PEMPA
还将通过包含用于增强功能的 <script>
标签来加载客户端 JavaScript
。
客户端导航
当用户在我们的应用程序中单击带有 href
的 anchor
元素时,我们的客户端数据获取代码会阻止默认的整页刷新行为并使用 JavaScript
更新 URL
。然后客户端路由逻辑会确定需要对 UI 进行哪些更新并手动执行这些更新,包括在数据获取库向服务端发出网络请求时显示任何 Loading
状态(UI 反馈)。服务器路由逻辑会调用数据获取代码从数据库中检索数据并将其作为响应(XML
或 JSON
)发送,然后客户端使用其渲染逻辑执行最终的 UI
更新。
当用户提交表单时,我们的客户端数据变更逻辑会阻止默认的整页刷新和发布行为,使用 JavaScript
序列化表单并将数据发送到服务端。然后,服务器路由逻辑调用数据变更函数,与数据库交互以执行变更,并将更新的数据响应给客户端。客户端渲染逻辑将使用更新后的数据来更新 UI;在某些情况下,客户端路由逻辑会将用户发送到另一个地方,这会触发与客户端导航流程类似的流程。
PEMPA 的优缺点
通过引入客户端代码并将 UI 反馈的责任推给我们自己,我们确实解决了 MPA
的问题。我们有更多的控制权,可以给用户一种更像自定义应用程序的感觉。
但同时为了给用户提供他们想要的最佳体验,我们必须负责路由、数据获取、变更和渲染逻辑。这样做有几个问题:
阻止浏览器默认行为 - 在路由和表单提交方面,我们做得不如浏览器好。在此之前,保持页面上的数据是最新的从来都不是一个需要考虑的问题,但现在这在我们的客户端代码中占了一半以上。此外,竞争条件、表单重新提交和错误处理都是隐藏 bug
的好地方;自定义代码 - 有更多的代码需要管理,而我们以前不必编写这些代码。我知道相关性并不意味着因果关系,但我注意到,一般来说,我们的代码越多,我们的错误就越多🤷♂️; 代码重复 - 在渲染逻辑方面存在大量的代码重复。客户端代码需要以与后端代码在变更或客户端转换后渲染所有可能状态相同的方式更新 UI
。后端拥有的UI
必须在前端也可用。而且大多数情况下它们使用的是完全不同的语言,这使得代码复用困难。进行客户端交互,然后确保客户端代码更新的UI
与整个页面刷新时所发生的情况相同,这是非常困难的;代码组织 - 对于 PEMPA
,这是非常困难的。由于没有集中存储数据或渲染 UI 的地方,人们几乎可以在任何地方手动更新DOM
,而且很难遵循代码规范,这会减慢开发速度。
就个人而言,这大约是我刚进入 Web
开发世界的时候。回想起这段时光,我心中充满了渴望的怀旧和颤抖的恐惧🍝。
单页应用 (SPA)
没过多久,我们意识到如果我们只是从后端删除 UI
代码,就可以消除重复的问题。这就是我们所做的:
你会注意到这个架构图几乎与 PEMPA
相同。唯一的区别是渲染的逻辑消失了,一些路由代码也消失了,因为我们不再需要 UI
路由,只剩下了 API
路由。这是 Backbone、Knockout、Angular、Ember、React、Vue、Svelte
等的时代。
SPA 架构
文档请求
由于后端不再具有渲染逻辑,所有文档请求(用户输入 URL
时发出的第一个请求)都由静态文件服务器(通常是 CDN
)提供服务。在 SPA
的早期,HTML
文档几乎总是一个有效的空 HTML
文件,其中包含用于“挂载”应用程序的 <div id="root"></div>
。然而如今,一些框架允许我们使用称为“静态站点生成”(SSG
)的技术在构建时预渲染尽可能多的页面。
客户端导航
数据变更
这个架构中的其他行为与 PEMPA
相同,只是现在我们主要使用 fetch
代替 XMLHttpRequest
。
SPA 的优缺点
有趣的是,在上面的架构行为中,与 PEMPA
的唯一区别是文档请求的体验更差了! 那么我们为什么还要这么做呢?
到目前为止,最大的优点就是开发者体验。这是从 PEMPA
向 SPA
过渡的原始驱动力,没有代码重复是一个巨大的好处。我们通过各种方法证明了这一改变的合理性(DX
毕竟是 UX
的输入)。不幸的是,改进 DX 是 SPA
真正为我们所做的一切。
我个人相信 SPA
架构有助于提高感知性能,因为 CDN
对 HTML
文档的响应速度比服务器生成 HTML
文档的速度要快,但在现实世界的场景中,这似乎从来没有什么区别(要归功于现代基础设施)。可悲的现实是,SPA
仍然存在一些与 PEMPA
相同的所有其他问题,尽管有了更现代的工具,使事情更容易处理。
更糟糕的是,SPA
还带来了几个新问题:
包大小 — 有点爆炸了。关于 JavaScript
对网页性能的影响,请参阅web Almanac
这篇详尽的文章。瀑布请求 — 因为所有用于获取数据的代码现在都在 JavaScript
包中,我们必须等待它被下载后才能获取数据。与此同时,还需要利用这些包的代码拆分和懒加载,现在我们有了这样的关键依赖情况:document→ app.js→ page.js→ component.js→ data.json→ image.png
。这最终会导致更糟糕的用户体验。对于静态内容,我们可以避免很多这样的问题,但SSG
策略的开发者正致力于解决这些问题并乐意向我们出售他们的特定于供应商的解决方案。运行时性能 — 有这么多的客户端 JavaScript
要运行,一些低功率设备很难跟上(可以阅读这篇文章:https://v8.dev/blog/cost-of-javascript-2019 )。过去在我们强大的服务器上运行的程序现在可以在人们手中的微型计算机上运行。我知道我们用更少的能源把人类送上了月球,但这仍然是一个问题。状态管理 — 这成了一个大问题。为了证明这一点,我提供了可用于解决此问题的库的数量😩。以前, MPA
会在DOM
中渲染我们的状态,我们只需要引用/修改它。现在我们只得到JSON
,我们不仅要让后端知道数据何时更新,还要保持该状态的内存表示是最新的。这具有缓存挑战的所有标志,毫不夸张地说,是软件中最困难的问题之一。在典型的SPA
中,状态管理占人们工作代码的30-50%
。
为了帮助解决这些问题并减少它们的影响,已经有一些开源库出现了。自 2010
年代中期以来,SPA
成为了开发网页应用的标准方法。现在我们已经进入了 21 世纪 20 年代,一些新的想法即将出现。
渐进增强的单页应用 (PESPA)
MPA
的心智模型非常简单,并且也具有更强大的功能。经历过 MPA
阶段并在 SPA
中工作的人们确实为我们在过去十年中失去的简单性感到惋惜。如果你考虑到 SPA
架构背后的动机主要是为了在 PEMPA
上改进开发人员的体验,那么这一点就特别有趣。如果我们能够以某种方式将 SPA
和 MPA
合并到一个体系结构中,获得两者的优点,那么我们就有希望得到既简单功能又强大考虑到渐进式增强,即使没有客户端 JavaScript
,基线也是一个功能性应用程序。考虑到渐进式增强,即使没有客户端 JavaScript,基线也是一个功能性应用程序。的东西。这就是渐进式增强单页应用。
考虑到渐进式增强,即使没有客户端 JavaScript
,基线也是一个功能性应用程序。因此,如果我们的框架支持并鼓励渐进式增强作为核心原则,那么我们的应用程序的基础就是 MPA
的简单心智模型的坚实基础。具体来说,就是在请求/响应周期的背景下思考事物的心智模型。这使我们在很大程度上消除了 SPA
的问题。
需要强调的是:渐进式增强的主要好处不是“你的应用程序不需要 JavaScript
就可以工作”(尽管这是一个很好的附带好处),而是心智模型大大简化了。
为了做到这一点,PESPA
需要在 Prevent default
时 “模拟浏览器”。因此,无论浏览器是发出请求还是发出基于 JavaScript
的 fetch
请求,服务器代码都以相同的方式工作。因此,当我们仍然拥有这些代码时,我们可以在剩下的代码中保留简单的心智模型。其中一个重要部分是,PESPA
模拟浏览器的行为,即在发生变更时重新验证页面上的数据,以保持页面上的数据是最新的。使用 MPA
,我们只需要重新加载整个页面。对于 PESPA
,这种重新验证发生在 fetch
请求中。
记住,我们在 PEMPA
中也有一个重要的问题:代码重复。PESPA
通过使后端 UI代码和前端UI代码完全相同来解决这个问题。通过使用一个既能在服务器渲染又能在客户端上进行交互/处理更新的UI库,我们就不存在代码重复的问题。
您会注意到有一些用于数据获取、变更和渲染的小框。这些是用来增强的。例如,挂起状态、乐观UI 等在服务器上无法实现,所以我们将有一些只在客户端上运行的代码。但即便如此,在现代UI库中,实现也非常简单。
PESPA 架构
文档请求
使用 PESPA
的文档请求实际上与 PEMPA
相同。应用程序所需的初始 HTML
直接从服务器发送,并且还会加载 JavaScript
以增强用户交互体验。
客户端导航
当用户单击链接时,我们会阻止浏览器的默认行为。我们的路由将确定新路由所需的数据和 UI
,并为下一个路由需要的任何数据触发数据获取,并渲染为该路由渲染的 UI
。
数据变更
PESPA
的变更是通过表单提交完成的。没有更多的 onClick+fetch
废话(但是命令式变更对于渐进增强是体验更好的,比如当用户会话超时时重定向到登录页面)。当用户提交表单时,我们将组织浏览器默认行为。我们的变更代码会序列化表单,并将其作为请求发送到与表单动作相关联的路由(默认为当前 URL
)。后端路由逻辑调用数据库交互代码并返回成功的响应(例如一个点赞操作)或重定向(例如创建一个新的GitHub repo)。如果是重定向,路由处理器会为该路由(并行)加载代码/数据/资产,然后触发渲染逻辑。如果不是重定向,路由处理器会重新验证当前UI的数据,并触发渲染逻辑来更新UI。有趣的是,不管它是内联变更还是重定向,路由处理器都参与其中,为两种类型的变更提供了相同的心智模型。
PESPA 的优缺点
PESPA
消除了以前架构中的大量问题。让我们一一看一下:
MPA
问题:
全页刷新 - PESPA
阻止浏览器默认行为,使用客户端JS
来模拟浏览器。从我们编写的代码的角度来看,这与MPA
没有什么不同,但从用户的角度来看,这是一种改进了很多的体验。UI
反馈控制 -PESPA
允许我们完全控制网络请求,因为我们正在阻止浏览器默认行为并发出数据获取请求,因此我们可以以任何对我们的UI
最有意义的方式向用户提供反馈。
PEMPA
问题:
阻止浏览器默认行为 - PESPA
的一个核心方面是它们的行为方式应该与浏览器在路由和表单方面的行为方式大致相同。这就是他们为我们提供MPA
心智模型的方式。取消来自重新提交表单的请求,正确处理无序响应以避免竞争条件问题,以及显示错误以避免永远不会消失的微调器。这就是框架真正有用的地方。自定义代码 - 通过在客户端和服务器之间共享代码并拥有模拟浏览器行为的正确抽象,我们最终大大减少了我们必须自己编写的代码量。 代码重复 - PESPA
的部分想法是服务器和客户端使用完全相同的代码来渲染逻辑。所以没有重复可言。不要忘记挑战:“进行客户端交互,然后确保客户端更新的UI
与我们刷新页面时获得的UI
相同。” 对于PESPA
,它应该始终通过我们开发人员的努力或考虑。代码组织 - 由于 PESPA
的浏览器模拟提供的心智模型,应用程序状态管理不是一个考虑因素。并且渲染逻辑在网络两端的处理方式相同,因此也不会出现随意的DOM
变更。服务器/客户端阻隔 - 模拟浏览器的 PESPA
意味着前端代码和后端代码位于同一位置,从而消除了阻隔并提高了我们的工作效率。
SPA
问题:
包大小 - 使用 PESPA
需要一个服务器,这意味着我们可以将大量代码移动到后端。所有UI
需要的是一个可以在服务器和客户端上运行的小型UI
库、一些用于处理UI
交互和反馈的代码以及用于组件的代码。多亏了URL
(基于路由的)代码拆分,我们终于可以告别拥有数百KB JS
的网页了。最重要的是,由于渐进式增强,大多数应用程序应该在JS
完成加载之前工作。目前JS
框架正在努力进一步减少客户端所需的JS
数量。瀑布请求 - PESPA
的一个重要部分是它们可以了解给定URL
的代码、数据和资产要求,而无需运行任何代码。这意味着除了代码拆分之外,PESPA
还可以一次触发对代码、数据和资产的请求,而不是依次等待一个。这也意味着PESPA
可以在用户触发导航之前预先获取这些内容,以便在需要时浏览器可以立即返回,从而使整个应用程序的使用体验变得好。运行时性能 - PESPA
在这个部分有两件事情要做:1)他们将大量代码移动到服务器,因此设备首先要执行的代码更少;2)由于渐进增强,UI
已经准备好在JS
完成加载和执行之前使用。状态管理 - 因为浏览器模拟,我们提供了 MPA
心智模型,所以应用程序状态管理在PESPA
上下文中不是问题。这一点的证据是应用程序应该在没有JavaScript
的情况下大部分工作。当变更完成时,PESPA
会自动重新验证页面上的数据。
有一点很重要,无论有没有客户端 JavaScript
,PESPA
的工作方式都不完全相同。无论如何,这绝不是渐进增强的目标。只是大多数应用程序应该在没有 JavaScript
的情况下工作。这不仅仅是因为我们关心无 javascript
的用户体验。这是因为通过以渐进增强为目标,我们大大简化了我们的 UI
代码。你会惊讶于我们可以在没有 JS
的情况下走多远,但是对于某些应用程序来说,没有客户端 JavaScript
就没有必要或不切实际。但是,即使我们的某些 UI
元素确实需要一些 JavaScript
来操作,我们仍然可以获得 PESPA
的主要好处。
PESPA
的区别:
功能是基线 - 用于增强的 JS
未启用懒加载+智能预取(不仅仅是 JS
代码)将代码推送到服务器 无需手动复制 UI 代码(如在 PEMPA
中)透明浏览器仿真 (# useThePlatform
)
至于缺点,我们还在研究中。但以下是一些初步想法:
许多习惯于 SPA
和 SSG
的人会感叹我们现在有服务端代码运行我们的应用程序。然而,对于任何现实世界的应用程序,我们都无法避免服务端代码。当然,在某些用例中,我们可以一次构建整个站点并将其粘贴到 CDN
上,但我们为日常工作而开发的大多数应用程序都不属于这一类。
与此相关的是人们对服务器成本的关注。这个想法是,SSG
允许我们一次性创建应用,然后通过 CDN
以非常低的成本将其提供给几乎无限数量的用户。这有两个缺陷。1) 我们可能会在我们的应用程序中使用 API
,因此这些用户仍然会在访问时触发大量我们最昂贵的服务端代码。2) CDN
支持 HTTP
缓存机制,所以如果我们真的能够使用 SSG
,那么我们绝对可以利用它来提供快速响应并限制渲染服务器处理的工作量。
人们离开 SPA
时遇到的另一个常见问题是,现在我们必须应对在服务器上进行渲染的挑战。对于习惯于只在客户端上运行代码的人来说,这绝对是一种不同的模型,但如果我们使用的工具考虑到了这一点,这就不是什么挑战了。如果我们没有这样做,那么它肯定是一个挑战,但是有一些合理的变通方法,可以强制某些代码在我们迁移时只在客户端运行。
正如我所说,我们仍在发现渐进式增强型单页应用程序的缺点,但我认为它的好处是值得的,我们目前可以察觉到。
我还应该提到,尽管我们已经在相当长的一段时间内使用现有工具实现了 PESPA
体系结构的功能,但在共享渲染逻辑代码的同时关注渐进增强还是新的。这篇文章主要感兴趣的是演示事实上的标准架构,而不仅仅是平台的功能。
PESPA 实践:Remix
PESPA
的领头羊是 Remix
,这是一个专注于 Web
基础和现代用户体验的 Web
框架。Remix
是第一个开箱即用的 Web
框架,它提供了我所描述的 PESPA
提供的一切。在这方面,其他框架也可以效仿 Remix
的做法。我特别注意到 slveltekit
和 SolidStart
在他们的实现中都采用了 PESPA
原则。我想还会有更多的元框架(同样,元框架支持 PESPA
架构已经有一段时间了,但是 Remix
把这种架构放在了最前沿,其他的也在效仿)。当我们为我们的 PESPA
建立一个 Web
框架时,情况如下:
在这种情况下,Remix
充当了跨 Web
的桥梁。如果没有 Remix
,我们必须自己实现它才能拥有完整的 PESPA
。Remix
还通过结合基于约定和基于配置的路由来处理我们的路由。Remix
还将帮助我们逐步增强数据获取和变更(例如 twitter
上的点赞按钮)以及用于实现诸如挂起状态和乐观 UI 之类的 UI
反馈。
多亏了 Remix
中内置的嵌套路由,我们也获得了更好的代码组织( Next.js
也在追求)。虽然 PESPA
架构不需要嵌套路由,但基于路由的代码拆分是一个重要部分。此外,我们通过嵌套路由获得了更精细的代码拆分,因此这是一个重要方面。
Remix
证明我们可以通过 PESPA
架构更快地构建更好的体验。我们最终会遇到这样的情况:
最后
就我个人而言,我非常期待这个转变。同时获得更好的 UX
和 DX
是一种坚实的胜利。我认为这是一个重要的决定,我对我们的未来感到兴奋。另外我也创建了一个库,演示了使用 TodoMVC
应用程序在各个时代移动的所有代码! 你可以在这里找到它:
https://github.com/kentcdodds/the-webs-next-transformation
本文译自:epicweb.dev/the-webs-next-transition 作者:Kent C. Dodds 往期精彩推荐:2022 Web 年鉴 — JavaScript
如果你有任何想法,欢迎在留言区和我留言,如果这篇文章帮助到了你,欢迎点赞和关注。
如果你想加入高质量前端交流群,或者你有任何其他事情想和我交流也可以添加我的个人微信 ConardLi 。
点赞
和在看
是最大的支持⬇️❤️⬇️