作者:Chris
早在2012年,我就开始主要用JavaScript进行编程。我曾为一家本地企业用PHP从头到尾做了一个应用,一个基本的CMS内容管理系统和网站,他们决定要重写它并增加一些功能。这个项目的经理希望我使用.NET,部分原因是他懂.NET,但也因为他希望这个网站感觉像一个本地应用程序--没有页面刷新或行动之间的长时间停顿。经过一番研究和原型设计,我说服了他,我们可以用Web做同样的事情,使用刚刚开始出现的全新JS框架之一。
我选择的第一个框架实际上是Angular 1。在我遇到社区路由器的一些问题之前,我建立了一个相当大的应用程序,并使用了FuelPHP的后端--每当你重新渲染子路由/出口时,它就会闪烁,而且真的感觉它在设计时没有考虑到这种使用情况。有人向我推荐了Ruby on Rails + Ember,在试过之后,我觉得效果很好。我喜欢这两个框架的理念,喜欢这些社区,而且与当时的替代方案相比,总的来说,它非常有成效。
从那时起,很多事情都发生了变化--框架来了,走了,而且有了很大的发展。你可以在浏览器中用JavaScript构建应用程序的想法,从某种程度上的边缘做法变成了一种标准做法。我们构建的基础设施已经完全改变,实现了大量的新的可能性。
在这段时间里,各种想法之间的竞争和冲突也是相当多的。我想我们大多数在前端领域工作了一段时间的人可能都有过一些关于......嗯,一切的争论。使用哪种JavaScript框架,如何编写CSS,函数式编程与面向对象编程,如何最好地管理状态,哪种构建系统或工具最灵活、最快速,等等。回顾过去,我觉得很有趣,我们经常为错误的事情争论不休,而忽略了更大的模式,当然这就是事后诸葛亮的好处。
所以我想做一个回顾,回顾过去几十年的JavaScript开发,看看我们已经走了多远。我认为我们可以把它大致分为四个主要时代:
之前的时代
第一个框架
以组件为中心的视图层
全栈式框架 (← 我们目前在这里)
每个时代都有自己的主题和核心冲突,在每一个时代,我们都作为一个社区学到了关键的经验教训,并缓慢但肯定地取得了进步。
今天,争论仍在继续。网络是否变得过于臃肿了?普通网站真的需要用React编写吗?我们甚至应该使用JavaScript吗?我不认为我们能在这里看到未来,而且最后我怀疑我们可能会再次发现,我们在互相讨论,错过了更大的趋势。但是,也许从过去得到的一些观点会帮助我们向前迈进。
之前的时代
JavaScript于1995年首次发布。就像我上面提到的,我是在2012年开始写JS的,差不多20年后,接近我称之为第一批框架的时代的开始。你可以想象,我在这里可能会掩盖很多历史,而且这个时代可能会被分解成许多子时代,每个时代都有自己的模式、库和构建工具等等。
也就是说,我不能写我没有经历过的事情。当我开始编写前端应用程序时,新一代的框架刚刚开始成熟。Angular.js、Ember.js、Backbone,等等。
在这之前,最先进的是jQuery和MooTools等库。这些库在他们的时代是非常重要的--它们顺滑的帮助浏览器实现了JavaScript的方式之间的差异,这些差异是非常重要的。例如,Internet Explorer实现事件的方式与Netscape完全不同--冒泡事件与捕捉事件。这就是为什么我们今天的标准最终实现了这两种方式,但在这之前,你需要使用库来编写能在两种浏览器上使用的代码。这些库主要用于制作小型的、独立的用户界面部件。大多数应用程序的业务逻辑仍然是通过表单和标准的HTTP请求进行的--在服务器上渲染HTML并将其提供给客户端。
在这个时代,也没有很多构建工具,至少我知道。当时的JavaScript还没有模块(至少没有标准的模块),所以没有任何办法导入代码。所有的东西都是全局性的,要组织好这些东西是非常困难的。
在这种环境下,可以理解的是,JS通常被视为一种玩具语言,而不是你用它来写一个完整的应用程序。你最常做的事情是加入jQuery,为一些UI小工具编写一些脚本,然后就可以了。随着时间的推移和XHR的引入和普及,人们开始把他们的UI流程的一部分放到一个页面中,特别是对于需要在客户端和服务器之间进行多次来回交互的复杂流程,但应用程序的大部分内容还是留在服务器上。
这与移动应用开始出现时的情况形成了鲜明的对比。从一开始,iOS和Android上的移动应用就是用Objective C和Java等严肃语言™编写的完整应用。此外,它们是完全由API驱动的--所有的UI逻辑都在设备上,而与服务器的通信则纯粹是数据格式。这推动了更好的用户体验和移动应用的爆炸性增长,直接导致了我们今天关于mobile和web哪个更好的争论。
用JavaScript来做所有这些事,一开始被认为是可笑的。但随着时间的推移,应用程序开始变得更加雄心勃勃。社交网络增加了聊天、DM和其他实时功能,Gmail和Google Docs表明可以在浏览器中编写与桌面相当的体验,越来越多的公司转向为越来越多的使用案例编写网络应用,因为网络在任何地方都可以使用,而且更容易长期维护。这推动了整个行业的发展--现在很明显,JS可以用来编写复杂的应用程序。
然而,这样做是困难的。当时的JavaScript并不具备今天的所有功能--就像我说的,所有东西都是全局性的,你通常需要手动下载并将每个外部库添加到你的静态资产文件夹中。当时还没有NPM,模块也不存在,JS也没有今天一半的功能。在大多数情况下,每个应用程序都是定制的,每个页面都有不同的插件设置,每个插件都有不同的系统来管理状态和渲染更新。为了解决这些问题,最早的JavaScript框架开始出现了。
第一批框架
大约在2000年代末和2010年代初,第一批专门用于编写完整客户端应用程序的JS框架开始出现。这个时代的几个著名的框架是:
Backbone.js
Angular 1
Knockout.js
SproutCore
Ember.js
Meteor.js
当然,还有很多其他的,可能还有一些在某些圈子里更大的。这些是我记得的,主要是因为我用它们来做原型或构建东西,而且它们相对来说很流行。
这是一代框架,正在进入未知的领域。一方面,他们试图做的事情是非常雄心勃勃的,很多人认为它不会真的成功。有许多反对者认为单页JS应用程序(SPA)从根本上来说更糟糕,而且在很多方面他们是对的--客户端渲染意味着机器人不能轻易抓取这些页面,而且用户甚至需要等待几秒钟才能开始绘制应用程序。很多这些应用程序都是无障碍的噩梦,如果你关闭了JavaScript,它们就根本无法工作。
另一方面,我们没有在JS中构建完整应用程序的经验,因此有大量关于最佳方法的竞争性想法。大多数框架都试图模仿其他平台上的流行做法,所以几乎所有的框架最后都是模型-视图-*的某种迭代。Model-View-Controller,Model-View-Producer,Model-View-ViewModel,等等。但从长远来看,这些都不是真正意义上的工作--它们不是特别直观,而且很快就变得非常复杂。
这也是一个我们真正开始尝试如何编译JavaScript应用程序的时代。Node.js在2009年发布,NPM在2010年紧随其后,为(服务器端的)JavaScript引入了包。CommonJS和AMD争夺如何最好地定义JS模块,而像Grunt、Gulp和Broccoli这样的构建工具则争夺如何将这些模块组合成一个可运输的最终产品。在大多数情况下,这些都是类似于任务运行器的工具,它们真的可以构建任何东西,只是碰巧要构建JavaScript--以及HTML、CSS/SASS/LESS和其他许多进入网络应用的东西。
然而,我们从这个时代学到了很多东西;重要的基本经验,包括:
基于URL的路由是基础。没有这种路由的应用程序会破坏web,因此需要在框架中从一开始就考虑到这一点。
通过模板语言扩展HTML是一个强大的抽象层。即使它有时会有点笨拙,但它使你的用户界面与你的状态保持同步变得更加容易。
SPA的性能是很难的,网络有很多额外的限制,而原生应用则没有。我们需要通过web发送所有的代码,让它JIT,然后运行,以使我们的应用程序开始运行,而本地应用程序已经被下载和编译了。这是一项艰巨的任务。
作为一种语言,JavaScript有很多问题,它真的需要被改进,以使事情变得更好--框架不能单独做到这一点。
我们绝对需要更好的构建工具、模块和包装,以便大规模地编写应用程序。
总的来说,这个时代是富有成效的。尽管有缺点,但随着应用程序的复杂性增加,将客户端与API分离的好处是巨大的,而且在许多情况下,所产生的用户体验是惊人的。如果情况不同,这个时代可能会继续下去,我们到现在还在迭代MV*风格的想法。
但后来一颗小行星突然出现,把现有的范式砸得粉碎,造成了一个小的灭绝事件,把我们推进了下一个时代--这颗小行星名叫React。
以组件为中心的视图层
我不认为 React 发明了组件,但老实说,我不太确定它们最初是从哪里来的。我知道 .NET 中的现有技术至少可以追溯到 XAML,并且 Web 组件也开始作为规范开发。最终这并不重要——一旦这个想法出现,每个主要框架都很快采用了它。
事后看来,这完全是有道理的--扩展HTML,减少长期存在的状态,将JS业务逻辑直接与模板联系起来(无论是JSX还是Handlebars还是Directives)。基于组件的应用程序消除了完成工作所需的大部分抽象概念,并且明显地简化了代码的生命周期--一切都与组件的生命周期而不是应用程序的生命周期联系在一起,这意味着作为一个开发者,你要考虑的事情要少得多。
然而,当时还有一个转变:框架开始把自己吹嘘成 "视图层",而不是完整的框架。他们不再解决前端应用所需的所有问题,而只是专注于解决渲染问题。其他问题,如路由、API通信和状态管理,则由用户自己决定。这个时代著名的框架包括:
React.js
Vue.js
Svelte
Polymer.js
还有很多其他的。现在回过头来看,我认为这是第二代框架的一个流行框架,因为它确实做了两件主要的事情:
它极大地缩小了范围。该框架的核心不是试图在前期解决所有这些问题,而是专注于渲染,许多不同的想法和方向可以在更广泛的生态系统中探索其他功能。有很多糟糕的解决方案,但也有很好的解决方案,为下一代从精华中挑选最好的想法铺平了道路。
这使得采用它们变得更加容易。采用一个完整的框架,接管你的整个网页,几乎意味着重写你的大部分应用程序,这对现有的服务器端单体来说是不可能的事。有了React和Vue这样的框架,你可以把它们的一小部分放到现有的应用程序中,一次一个小部件或组件,让开发者逐步迁移他们现有的代码。
这两个因素导致第二代框架迅速发展,使第一代框架黯然失色,从远处看,这一切似乎很有意义,是一种合理的演变。但当时身处其中,是相当令人沮丧的经历。
首先,在工作中争论使用哪种框架,或者我们是否应该重写我们的应用程序时,并不经常遇到这样的框架。相反,经常是 "它更快!"或 "它更小!"或 "它是你需要的一切!"。还有关于函数式编程和面向对象编程的辩论,很多人把FP作为我们所有问题的解决方案。公平地说,这些事情都是真的。仅有视图层的框架更小(起初)、更快(起初),而且是你所需要的全部(如果你自己建立或缝合了很多东西)。当然,函数式编程模式解决了大量困扰JavaScript的问题,而且我认为平均来说,JS因为它们而变得更好。
然而,现实是从来没有什么灵丹妙药。应用程序仍然庞大、臃肿、复杂,状态仍然难以管理,路由和SSR等基本问题仍然需要解决。对于我们中的很多人来说,人们想要的似乎是放弃试图解决所有这些问题的解决方案,而换成一个让读者自己去解决的解决方案。根据我的经验,这也是工程小组的普遍做法,他们会很高兴地接受这种改变,以便推出新的产品或功能,然后又不资助全面开发所有这些额外功能所需的时间。
结果(根据我的经验,更多的时候)是围绕这些视图层建立的自制框架,这些框架本身就很臃肿、复杂,而且非常难以操作。我认为人们在使用SPA时遇到的许多问题都来自于这个分散的生态系统,而这个生态系统恰恰是在SPA使用爆炸性增长的时候出现的。我仍然经常遇到一个新的网站,它不能正确地做路由或很好地处理其他小细节,这绝对是令人沮丧的。
但另一方面,现有的第一代全服务框架在解决这些问题方面也做得不是太好。部分原因是由于很多技术债务的包袱。第一代框架是在ES6之前,在模块之前,在Babel和Webpack之前,在我们弄清楚许多事情之前建立的。迭代进化是非常困难的(作为前Ember核心团队成员,我对此深有体会),而且完全重写它们,就像Angular对Angular 2所做的那样,扼杀了他们社区的发展势头。因此,当涉及到JavaScript框架时,开发人员处于两难境地--要么选择一个开始显示其年龄的一体化解决方案,要么跳入自由竞争中,DIY一半的框架,希望得到最好的结果。
就像我说的,当时这让人非常沮丧,但最后还是产生了大量的创新。随着这些框架找出它们的最佳实践,JavaScript生态系统发展得非常快,还发生了一些其他的关键变化:
像Babel这样的转译器成为常态,并有助于使语言现代化。与其等待多年的功能标准化,不如今天就能使用,而且语言本身也开始以更快、更迭代的速度增加功能。
ES模块被标准化,使我们最终能够开始围绕它们构建现代的构建工具,如Rollup、Webpack和Parcel。基于导入的捆绑慢慢成为规范,即使是样式和图片等非JS资产也是如此,这极大地简化了构建工具的配置,使它们变得更精简、更快速,而且总体上更好。
随着越来越多的API被标准化,Node和Web标准之间的差距也慢慢缩小。SSR开始成为一种真正的可能性,然后是每个严肃的应用程序都在做的事情,但每次都是一种定制的设置。
边缘计算的发布,使基于JavaScript的服务器应用程序在分发/响应时间方面获得了SPA的好处(SPA由于是CDN上的静态文件,所以以前一般可以更快地开始加载,即使最后完全加载和渲染需要更长时间)。
在这个时代结束的时候,一些问题仍然存在。状态管理和反应性仍然是(现在也是)棘手的问题,尽管我们有比以前更好的模式。性能仍然是一个困难的问题,尽管情况正在改善,但仍然有很多很多臃肿的SPA在那里。可访问性的情况也有所改善,但对于许多工程机构来说,它仍然经常是一个事后的想法。但这些变化为下一代框架铺平了道路,我想说的是,我们现在正在进入下一代框架。
全栈式框架
就我个人而言,上一个框架时代真的悄悄来临了。我想这是因为我在过去4年左右的时间里深入Ember渲染层的内部,试图清理前面提到的影响它作为第一代框架的技术债务(仍然)。但这也是因为它更加微妙,因为所有这些第三代框架都是围绕上一代的视图层框架建立的。值得注意的条目包括:
Next.js (React)
Nuxt.js (Vue)
Remix (React)
SvelteKit (Svelte)
Gatsby (React)
Astro (Any)
这些框架是随着视图层的成熟和巩固而开始的。既然我们都同意组件是建立在核心基础之上的,那么开始标准化应用程序的其他部分--路由器、构建系统、文件夹结构等,就很有意义了。慢慢地,这些元框架开始建立起与第一代多合一解决方案开箱即用的相同功能,从各自的生态系统中挑选最佳模式,并随着它们的成熟而将其纳入。
然后,他们更进一步。
在这之前,SPA一直都只关注客户。SSR是每个框架都希望解决的问题,但只是作为一种优化,一种获得渲染的方式,最终会在数兆字节的JS最终加载完毕后被取代。只有一个第一代框架敢于想得更远,即Meteor.js,但它的 "同构JS "的想法从未真正起飞。
但随着应用程序规模和复杂性的增加,这个想法被重新审视。我们注意到,将后端和前端配对在一起实际上是非常有用的,这样你就可以做一些事情,比如为某些请求隐藏API秘密,在返回页面时修改头信息,代理API请求。随着Node和Deno实现了越来越多的网络标准,服务器端JS和客户端JS之间的差距每年都在缩小,它开始看起来毕竟不是一个疯狂的想法。将其与边缘计算和惊人的工具结合起来,就会有一些令人难以置信的潜力。
最新一代的框架充分利用了这种潜力,将客户端和服务器无缝地融合在一起,我无法充分强调这种感觉有多么神奇。在过去9个月与SvelteKit的合作中,我不知道有多少次坐下来对自己说:"这就是我们应该一直做的事情。"
以下是我最近完成的一些任务,通过这种设置,这些任务变得异常简单:
在我们的应用程序中添加服务器端的OAuth,这样一来,auth令牌就不会离开服务器,同时还有一个API代理,在向我们的API发送请求时添加令牌。
将某些路由直接代理到我们的CDN,这样我们就可以托管在任何其他框架中构建的静态HTML页面,允许用户制作自己的自定义页面(我们为一些客户提供的服务)。
当我们需要使用一个需要密匙的外部服务时,添加几个不同的一次性API路由(不需要为我们的API添加一个全新的路由并与后端人员协调)。
将LaunchDarkly的使用转移到服务器端,这样我们可以加载更少的JS,从而降低整体成本。
通过后端路由代理我们的Sentry请求,这样我们就可以捕捉到由于广告屏蔽器而未被报告的错误。
而这仅仅是冰山一角。这种模式真的有很多很酷的地方,其中最大的一点是它如何重振渐进式增强的理念,利用服务器和客户端的组合特性,允许客户端在用户禁用JavaScript的情况下回退到基本的HTML + HTTP。当我开始从事SPA工作时,我已经完全放弃了这种做法,认为它们是未来的趋势,但我们有可能看到它卷土重来的世界,这真的很酷。
从经验上看,这些新功能让我把这些框架归类为新一代的框架。以前难以解决或不可能解决的问题现在变得微不足道,只是改变了一点点的响应处理逻辑。可靠的性能和用户体验是开箱即用的,不需要任何额外的配置。我们不需要建立整个新的服务,而是能够根据需要添加一些额外的端点或中间件。这已经改变了生活。
我认为这一代框架也解决了第一代和第二代框架及其用户之间的一些主要矛盾点。它始于向零配置术语的转变,但我认为它最终是由第二代框架周围的生态系统成熟和稳定所驱动的,它是一种文化转变。第三代框架现在又开始尝试成为一体化的解决方案,试图解决我们作为前端开发者需要解决的所有基本问题--不仅仅是渲染问题。
现在比以往任何时候都感觉到社区在解决困扰SPA的所有问题上是一致的,而且重要的是,他们在一起解决这些问题。
Prev Chapter:一个Vite + Vue3 + Typescript + Pinia + Vueuse的项目模板及教程
Next Chapter:经典论文《The Anatomy of a Large-Scale Hypertextual Web Search Engine》pdf下载