【转】跨过四个时代,JavaScript框架终于可以与原生应用SDK竞争了

作者 | Chris Garrett
译者 | 张卫滨
策划 | 闫园园
本文最初发表于作者的个人博客站点,经原作者 Chris Garrett 授权,由 InfoQ 中文站翻译分享。

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

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

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

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

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

  • 史前时期

  • 第一代框架

  • 以组件为中心的视图层

  • 全栈框架(←我们在这里)

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

如今,这些争论仍在继续: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 应用需要的内容。

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

  • 基于 URL 的路由是基础,没有路由的应用会破坏 Web,而且在框架的设计之初就要考虑到这一点。

  • 通过模板语言扩展 HTML 是一个非常强大的抽象概念。即便它有时看上去很笨拙,但是它能够让用户界面与状态之间的同步变得更加容易。

  • SPA 的性能是很难解决的问题,web 有很多原生应用所没有的额外限制。我们需要通过网络发送所有的代码,让它 JIT(即时编译)并运行,这样我们的应用才能开始执行,而原生应用则早就已经下载和编译好了。这是一项艰巨的任务。

  • 作为一门语言,JavaScript 有很多问题,它真的需要改进,以便于达成更好的效果,仅靠框架并不能实现这一点。

  • 我们绝对需要更好的构建工具、模块和打包机制,以便编写大规模的应用。

总的来说,这个时代是富有成果的。尽管有缺点,但是随着应用复杂性的增加,将客户端与 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

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

  • 它极大地缩小了范围。框架的核心不是试图解决所有这些问题,而是专注于渲染,许多不同的理念和方向可以在更广泛的生态系统中探索其他功能。其中,有很多糟糕的解决方案,但也有很好的方案,为下一代从精华中挑选最好的想法铺平了道路。

  • 它们的使用变得更加容易。如果采用某个完整的框架,让它接管整个 web 页面,这在很大程度上意味着要重写大部分的应用,对于现有的服务器端单体来说这是很难接受的。但是有了 React 和 Vue 这样的框架,我们可以把它们中的一小部分引入到现有的应用中,一次实现一个组件,让开发人员逐步迁移现有的代码。

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

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

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

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

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

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

  • 像 Babel 这样的转译器成为常态,并帮助实现了语言的现代化。借助它们,我们不必等待许多年才能使用标准化的特性,而是马上就是使用,语言本身也开始以更快的节奏增加新的特性。

  • ES 模块被标准化,使我们最终能够开始围绕它们构建现代化的构建工具,如 Rollup、Webpack 和 Parcel。基于 bundling 的导入慢慢成为规范,甚至样式和图片等非 JS 资产也是如此,这极大地简化了构建工具的配置,使它们变得更精简、更快速,而且总体上更好。

  • 随着越来越多的 API 被标准化,Node 和 Web 标准之间的差距也慢慢缩小。SSR 开始逐渐成为可能,然后成为每个严肃的应用都要做的事情,但每次都需要定制化的设置。

  • 边缘计算的出现,使基于 JavaScript 的服务器应用也在分发 / 响应时间方面获得了 SPA 的好处(由于是 CDN 上的静态文件,所以 SPA 一般可以更快地开始加载,即便最后完全加载和渲染可能需要更长时间)。

在这个时代结束的时候,一些问题仍然存在。状态管理和反应性仍然是(现在也是)棘手的问题,尽管我们有比以前更好的模式。性能仍然是一个难题,尽管情况正在改善,但仍然有很多臃肿的 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 的合作中,我不知道有多少次对自己说:“这就是我们一直该做的事情。”

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

  • 将服务器端的 OAuth 添加到我们的应用中,这样 auth token 就不会离开服务器,同时实现一个 API 代理,在向我们的 API 发送请求时添加 token。

  • 将某些路由直接代理到 CDN,这样我们就可以托管在任何其他框架中构建的静态 HTML 页面,允许用户制作自己的自定义页面(我们为一些客户提供的服务)。

  • 当使用需要密匙的外部服务时,添加多个不同的一次性 API 路由(不需要为 API 添加全新的路由并与后端人员协调)。

  • 将对 LaunchDarkly 的使用转移到服务器端,这样我们可以加载更少的 JS,从而降低整体成本。

  • 通过使用后端路由代理 Sentry 请求,我们可以捕捉到由于广告屏蔽器而未被报告的错误。

而这仅仅是冰山一角。这种模式真的有很多很酷的地方,其中最大的一点是它重振了渐进式增强的理念。借助服务器和客户端的组合特性,能够让客户端在用户禁用 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 站点普遍会更好一些,用户体验会普遍更顺畅一些。这不会修复所有网站,很少有代码可以解决糟糕的用户体验设计所带来的问题。但它会奠定一个共同的基础,所以每个网站在开始时都能更好一些,每位开发人员都有更多的时间专注于其他事情。