大家好,我是 考拉🐨,今天跟大家一起聊聊 JavaScript 框架。
太过保守很难在 Javascript
生态系统中保持与时俱进。
对于那些刚进入这个行业的人来说,要在新的库、框架、概念和有力的意见中关注正在发生的事情,很有挑战性。
这是个很好的提醒,默认情况下,使用“无聊”的技术,你所熟悉的技术,并且成为晚期采用者,通常是个不错的选择。
闲话少叙,本文将带读者了解 Javascript
中生态系统中的最新进展,通过研究过去在构建大规模 Web 应用时的痛点来了解当前的情况。
不要把注意力集中在快速增长的解决方案上,而是从潜在问题入手。每一种架构都会有不同的答案,并且会有不同的权衡。到本文结束时,我们会列出流行框架的高级模型,如 React、Svelte、Vue、Solid、Astro、Marko、Fresh、Next、Remix、Qwik
,以及适合当今环境的“元框架” 。
鉴往知来。让我们回首来时路,再看看未来的趋势。这次,我们将专注于大型项目中的问题,这些问题激发了其他方法和思维方式。
Web 最初由静态文档链接在一起组成。那时候,人们可以提前准备一份文件,并把它放在电脑上。而现在最酷的就是,人人都可以访问它,无需亲临其境。
不知从何时起,我们觉得,让这些文件变成动态,会非常酷。于是我们有了像 CGI 这样的技术,使我们能够根据请求提供不同的内容。然后,我们有了像 Perl 这样的表达式语言来编写这些脚本。它对最初针对 Web 开发的 PHP 产生了影响。PHP 的创新之处在于将 HTML 直接连接到后端代码。这使得以编程方式创建嵌入动态值的文件变得容易了。
Web 最重要的突破之一来自于此:
<html>
<body>
Hello ConardLi !
</body>
</html>
具有易于嵌入的动态值:
<html>
<body>
Y2K? <?php echo time(); ?>
</body>
</html>
这些动态页面很受欢迎。我们可以很轻松地对发送给用户的内容进行定制,包括启用会话的 cookies
。在与数据库交互的语言生态系统中,已经有了基于服务器的模板框架。通过这些框架,我们可以轻松地从静态页面开始,然后扩展到动态页面。
Web 的发展一日千里,我们想要更多的互动体验。为了这个目的,我们使用了 Flash 这样的浏览器插件。在其他方面,我们会在后端提供的 HTML 上“撒上” Javascript
片段。
像 jQuery
和 Prototype
这样的工具出现了,它们隐藏了 Web API 的复杂度,消除了浏览器之间的差异。
光阴荏苒,科技公司的规模在不断扩大,并且由于项目和开发团队的增长,在模板中加入更多的业务逻辑是非常普遍的。
编写的服务器代码,将处理后的数据传输到服务器模板语言中。模板常常会演变成业务逻辑的“混合体”来访问全局变量。由于像 SQL 注入这样的攻击已经司空见惯,因此安全问题也越来越突出。
最终,论文《Ajax:Web 应用的新方法》(Ajax: A New Approach to Web Applications
)为我们带来了 Ajax
技术。现在你用 Ajax
技术可以做的新事情就是用异步方式更新页面,而不再是以同步的方式来更新页面。这种模式被第一批大型客户端应用程序所推广,如谷歌地图和谷歌文档。后来,我们开始看到 Web
分发对桌面风格的软件的影响力。与在商店里购买光盘的软件相比,这是一个重大的进步。
当 Node.js
出现的时候,它所带来的新特性,就是用与前端相同的语言来编写你的后端。所有这些都是开发人员所熟悉的异步优先模式。这曾经令人无法抗拒,当然现在也是。随着越来越多的企业上线,竞争优势在于能否快速交付和迭代。
Node.js
的生态系统强调重复使用小型的单用途包,你可以利用现成的去完成任务。
我们更渴求能够与桌面、移动设备相媲美的 Web。现在,我们已经有了一系列可重用的“小部件”库和工具,如 jQuery UI、Dojo、Mootools、ExtJs
和 YUI 等。
我们对这些小玩意儿的关注程度与日俱增,并且在前端的工作也越来越多。这往往导致了前端和后端的模板重复。像 Backbone
和 Knockout
以及许多其他的框架出现了。它们通过 MVC、MVVM
等架构为前端增加了关注点的分离,并且,架构可以兼容我们收集到的所有小部件和 JQuery
插件。添加结构有助于扩展所有这些前端代码。并且可以加速从后端传送模板。
我们仍然编写微调的 DOM
操作来更新页面并保持组件的同步。这个问题非同小可,而且与数据同步相关的错误也很常见。
在谷歌的支持下,Angular
登场了。它通过增强 HTML 的动态性,促进了生产力的提高。它配备了双向数据绑定,以及一个受电子表格启发的反应性系统。这些声明式的双向绑定消除了许多必须更新的模板。这是好事,可以让我们的工作效率更高。
随着规模的扩大,跟踪变化越来越困难,常常会造成性能下降。更新的周期会发生,并占据主线程(今天像 Svelte
这样的库可以在降低其缺陷的情况下保持双向绑定)。除了移动设备的兴起之外,这些提高生产力的框架也加速了前端和后端的分离。这为探索强调这种解耦的不同架构铺平了道路。
这是 JAMstack
理念的一个主要部分,强调提前预生成 HTML,并从 CDN 提供服务。在当时,这是对提供静态文档服务的一种倒退。但现在,我们有了基于 git 的工作流,有了强大的 CDN 基础设施,有了可以与独立 API 互动的解耦前端,就无需依靠远在天边的中央服务器。与运营服务器相比,将静态资产放置到 CDN 上要便宜很多。
今天,像 Gatsby、Next 和很多的其他工具都利用了这些想法。
快步流星地进入大科技时代。我们正试图追风逐电,一改故辙。对于那些进入这个行业的人来说,Javascript
很大,而构建一个由独立后端支持的解耦 SPA
已经成为现实。
在 Facebook
,React
的诞生面临着几个挑战。
React
诞生了,你能做得很酷的新事情就是声明性地编写前端代码。
前端关注点的分离是著名的反思,以前的 MVC
框架无法扩展。人们并不喜欢从模板向 Javascript
驱动的 JSX
过渡。但是我们大多数人都接受了。
组件模型允许解耦独立的前端团队,他们可以更容易地在独立组件上并行工作。作为一个架构,它允许组件的分层。从共享的原语到构成页面根目录的“有机体”。单向的数据流使数据流更易于理解、跟踪和调试。这就提高了之前难以企及的可预见性。虚拟 DOM
就是我们可以编写函数,返回用户界面的说明,让 React
去解决这些难点。这样可以避免在数据频繁变化时出现的一致性问题,并且使得模板的组成更加人性化。
React
非常流行,已经成为了业界的标准,即使是那些不想要其特性的网站来说也是如此。在规模的远端,我们开始看到一些限制。
DOM
是 React
模型的一个问题。浏览器并不是为了在连续的渲染周期中不断创建和销毁 DOM
节点而构建的。就像任何可以通过引入一个新的间接级别来解决的问题一样,React
把它抽象到了虚拟 DOM
后面。
人们只有在 100 毫秒以内感知到反馈,才会感到流畅。而在做像滚动页面这样的事情时则要低得多。在与单线程环境相结合的情况下,这种优化已经成为高度交互式应用的新瓶颈。当虚拟 DOM
和真实 DOM
之间发生协调时,大型交互式应用程序会对用户的输入失去响应。像“长任务”这样的术语开始出现了。
这导致了 React
在 2017 年被重新编写,为并发模式奠定了基础。
与此同时,更快的移动意味着传输更多的代码。浏览器在运行大量 Javascript
时,启动速度慢就成为一个问题。我们开始注意到所有隐含的运行时成本,不仅是 HTML 和虚拟 DOM,还有我们编写 CSS 的方式。
组件模型简化了我们在 CSS 方面的经验。我们可以将样式与组件放在一起,这提高了可删除性。对于那些以前不敢删除 CSS 代码的人来说,这是一个非常好的属性。我们一直在处理的级联和所有的特殊性问题都被 JavaScript
库中的 CSS 抽象化了。
这些第一波的库往往伴有隐含的运行时成本。我们需要等到组件被渲染后,再将这些样式注入到页面中,这就造成了 JavaScript
包中的样式问题。从规模上来说,糟糕的性能往往是千夫所指,而我们也注意到了这些成本。这导致 JavaScript
库中出现了新的 CSS,它通过使用智能预编译器来提取样式表,这些库专注于没有运行时的开销。
当浏览器渲染 HTML
时,像 CSS 或脚本这样的渲染障碍资源会阻止 HTML
的其他部分显示出来。在一个组件的层次结构中,父组件往往会成为子组件的渲染障碍。
在实践中,许多组件依赖于数据库的数据和 CDN 的代码(通过代码分割)。这经常会造成瀑布式的网络请求阻塞。在渲染之后,组件会获取数据,解锁异步子组件。接着,它们将会获取它们所需的数据,并重复这一过程。经常可以看到“下拉列表的地狱”或累积布局偏移,这些变化是在加载 UI 时出现在屏幕上的。
React 后来发布了 Suspense
,以使页面的加载阶段更加顺畅。但是,默认情况下,这并不能防止持续的网络瀑布问题。Suspense
支持“在获取数据时渲染”的模式。
我们将继续绕行,了解 React 的一些权衡如何在规模上得到缓解。这将有助于构建新框架中的模式。
在 React
中,虚拟 DOM 的运行时成本是无法避免的。并发模式是一个解决问题的方法,它可以让你在高度互动的体验中保持对事情做出响应。
在 JavaScript
中的 CSS 领域,使用了一个名为 Stylex
的内部库。当成千上万的组件被渲染时,这可以维持人性化的开发人员体验,而无需运行时的成本。
Facebook
用 Relay
来避免顺序性的网络瀑布问题。对于一个给定的入口点,静态分析可以精确地确定要加载的代码和数据。这就意味着代码和数据都可以在一个优化的 graphQL
查询中并行加载。
这比初始加载和 SPA 转换的顺序网络瀑布要快得多。
其中一个基本问题就是传递 JavaScript
,这些 JavaScript
与具体的用户无关。
如果有 A/B 测试,特性标记的经历,以及针对特定类型和群组的用户的代码时,那就很困难了。还有语言和地区设置。当代码有许多分支时,静态依赖关系图不能看到在实践中为特定用户群一起使用的模块。
Facebook
使用了一个由人工智能驱动的动态包系统。这利用其紧密的客户 - 服务器集成,在运行时根据请求计算出最佳的依赖图。这与一个根据优先级分阶段加载包的框架相结合。
Facebook
拥有复杂的基础设施和多年来构建的内部库。如果你是一家大型科技公司,你可以投入大量的资金和资源来优化这些大规模的权衡。
这为前端产品开发人员创造了一个成功的深渊,可以让他们在完成任务的同时保持性能。
我们中的大多数人都不会像 Facebook
那样的规模上构建一套应用。然而,对于许多大型企业来说,性能是个话题。我们可以从这些模式中学习,例如:尽可能多地获取数据,并行化网络,以及使用内联需求等等。
大型科技公司经常在内部推出自己的应用框架。在不同的用户资源库中,遗留了大量的解决方案。这导致了许多 Javascript
生态系统疲劳和框架倦怠。
还跟我们在一起?我们正处于 SPA 的时代。这就是目前从事这一行的人所面临的现状。
React
是无可争议的冠军,然而,我们看到了大规模的取舍。
React
提供了一个层。它将其他必要的层留给了生态系统,在路由、状态管理、数据获取等各个重要方面造成了混乱,每个层都有自己的概念和 API。
不可变与可变,带有类的 OOP
与函数式的 OOP
,争论和库都如火如荼。
如今,很多开发人员都被不确定的事情所困扰,他们不知道应该怎么去做,也不知道该怎么去构建。
组件是有黏性的。但运行时成本、Javascript
驱动的 JSX 以及复杂性都有待讨论。很多不是来自大型科技公司的草根替代方案,已经获得了广泛的认同。让我们对这些方案做一个总论:
当人们在评估迁移到 Angular 2
或 React
时,Vue
填补了入门门槛低的空白。你不必为复杂的 webpack
配置而担心。你可以从 CDN
上下载并开始使用对许多开发人员来说很直观的模板来构建组件。
核心团队可以使用路由和样式等核心组件,减少决策疲劳。它还通过对模板进行静态分析,缓解了 React
调和算法的某些方面,以实现优化,加快运行时。这被称为编译器通知的虚拟 DOM。
Svelte
开创了预编译方法的先河,消除了我们在运行时看到的复杂性和开销。
我们的想法是要有一个可以自行编译的框架,并简化输出最小的普通 JavaScript
。所有这些都是基于声明式组件和熟悉的可变 JavaScript
风格来保持现代的创作体验。Svelte
完全避免了使用虚拟 DOM,因此不会受到编写 JavaScript
的不可变风格的约束,这种风格可以用来做更新状态之类的事情。对于许多人来说,这是一个更简单、更理智地在 Web 上构建东西的模型。
Solid
有一个直接的和可预测的反应性模型,其灵感来自 Knockout
。像 React
一样,它也避免了使用模板来简化函数的可组合性。
而 React
采取的是不断重新渲染世界的方法。Solid
只渲染一次,并在不增加虚拟 DOM 开支的情况下,使用精简的反应性系统进行细粒度的更新。Solid
看起来就像我们许多 React 开发人员想要使用钩子的新代码那样。它的 API 也许更人性化,并且在许多方面非常顺利,例如钩子的依赖数组,其重点是细粒度的反应性和可组合的原语。
对于每个框架,还有许多可说的。每个人都会在自己的基本模式和喜好上作出不同的权衡。
在现实中,进化往往是由人类的意志决定的。尝试不同的解决方案来解决当前的痛点,每个框架都从彼此中学习。其中一个重要的主题就是精简和简化。把事情从运行时移到编译时是这些主题之一,它激发了 “React forget
”,这是一个有望能够消除记忆化需求的特性。它们的共同点是解决了文件的交互部分。正如我们所看到的,这是一个具有挑战性的方面,要以一种容易扩展的方式来解决。
同时,我们看到了纯客户端渲染的权衡。当加载一个页面时,那个空白的白屏需要更长的时间。在移动设备和网络上,这真是一场灾难。对于很多网站来说,网页打开速度更快,且性能不降低,成为一个主要的竞争优势。
我们迈出了这一步,正在探索通过首先在服务器上渲染内容来加快渲染速度的方法(后来才发现这是一种权衡)。这个最初的倒退引发了许多“元”框架和 HTML 优先前端框架的新浪潮。
我们不会停止探索。我们所有探索的终点就是我们开始的地方。也是第一次知道这个地方。
受 PHP
的启发,Next 开始简化创建静态页面推送到 CDN
的过程。它还解决了在 React
应用程序中使用 SSR
的棘手问题。
它还提供了一些关于使用基于文件的路由来构建应用程序的意见,这很受欢迎。还有其他一些不错的特点。从那时起,又有一波“元”框架被创建。对于 Vue
,我们在 Nuxt
中有一个类似的框架。Svelte
的 Sveltekit
,以及即将推出的 SolidStart
。
这些都是服务器优先,旨在整合 Web 框架的所有部分和人体工程学。这并不仅仅是人们长久以来所关心的互动元素。
对话的出发点是改进用户的经验和开发人员的经验,而非一种交换。
多页面架构从服务器上提供 HTML
,其中导航是全页面刷新。快速启动对于很多站点来说都是至关重要的,尤其是那些没有登录的站点。它直接关系到诸如搜索排名和跳出率之类的事情。对于许多互动性低的网站和应用程序来说,使用像 React
这样的客户端渲染库,就过于夸张了。
对许多人来说,这意味着翻转脚本。做到 HTML
优先而不是 Javascript
优先,MPA 优于 SPA,并默认为零 Javascript
。
像 Marko、Astro、Fresh、Rocket
和 Enhance
等框架都采用了这种方法。与一些元框架相比,路由器停留在服务器上,而不是让客户端的路由器在第一次加载后接管。在 Javascript
生态系统中,这是对 Node.js
之后不久的基于服务器的模板制作的一种倒退。
这一轮的 MPA 与前几代不同。“Sprinkles
”是在一个基于组件的模型中编写的,通常使用 island 模式。在前端和后端代码中使用相同的语言。往往在同一个文件中共存。这就消除了在添加一些交互性时前端和后端构造不同的重复模板代码的问题。
Remix
在 React
生态系统中带来了渐进增强的回归。
从技术角度来看,Remix
是 React Router
的编译器,和其他新兴的元框架一样,是一个边缘兼容运行时。它通过嵌套布局和数据获取 API
,解决了 Facebook
通过 Relay
大规模解决的相同挑战。
这允许早期的代码和数据的并行获取。这是用 Suspense
实现“边渲染边获取”模式的一个良好前提条件。对渐进增强的强调意味着它的 API 基于 Web 标准,数据变异的故事基于 HTML 表单。
而不是通过连接事件处理程序来进行必要的获取请求。你渲染表单,将数据提交给在服务器上处理它们的动作函数(通常在同一个文件中)。受到 PHP 的启发。
与 Next
类似,应用程序可以缩小规模,像传统的服务器渲染的 MPA
那样在没有 Javascript
的情况下工作,或者按每页的规模扩展到交互式 React 应用程序。
Remix
还提供了许多 API 和模式,用于处理诸如乐观的 UI 更新、静态条件的处理以及优雅的退化之类的事情,这些都是你希望一个专注于终端用户体验的深思熟虑的框架所提供的。
不要与 Quic
协议相混淆。Qwik
这个框架是关于尽量减少不必要的 Javascript
。虽然它的 API
看起来像 React
,但它的方法与其他元框架不同,因为它专注于水化过程。
就像你可以暂停一台虚拟机并将其移动到不同的物理机上。Qwik
把这个想法带到了服务器和浏览器之间发生的工作。它的“可恢复”水化的想法意味着你可以在服务器上启动一些东西,然后在客户端上恢复,而不需要任何重新工作。这与部分水化形成对比,后者在水化工作发生时进行移动,而 Qwik
则试图在一开始就避免这样做。
这是一套有趣的想法,它利用了服务器和客户端紧密结合的力量,允许这种动态捆绑和服务。
这些概念开始模糊了 MPA
和 SPA
之间的界限,一个应用程序可以从 MPA
开始,动态地过渡到 SPA
。有时(用更流行的话来说)被称为 “过渡性应用程序”。
同时,后端基础设施和托管也在不断改进。CDN
的边缘使我们的 SPA
的静态资产服务变得简单而快速。现在将运行时和数据转移到边缘也变得可行了。这是在浏览器之外创建一个新的运行时层,但仍然尽可能地接近用户。这使得将目前在浏览器中完成的许多事情移回服务器变得更加容易。同时在一定程度上减轻了这样做所带来的网络延迟的取舍。
像 React
服务器组件这样的想法正在探索将服务器组件的输出从这一层流向浏览器的概念。像 Deno
和 Bun 这样的新的 Javascript
运行时正在出现,以简化和精简 Javascript
生态系统,并为这个边缘运行时的新世界而构建,为速度和快速启动时间而优化。
这也导致了应用框架采用标准的网络 API 来在这一层运行。随着无服务器功能和流媒体架构被探索出来。
流(Streaming
)是这里的一个大主题。它允许提前刷新 HTML,因此浏览器可以在接收到它时逐步进行渲染。在后端同时获取任何数据时,开始处理任何阻碍渲染的资源,如 CSS 和 JS。这有助于并行化许多其他顺序往返行程。
本文讲了那么多,但实际上只是触及皮毛而已。对于本文中提到的最佳框架、架构或模式,以及我们没有提到的无数其它框架、架构和模式,并没有一个通用的答案。它始终是对特定指标的权衡。而要知道如何权衡,取决于你正在构建的东西、你的用户是谁、他们的使用模式,以及围绕关键用户体验的任何其他要求(如性能预算)的设定。
对于我们中的大多数人来说,真相在某个中间的地方。新一波框架和创新的伟大之处在于,它们提供了根据需要扩大和缩小规模的杠杆。对于那些进入这个行业的人和那些经验丰富的人来说,投资于基本面总是一个不错的选择。
框架的演变慢慢地将原生 Web 推向了更远的地方,消除了以前对框架的需求,并减轻了之前的取舍,使我们能够越来越多地采用其原生特性。