早在2012年,我就开始用JavaScript进行编码。
我选择的第一个框架实际上是Angular1。彼时,我基于FuelPHP后端开发了一个相当大的应用程序。在功能上,每当重新渲染子路由/出口时,路由器就会闪烁,这一点是我们在开发过程中没有考虑过的突发情况。后来,有人向我推荐了RubyonRails+Ember的解决方案,在试过之后,我觉得效果很好。我喜欢这两个框架的理念,喜欢构建它们的社区。
从那时起,很多事情都发生了变化,许多新的框架出现,旧的框架淘汰。用JavaScript在浏览器中构建应用程序的想法,从少数开发者的尝试逐渐变成了前端领域中一种标准的操作方式。随着开发者所构建的基础设施的改变,带来了许多新的可能。
回顾过去,我觉得很有趣,因为我们经常为小的错误争论不休,而忽略了更有意义的事情,当然这也是“事后诸葛亮”了。所以我想做一个回顾,总结过去几十年的JavaScript开发,看看我们已经走了多远。我认为我们可以把它粗略地分为四个主要时代:
前世
第一代框架
以组件为中心的视图层
全栈式框架(←这也是我们当前所处的位置)
每个时代都有自己标志性的特点和亟待解决的矛盾,每一个时代也赋予了我们不同的经验与教训,最终让我们取得了进步。
JavaScript是在1995年首次发布的。就像我上面提到的,我是在2012年开始写JS的,差不多是JavaScript诞生的20年后,也就是我称之为FirstFrameworks时代的开始。你可以想象,这个时代可能会被分解成许多子时代,每个时代都有自己的模式、库和构建工具等。
也就是说,我不能写自己没有经历过的事情。当我开始编写前端应用程序时,新一代的框架刚刚开始成熟,像Angular.js、Ember.js、Backbone等等。
在这之前,最先进的是jQuery、MooTools等库。这些库在他们的时代是非常重要的,它们帮助消除浏览器实现JavaScript方式之间的差异,这些差异非常重要。例如,IE实现事件的方式与Netscape(网景浏览器)完全不同,其方式分别为事件冒泡与事件捕获。这就是为什么我们今天的标准最终实现了这两种方式的原因,但在这之前,你需要使用库来编写能在两种浏览器上使用的代码。这些库主要用于制作小型的、独立的用户界面部件。大多数应用程序的业务逻辑仍然是通过表单和标准的HTTP请求进行的,即在服务器上渲染HTML并将其提供给客户端。
在这个时代,也没有很多构建工具可言(至少我了解到的没有)。当时的JavaScript还没有模块(至少没有标准的模块)功能,所以没有任何办法导入代码。所有的东西都是全局性的,要组织好这些东西是非常困难的。
这与移动应用开始出现时的情况形成了鲜明的对比。从一开始,iOS和Android上的移动应用程序就是用Objective-C和Java等高级语言编写的完整应用。此外,它们是完全由API驱动——所有的UI逻辑都存在设备上,而与服务器的通信则纯粹是以数据形式进行。这带来了更好的用户体验和移动应用程序的爆炸性增长,直接导致了我们今天关于移动设备和Web网络哪个更好的争论。
很明显,现在JS可以用来编写更为复杂的应用程序。然而,当时环境下,想要实现这一点还是有些困难的,因为那时的JavaScript并不具备今天的所有功能。就像我说的,所有东西都是全局性的,你通常需要手动下载并将每个外部库添加到你的静态资产文件夹中。当时还没有NPM,模块也不存在,JS也没有今天一半的功能。在大多数情况下,每个应用程序都是需要定制的,每个页面都有不同的插件设置,每个插件都有不同的系统来管理状态和渲染更新。为了解决这些问题,最早的JavaScript框架出现了。
在2000年代末和2010年代初,第一批专门用于编写完整客户端应用程序的JS框架开始出现。这个时代的几个著名的框架是:
Backbone.js
Angular1
Knockout.js
SproutCore
Ember.js
Meteor.js
当然,还有很多其他或是在某些圈子里更知名一些的框架。不过,以上这些是我记得的,我也常用它们来做原型或进行构建。
另一方面,我们没有在JS中构建完整应用程序的经验,因此关于最佳方法上,很多开发者有许多不同的想法。大多数框架都试图模仿其他平台上的流行做法,所以几乎所有的框架最后都是「Model-View-*」的迭代方式,如Model-View-Controller、Model-View-Producer、Model-View-ViewModel等。从长远来看,其中的一些问题的确被解决了,但是解决方案往往不太直观,而且在应用过程中越来越复杂。
这也是一个我们真正开始尝试如何编译JavaScript应用程序的时代。
Node.js在2009年发布,NPM在2010年紧随其后,将包的概念引入(服务器端的)JavaScript中。CommonJS和AMD就如何定义最好的JS模块展开竞争,而像Grunt、Gulp和Broccoli这样的构建工具则争夺如何将这些模块组合成一个可交付的最终产品。
在大多数情况下,这些都是非常通用的类似于任务运行器的工具,可以真正构建任何东西,只是碰巧与HTML、CSS/SASS/LESS等技术结合,基于JavaScript开发出很多适用于Web端的产品。
从这个时代,我们学到了很多东西,收获了宝贵的经验,包括:
URL路由是基础。没有它的应用程序往往会出现Bug,开发者需要从一开始就在框架中考虑它。
通过模板语言扩展HTML是一个强大的抽象层。即使有时它可能有点笨拙,但它可以让你的UI与状态保持同步且更容易。
SPA(单页面应用)的性能很差,而且Web开发有许多原生应用没有的额外限制。我们需要通过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因为它们而变得更好。
结果,根据我的经验,开发更多的时候是围绕这些视图层建立的自制框架,这些框架本身就很臃肿、复杂,而且非常难以操作。我认为人们在SPA上遇到的许多问题都来自于这个支离破碎的框架系统,而这个框架系统恰好出现在SPA使用量激增的时候。我仍然经常遇到一个新网站不能很好地处理路由或其他小细节,这绝对是令人沮丧的。
但另一方面,现有的全服务第一代框架也不能很好地解决这些问题。部分原因在于依然有大量的科技债务负担。第一代框架是在ES6之前,同时也在模块以及Babel和Webpack之前,是在我们弄清楚许多事情之前建立的。迭代进化是非常困难的(作为前Ember核心团队成员,我对此深有体会),而且完全重写它们,就像Angular对Angular2所做的那样,扼杀了他们的发展势头。因此,当涉及到JavaScript框架时,开发人员处于两难境地——要么选择一个过时的一体化解决方案,要么选择自由发挥,并DIY一半的框架,以此希望得到最好的结果。
就像我说的,当时这让人非常沮丧,可到最后还是涌现了大量的创新。随着找出这些框架的最佳实践,JavaScript的整个系统都发展得非常快,还发生了一些其他的关键变化:
像Babel这样的转译器成为常态,并有助于使语言现代化。与其等待数年才能实现功能的标准化,不如今天就能使用,而且语言本身也开始以更快、更多的迭代速度增加功能。
ES模块被标准化,使我们最终能够开始围绕它们构建现代的构建工具,如Rollup、Webpack和Parcel。基于导入的捆绑慢慢成为规范,即使是样式和图片等非JS资产也是如此,这极大地简化了构建工具的配置,使它们变得更精简、更快速,总体上更上一层楼。
随着越来越多的API被标准化,Node和Web标准之间的差距也慢慢缩小。SSR开始成为一种真正的可能性,随后变成每个标准的应用程序都在做的事情,但每次都是某种程度上的定制化设置。
到这个时代结束时,仍然存在一些问题。即使我们有了比以前更好的模式,但状态管理和反应性至今都是棘手的问题。性能情况也正在改善,可仍然存在许多臃肿的SPA。可访问性的情况也经常是许多开发团队事后的想法。但这些变化为下一代框架铺平了道路。而我想说,我们刚刚进入下一个框架时代。
全栈式框架
Next.js(React)
Nuxt.js(Vue)
Remix(React)
SvelteKit(Svelte)
Gatsby(React)
Astro(Any)
这些框架随着视图层的成熟和固化而出现。既然我们都同意组件是构建框架的核心基础,那么就有必要开始标准化应用程序的其他部分——路由器、构建系统、文件夹结构等等。慢慢地,这些元框架开始构建第一代一体化解决方案所能提供的相同功能,从各组件内部中选择最佳的模式,并随着它们的成熟而将它们合并,随后再更进一步发展。
到目前为止,SPA一直专注于客户端。SSR是每个框架都渴望解决的问题,但只是作为一种优化,一种渲染的方式,最终会在JS加载完成后被替换。第一代框架中只有一个敢于做得更大,那就是Meteor.js,但它的“同构JS”理念从未真正实现。
但随着应用程序的规模和复杂性的增长,这个想法被重新审视。我们注意到,将后端和前端组合在一起是非常有用的,这样你就可以为某些请求隐藏API秘密、在页面返回时修改header、代理API请求。随着Node和Deno实现越来越多的web标准,服务器端JS和客户端JS之间的差距每年都在缩小,这似乎不再是一个疯狂的想法。将它与边缘计算和令人惊叹的工具相结合,你就有了不可思议的潜力。
最新一代的框架充分利用了这一潜力,将客户端和服务器无缝地融合在一起,我无法强调这是多么令人惊叹的感觉。在过去的9个月里,我和SvelteKit一起工作,坐下来的并感叹:“我们应该一直这样做。”的次数多得数不清了。
以下是我最近完成的一些任务,通过这个设置,它们变得非常容易:
添加服务器端OAuth到我们的应用程序,这样authToken永远不会离开服务器,同时还有一个API代理,每当一个请求发送到我们的API时添加Token。
将某些路由直接代理到我们的CDN,这样我们就可以托管内置在任何其他框架中的静态HTML页面,允许用户制作他们自己的定制页面(这是我们为一些客户提供的服务)。
当我们需要使用需要密钥的外部服务时,添加几个不同的一次性API路由(不需要添加一个全新的路由到我们的API并与后端人员协调)。
将我们对launchdark的使用转移到服务器端,这样我们就可以加载更少的JS并降低整体成本。
而这仅仅是冰山一角。这种模式真的有很多很酷的地方,其中最大的一点是它如何重振渐进式增强的理念,利用服务器和客户端的组合特性,允许客户端在用户禁用JavaScript的情况下回退到基本的HTML+HTTP。当我开始从事SPA工作时,我已经完全放弃了这种做法,认为它们只是未来的趋势,但我们有可能看到它再次回归的景象,这真的很酷。
从经验上看,根据这些新功能,我把这些框架归类为新一代的框架。以前难以解决或不可能解决的问题现在变得微不足道,只是改变了一点点响应处理逻辑。不需要任何额外的配置的情况下,可靠的性能和用户体验是信手拈来的。我们不需要建立整个新的服务,而是能够根据需要添加一些额外的端点或中间件。这已经改变了生活。
我认为这一代框架也解决了第一代和第二代框架及其用户之间的一些主要矛盾点。它始于向零配置术语的转变,但我认为它最终是由第二代框架周围的衍生的系统所驱动的,并逐渐变得成熟和稳定。第三代框架现在又开始尝试成为一体化的解决方案,试图解决我们作为前端开发者需要解决的所有基本问题,而不仅仅是渲染问题。
现在比以往任何时候都更感觉到社区在解决困扰SPA的所有问题上是一致的,而且重要的是,他们在一起解决这些问题。
我们下一步该怎么走?
总的来说,我认为JavaScript社区正朝着正确的方向发展。
当下的开发者们正在专注研究从头开始构建完整应用程序的成熟解决方案,这些解决方案并不局限于"一个视图层"。同时,我们终于开始与原生应用的SDK在同一起跑线上竞争,提供一个开箱即用的完整工具包。
接下来,我们仍有很多工作要做。在SPA领域,可访问性是一个长期需要解决的问题;在GraphQL之外,我仍然认为数据可以被用来实现一些功能(不管你喜欢与否,大部分的网络仍然运行在REST上)。但趋势是正确的,如果我们继续朝着共享解决方案的方向发展,我认为我们可以用比以前更好的方式解决这些问题。
我还对将这些模式更进一步带到网络平台本身背后的潜力感到兴奋。Web组件仍在悄悄地迭代,致力于解决SSR和摆脱全局注册等问题,这将使它们与这些第三代框架更加兼容。在另一个方向,WebAssembly可以以一种令人难以置信的方式迭代这种模式。想象一下,能够用任何语言编写一个全栈框架。同类型的Rust、Python、Swift、Java等语言最终可以将前端和后端之间的障碍减少到几乎为零,只是在你的系统边缘有一点HTML模板(讽刺的是,尽管有了更好的用户体验,但这使我们几乎绕了一圈)。