[关闭]
@levinzhang 2022-09-25T22:16:22.000000Z 字数 8714 阅读 389

JavaScript框架的四个时代

摘要

JavaScript框架的发展经历了漫长的历程,在这个过程中,新的技术和规范不断出现,以解决我们开发中的各种问题。本文回顾了JavaScript框架的不同时代,并展望了未来的趋势和方向。


本文最初发表于作者的个人博客站点,经原作者
Chris Garrett授权,由InfoQ中文站翻译分享。

早在2012年,我就开始使用JavaScript框架进行编码了。我曾经为本地的一家企业从头构建了一个PHP应用,这是一个基础的CMS和Web站点。他们决定对其进行重写并增加一系列的特性,项目经理想让我使用.NET,部分原因在于他了解这项技术,同时他希望系统能够看起来像原生应用那样,也就是没有页面刷新以及操作之间的长时间停顿。在经历了一番研究和原型设计之后,我说服了他,借助刚刚兴起的诸多全新JS框架中的某一个,我们就可以使用Web完成相同的事情。

我选择的第一个框架实际上是Angular 1。我构建了一个非常大的应用,并使用FuelPHP作为后端,直到我遇到了社区路由的一些问题,也就是当重新渲染子路由/outlet的时候,页面会闪烁,而且我真的感觉它在设计时就没有考虑到这种情况。于是,有人向我推荐了Ruby on Rails + Ember,尝试之后,我觉得效果很好。我喜欢这两个框架的理念,喜欢这些社区,而且与当时的替代方案相比,它非常高效。

从那时到现在,有了太多的变化,很多框架来来往往,出现又消失并且有了很大的发展。在浏览器中使用JavaScript构建应用的想法,从某种程度上已经从边缘理念变成了标准实践。我们构建使用的基础设施也发生了根本性的改变,提供了大量新的可能性。

在这段时间里,各种想法之间的竞争和冲突也是很多的。我相信从事前端工作有些年头的人都或多或少参与过这样的争论……比如,使用哪种框架,如何编写CSS,采用函数式编程还是面向对象编程,如何最好地管理状态,哪种构建系统或工具最灵活、最快捷等等。回顾过往,我觉得很有趣,我们经常为错误的事情而争论,但忽略了更大的模式,当然这都是事后诸葛亮了。

所以,我想做一个回顾,看一下过去几十年JavaScript开发的历程,看看我们走过的历程。我认为可以粗略地将其划分为四个主要的时代:

  1. 史前时期
  2. 第一代框架
  3. 以组件为中心的视图层
  4. 全栈框架(←我们在这里)

每个时代都有自己的主题和核心冲突,在每个时代中,我们作为一个社区都学到了重要的经验教训,并且在缓慢但坚定地前进。

如今,这些争论仍在继续:Web是否正在变得过于臃肿?普通Web站点真的需要使用React来编写吗?甚至,我们应该使用JavaScript吗?我并不认为我们能够看透未来,而且我也怀疑,我们是否在相互争论中,错失了更宏伟的蓝图。但是,也许从过去的角度看问题能够更好地帮助我们前进。

史前时代

JavaScript最初是在1995年发布的。就像我在前文所述,我是在2012年开始编写JS的,差不多也就是在20年后。这个时代已经接近于我所说的第一代框架的开始时间了。正如你所预料的,在这里我可能会略过一些历史,而且这个时代可以被拆分为多个子时代,其中每个子时代都有自己的模式、库和构建工具等等。

也就是说,我不能写我没有经历过的事情。在我开始编写前端应用的时候,新一代的框架刚刚开始成熟,比如Angular.js、Ember.js、Backbone等。

在此之前,最先进的是像jQuery和MooTools这样的库。这些库在它们的时代是非常重要的,它们帮助我们解决了不同浏览器实现JavaScript方式的差异所带来的问题,这些差异是非常巨大的,比如Internet Explorer实现事件的方式与Netscape是完全不同的,分别是冒泡事件和捕获事件机制。这也是今天的标准实现最终提供了这两种方式的原因。这些库主要用于构建小型、独立的UI组件。大多数应用程序的业务逻辑依然需要通过表单和标准HTTP请求来解决,也就是在服务器端渲染HTML并将其发送至客户端。

在这个时代,并没有太多的构建工具。当时的JavaScript还没有模块(至少没有标准的模块),所以没有办法导入代码。所有的东西都是全局性的,要组织好它们是非常困难的。

我们可以理解,在这种环境中,JS通常被视为一种玩具语言,而不是用来编写完整的应用。开发人员最常做的事情就是引入jQuery,并为一些UI组件编写脚本,这就足够了。随着时间的推移,以及XHR的引入和普及,人们开始将UI流程的一部分放到一个页面中,特别是对于需要在客户端和服务器之间进行多次往返交互的复杂流程,但应用程序的大部分内容依然在服务器上。

这与移动应用刚开始出现的情况形成了鲜明的对比。从一开始,iOS和Android上的移动应用就是使用像Objective C和Java这样的严肃语言(Serious Languages™)编写的。此外,它们完全是由API驱动的,所有的UI逻辑都在设备上,而与服务器的通信靠纯粹的数据格式。这导致了更好的用户体验和移动应用的爆炸性增长,并且直接导致了如今关于移动应用和Web哪一种更好的争论。

将所有的这一切都使用JavaScript实现的想法最初被认为是很可笑的。但是,随着时间的推移,web应用变得更具野心。社交网络增加了聊天、DM(私信,direct message)和其他实时功能,Gmail和Google Docs的成功表明可以在浏览器中编写出不亚于桌面端的体验,越来越多的公司开始为越来越多的使用场景编写web应用,因为web在任何地方都可以运行,而且易于长期维护。这推动了整个行业的发展。现在来看,JS显然可以用来编写复杂的应用程序。

但是,当时这样做是很困难的。那时的JavaScript并不具备如今的所有功能,就像我说的,所有东西都是全局性的,开发人员通常需要手动下载并将每个外部库添加到静态资产文件夹中。当时还没有NPM,模块也不存在,JS甚至没有今天一半的特性。在大多数情况下,每个应用都是定制的,每个页面都有不同的插件设置,每个插件都有不同的系统来管理状态和渲染更新。为了解决这些问题,最早的JavaScript框架逐渐出现了。

第一代框架

大约在2000年代末和2010年代初,第一代用于编写完整客户端应用的框架开始出现。这个时代著名的框架包括:

  1. Backbone.js
  2. Angular 1
  3. Knockout.js
  4. SproutCore
  5. Ember.js
  6. Meteor.js

当然,还有很多其他的框架,甚至有的使用范围更大。这些是我记得的,主要是因为我曾经使用它们构建过原型或其他成果,而且它们比较流行。

这一代的框架正在试图进入一个未知的领域。一方面,它们要做的事情是很有野心的,很多人认为它们并不会成功。有许多的反对者认为单页JS应用(SPA)从根本上来讲是非常糟糕的,在很大程度上,他们的想法不无道理,因为客户端渲染意味着机器人不能很容易地爬取这些页面,而且用户要等待好几秒钟应用才会绘制。很多这样的应用是无障碍访问的噩梦,如果你关闭了JavaScript,它们根本无法运行。

另一方面,我们没有在JS中构建完整应用的经验,因此存在大量关于最佳实践的竞争性想法。大多数框架都在试图模仿其他平台上的流行做法,所以大多数的框架都经历了Model-View-*的变迁,如Model-View-Controller、 Model-View-Producer、Model-View-ViewModel等等。但从长远来看,这些都算不上真正有意义的工作,它们并不直观,而且很快就会变得非常复杂。

这也是一个我们真正开始尝试编译JavaScript应用的时代。2009年,Node.js发布,2010年NPM紧随其后,它们为(服务器端)的JavaScript引入了包的概念。CommonJS和AMD竞争如何最好地定义JS模块,而像Grunt、Gulp和Broccoli这样的构建工具则在竞争如何将这些模块组合成一个可交付的最终产品。在大多数情况下,它们都是类似于任务运行器的工具,它们其实可以构建任何东西,只是碰巧支持构建JavaScript而已,当然还包括HTML、CSS/SASS/LESS和其他web应用需要的内容。

但是,我们在这个时代学到了很多东西,它们都是重要的基础经验,包括:

总的来说,这个时代是富有成果的。尽管有缺点,但是随着应用复杂性的增加,将客户端与API进行分离的效益是巨大的,而且在许多情况下,所产生的用户体验是非常令人赞叹的。如果没有意外的话,这个时代可能会继续下去,我们到现在还在延续MV*风格的想法。

但后来一颗小行星突然出现,把现有的范式砸了个粉碎,造成了一个小规模的灭绝事件,把我们推进了下一个时代,这颗小行星叫做React。

以组件为中心的视图层

我并不认为是React发明了组件,但说实话,我也不太清楚它们最初起源于何方。我知道至少在.NET的XAML中就有类似的技术,而web component也在那时开始作为一项规范发展起来。归根到底,这并不重要,一旦这个想法被提出来,每个主流的框架都会很快采用它。

事后看来,这完全是有道理的:扩展HTML,减少长期存在的状态,将JS的业务逻辑直接与模板关联起来(不管具体是使用JSX、Handlebars还是Directives)。基于组件的应用消除了完成任务所需的大部分抽象,并且明显简化了代码的生命周期,也就是一切内容都与组件的生命周期而不是应用的生命周期关联在一起,这意味着作为开发人员,我们要考虑的事情要少得多。

然而,当时还有一个转变:框架开始把自己宣传成“视图层(view-layer)”,而不是完整的框架。它们不再试图解决前端应用面临的所有问题,而只是专注于解决渲染问题。其他的问题,如路由、API通信和状态管理,则由用户自己决定。这个时代著名的框架包括:

  1. React.js
  2. Vue.js
  3. Svelte
  4. Polymer.js

还有很多其他的框架。现在回想起来,我认为这为第二代框架奠定了基础,因为它确实做了两件重要的事情:

  1. 它极大地缩小了范围。框架的核心不是试图解决所有这些问题,而是专注于渲染,许多不同的理念和方向可以在更广泛的生态系统中探索其他功能。其中,有很多糟糕的解决方案,但也有很好的方案,为下一代从精华中挑选最好的想法铺平了道路。
  2. 它们的使用变得更加容易。如果采用某个完整的框架,让它接管整个web页面,这在很大程度上意味着要重写大部分的应用,对于现有的服务器端单体来说这是很难接受的。但是有了React和Vue这样的框架,我们可以把它们中的一小部分引入到现有的应用中,一次实现一个组件,让开发人员逐步迁移现有的代码。

这两个因素导致第二代框架迅速发展,并使第一代框架黯然失色,从长远来看,这一切似乎很有意义,是一种合理的演变。但如果你当时身处其中,那么这是一段相当令人沮丧的经历。

首先,在工作中争论该使用哪种框架,或者我们是否应该重写应用时,并不会经常涉及到这些基础的问题。相反,经常提到的是“它更快!”、“它更小!”或“它解决了我们的所有问题!”。还有关于函数式编程和面向对象编程的辩论,很多人把函数式编程作为所有问题的解决方案。公平地说,这些说法都是正确的。仅包含视图层的框架更小、更快(起码最初是这样的),而且包含了需要的全部内容(当然需要你自己构建或搭配很多的基础设施)。当然,函数式编程模式解决了大量困扰JavaScript的问题,我认为,JS因为它们而变得更好。

然而,现实是,从来没有没有银弹。应用程序仍然庞大、臃肿、复杂,状态仍然难以管理,路由和SSR等基本问题仍然需要解决。人们似乎想要放弃一揽子解决所有问题的解决方案,并把选择权留给读者。根据我的经验,这种情况也普遍存在于工程化的组织中,他们乐于接受这种改变,以便推出新的产品或特性,然而又不能提供开发这些额外特性所需的时间。

根据我的经验,这通常带来的结果就是围绕视图层建立自制的框架,这些框架本身就很臃肿、复杂,而且非常难以使用。我认为人们在使用SPA时遇到的许多问题都来源于这个分散的生态系统,而这个生态系统恰好又是SPA被大规模采用时出现的。我依然经常看到一些新的网站,它不能正确地实现路由或很好地处理其他细节,这绝对是令人沮丧的。

但另一方面,现有的第一代全服务框架在解决这些问题方面也做得不够好。部分原因是技术债务的包袱。第一代框架是在ES6之前诞生的,这要早于模块规范,早于Babel和Webpack,当时我们还没有掌握这么多的经验。迭代演进是非常困难的(作为前Ember核心团队成员,我对此深有体会),而且完全重写它们(就像Angular对Angular 2所做的那样)会扼杀社区的发展势头。因此,当涉及到JavaScript框架时,开发人员处于两难的境地,要么选择一个垂垂老矣的一站式解决方案,要么享受充分的自由,对框架的一半内容进行DIY,并希望能得到最好的结果。

就像我说的,当时这让人非常沮丧,但最后还是产生了大量的创新。随着这些框架摸索出最佳实践,JavaScript生态系统的发展非常迅速,还发生了一些其他的重要变化。

在这个时代结束的时候,一些问题仍然存在。状态管理和反应性仍然是(现在也是)棘手的问题,尽管我们有比以前更好的模式。性能仍然是一个难题,尽管情况正在改善,但仍然有很多臃肿的SPA应用。可访问性的情况也有所改善,但对于许多工程组织来说,它依然是一个事后才考虑的事情。但这些变化为下一代框架铺平了道路,我想说的是,我们现在正在进入下一代框架。

全栈框架

就我个人而言,最新的这个框架时代已经悄悄来临了。我想这是因为我在过去4年左右的时间里深入到了Ember渲染层的内部,试图清理那些作为第一代框架所具有的上述技术债务。但是,非常奇妙的是所有的这些第三代框架都是围绕上一代的视图层框架建立的。代表性的框架包括:

  1. Next.js(React)
  2. Nuxt.js(Vue)
  3. Remix(React)
  4. SvelteKit(Svelte)
  5. Gatsby(React)
  6. Astro(Any)

这些框架是随着视图层的成熟和巩固而出现的。既然我们都同意组件是核心原语(core primitive),其他的内容都要基于它来构建,那么标准化应用的其他组成部分,如路由器、构建系统、文件结构等,也就是合理的了。
这些元框架(meta-framework)开始构建与第一代框架类似的开箱即用的一站式解决方案,也就是从各自的生态系统中挑选最佳模式,并随着它们的成熟而将其纳入。

然后,它们更进一步。

在此之前,SPA一直都只关注客户端。SSR是每个框架都希望解决的问题,但只是作为一种优化,一种进行渲染的方式,最终会在数兆字节的JS加载完毕后被取代。只有一个第一代框架敢于想得更远,那就是Meteor.js,但它的 ”同构(isomorphic)JS“的想法从未得到广泛认可。

但随着应用规模和复杂性的增加,这个想法被重新审视。我们注意到,将后端和前端搭配在一起实际上是非常有用的,这样我们可以做很多的事情,比如为某些请求隐藏API的secret、在返回页面时修改头文件、代理API请求等。随着Node和Deno实现了越来越多的网络标准,服务器端JS和客户端JS之间的差距每年都在缩小,慢慢地它不再是一个疯狂的想法。将其与边缘计算和强大的工具结合起来,就会有一些令人难以置信的潜力。

最新一代的框架充分利用了这种潜力,将客户端和服务器端无缝融合在一起,我无法直观描述这种感觉有多么神奇。在过去9个月与SvelteKit的合作中,我不知道有多少次对自己说:“这就是我们一直该做的事情。”

以下是我最近遇到的一些任务,这些任务因这种模式而变得异常简单:

而这仅仅是冰山一角。这种模式真的有很多很酷的地方,其中最大的一点是它重振了渐进式增强的理念。借助服务器和客户端的组合特性,能够让客户端在用户禁用JavaScript的情况下回退到基本的HTML + HTTP方式。当我开始从事SPA相关的工作时,就完全放弃了这种做法,认为SPA才是未来的趋势,但现在我们突然看到解决这种问题是完全可能的,不得不说这是一件很酷的事情。

鉴于这些新特性,我把这些框架归类为新一代的框架。以前难以解决或不可能解决的问题现在变得微不足道,只需改变一点响应处理逻辑即可。可靠的性能和用户体验是开箱即用的,不需要任何额外的配置。我们不需要建立全新的服务,只需根据需要添加一些额外的端点或中间件即可。

我认为这一代的框架也解决了第一代、第二代框架和用户之间的一些主要矛盾。它始于向零配置的转变,但我认为最终它是由第二代框架生态系统的成熟和稳定所驱动的,它是一种文化的转变。第三代框架现在又开始尝试成为一站式的解决方案,试图解决前端开发人员需要解决的所有基本问题,而不仅仅是渲染的问题。

现在比以往任何时候都更能感觉到社区在解决SPA的问题方面是一致的,而且重要的是,大家在一起解决这些问题。

未来的路在何方?

总的来说,我认为JavaScript社区正朝着正确的方向发展。我们终于开发出了成熟的解决方案,可以从头开始构建完整的应用,而不是“只有视图层”的解决方案。我们终于开始与原生应用的SDK在同一赛道上竞争,提供开箱即用的完整工具包。我们在这方面仍有很多工作要做。在SPA领域,可访问性长期以来都是一个事后的因素,而除了GraphQL,我仍然认为在数据方面可以开展一些工作(不管你喜欢与否,大部分的web仍然运行在REST之上)。但总的趋势是正确的,如果我们继续朝着共享解决方案的方向发展,我认为我们可以用比以往更好的方式解决这些问题。

我还对将这些模式进一步带到web平台本身之中的潜力感到兴奋。Web component仍在悄悄地发展,致力于解决SSR和解决全局注册等问题,这将使它们与这些第三代框架更易于兼容。在另一个方向,WebAssembly可以以一种令人难以置信的方式迭代该模式。想象一下,我们能够用任何语言编写一个全栈的框架。同构的Rust、Python、Swift、Java等语言最终可以将前台和后台之间的障碍完全消除,只需要在系统中增加一点HTML模板即可(令人觉得讽刺的是,这似乎绕了一大圈,不过我们有了更好的用户体验)。

我最大的希望是,我们正在摆脱碎片化的时代,走过每天都有新JS框架涌现的时代。自由和灵活孕育了创新,但它们也导致了web体验的混乱、不连贯,而且常常是根本性的破坏。设想有的开发人员不得不在50多个选项中做出选择,并在有限的资源和紧迫的期限内将它们拼凑在一起,所以解决碎片化所带来的体验是非常有意义的。一些应用非常快速、一致、可靠,而且使用起来很有趣,而另一些则令人沮丧、困惑、缓慢并且功能不完整。

如果我们能为开发人员提供更易于使用的工具,默认做正确的事情,也许web站点普遍会更好一些,用户体验会普遍更顺畅一些。这不会修复所有网站,很少有代码可以解决糟糕的用户体验设计所带来的问题。但它会奠定一个共同的基础,所以每个网站在开始时都能更好一些,每位开发人员都有更多的时间专注于其他事情。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注