前言
早在 2012 年,我就开始主要使用 JavaScript 进行编码。我从头开始为本地企业构建了一个 PHP 应用程序,一个基本的 CMS 和网站。后来,他们决定重写它并添加一堆功能。该项目的经理希望我使用 .NET,部分原因是他这是他了解的东西,但与此同时他希望重构后的应用要像一个原生客户端应用那样 - 没有整个页面刷新或操作之间的长时间卡顿。经过一些研究和原型设计,我说服他,我们可以用 web 技术实现同样的目标 - 使用当时刚出现的全新 JS 框架之一。
我选择的第一个框架实际上是 Angular 1。在遇到社区路由器的一些问题之前,我使用 FuelPHP 后端构建了相当大的应用程序模块 。当时遇到的问题是:每当您重新渲染子路由/出口时它都会闪烁。实际上这种情况只是当时在设计的时候并未考虑到该用例。有人向我推荐了 Ruby on Rails + Ember,在试一试之后,我认为它运行良好。我喜欢这两个框架的理念,喜欢它们的社区,总体而言,与当时的替代方案相比,它非常富有成效。
从那以后发生了很多新情况——某个框架出现了,然后又消失了,某天你关注它的时候,你发现它已经大规模地应用了。最终你发现,你可以在浏览器中使用 JavaScript 构建应用程序的想法从某种边缘变为标准实践。我们构建应用所依赖的基础设施已经完全改变,带来了许多新的可能性。
在那个时候,想法之间也存在相当多的竞争和冲突。我认为我们中的大多数人已经是前端老兵了,可能也看过了一些针对某些观点的争论......好吧,当时,一切都是有争论的。使用哪个 JavaScript 框架,如何编写 CSS,函数式编程与面向对象编程,如何最好地管理状态,哪个构建系统或工具最灵活和最快,等等。我们经常为错误的事情争论并错过更大的模式,现在回想起来,感觉自己也挺搞笑的,但这当然是事后诸葛亮了。
所以我想做一个回顾,回顾过去几十年的 JavaScript 开发以及我们已经走了多远。我认为我们可以大致将其分为四个主要时代:
- 远古时代(The Before Times)
- 框架时代(The First Frameworks)
- 「以组件为中心」的视图层时代(Component-Centric View Layers)
- 全栈框架时代(Full-stack Frameworks)(←我们在这里)
每个时代都有自己的主题和主要矛盾点,在每一个时代,我们都作为一个社区整体地吸取了重要的教训,并缓慢而坚定地前进。
今天争论的焦点是:web 是否变得过于臃肿?一般的网站真的需要用 React 编写吗?甚至说,我们应该使用 JavaScript 吗?我认为我们现在是无法看到未来的。最后我怀疑,未来我们回头看的时候,我们会不会再次发现,因为我们太过于聚焦当下而错过了更大的图景。前瞻也许永远无法完美的,但从过去获得一些洞察也许将有助于我们继续前行。
远古时代
JavaScript 于 1995 年首次发布。就像我上面提到的,我在 2012 年开始编写 JS,差不多是 20 年后,也就是我称之为 「First Frameworks」 时代的开始。正如你可以想象的那样,我可能会在这里忽略很多历史,同时,这个时代也可以会被分解成许多子时代,每个时代都由自己的模式、库和构建工具等来主导。
也就是说,我不能写我没有经历过的事情。当我开始编写前端应用程序时,新一代的框架刚刚开始成熟:Angular.js、Ember.js、Backbone 等等。
在此之前,最先进的库是 jQuery 和 MooTools 等库。这些库在当时非常重要——它们有助于消除浏览器实现 JavaScript 的方式之间的差异,这非常重要。例如,Internet Explorer 实现事件的方式与 Netscape 完全不同——「冒泡事件 VS 捕获事件」。这就是为什么我们今天最终实现了这两个标准的原因,但在此之前,您需要使用库来编写可以在两种浏览器中运行的代码。这些库主要用于制作小型、独立的 UI 小部件。大多数应用程序业务逻辑仍然通过表单和标准 HTTP 请求进行 - 在服务器上渲染 HTML 并将其提供给客户端。
在这个时代也没有多少构建工具可言,至少我知道。 JavaScript 还没有「模块」的概念(至少没有「标准模块规范」这个概念),所以没有任何方法可以“模块的方式”去导入外部代码。一切都是全局的,组织起来非常困难。
在这种环境下,可以理解 JS 通常被视为一种玩具语言(toy language),而不是你用来编写完整应用程序的东西。你会做的最常见的事情是在页面上引入 jQuery 脚本,为一些 HTML 文档上的局部 UI 写一些脚本,一天过去了,净是搞这些小玩意(and call it a day)。随着时间的推移和 XHR(XMLHttpRequest) 的引入和普及,人们开始将部分 UI 流程放在一个页面中,特别是对于需要在客户端和服务器之间进行多次来回交互的复杂流程,但大多数应用程序仍然采用传统的服务端渲染。
这与移动应用程序开始出现时形成鲜明对比。从一开始,iOS 和 Android 上的移动应用程序就是用 Objective C 和 Java 等 Serious Languages™ 编写的完整应用程序。此外,它们完全由 API 驱动——所有 UI 逻辑都存在于本地设备上,与服务器的通信纯粹以数据格式进行。这导致了更好的用户体验和移动应用程序的流行,直接推动我们到达了今天所处的位置 - 关于哪个更好,原生客户端技术还是 web 技术更好的争论。
起初,用 JavaScript 做所有这些事情被认为是可笑的。但随着时间的推移,应用程序开始变得更加雄心勃勃。社交网络增加了聊天和 DM 以及其他实时功能,Gmail 和 Google Docs 表明可以在浏览器中编写与桌面具有等效体验的应用,越来越多的公司开始转向为越来越多的用例编写 Web 应用程序,因为 web 无处不在,并且随着时间的推移更容易维护(译者注:Are you serious? 确实不是在讲反话?)。这将 JS 的能力边界向前推进了(This pushed the envelope forward)——现在很明显, JS 可以用来编写不一般的应用程序。
然而,在当时这么做还是很困难的。 当时的 JavaScript 没有今天的所有特性——就像我说的,一切都是全局的,你通常必须手动下载每个外部库并将其添加到静态资源文件夹中,然后在 html 页面中引用它。 NPM 还不存在,模块还没成型,JS 还没有今天一半的能力。在大多数情况下,每个应用程序都是定制的,每个页面上都有不同的插件设置,每个插件中用于管理状态和呈现更新的不同系统。为了解决这些问题,第一个 JavaScript 框架开始出现。
框架时代
大约在 2000 年代末和 2010 年初,第一个专门为编写完整的客户端应用程序而设计的 JS 框架开始问世。这个时代的一些值得注意的框架是:
当然,还有很多其他的,并且可能在某些技术圈子中甚至更出名。这些是我记得的,主要是因为我用它们来制作原型过或构建过东西,而且它们当时是比较流行的。
这都是首次进入未知领域的第一代前端框架。一方面,他们试图做的事情非常雄心勃勃,很多人并不看好它们。有许多批评者认为单页 JS 应用程序 (SPA) 从根本上说更糟糕,而且在很多技术观点方面他们是正确的 - 1)客户端渲染意味着爬虫引擎无法轻松抓取这些页面;2)用户将不得不等待应用程序甚至开始绘制几秒钟;3)很多这些应用程序的可访问性都是噩梦,如果你禁用了JavaScript,它们将根本无法运行。
另一方面,我们没有集体用 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 等构建工具则就如何将这些模块打包到一块,组合成可交付的最终产品展开竞争。在大多数情况下,这些都是非常通用的类似任务运行器的工具(task-runner-like tools),它可以真正构建任何东西,只是碰巧构建 JavaScript - 和 HTML,以及 CSS/SASS/LESS,以及进入 Web 应用程序的许多其他东西。
然而,我们从这个时代学到了很多东西。重要的基本经验教训,包括:
-
基于 URL 的路由是前端应用的基础。没有它的应用程序会破坏网络,需要从一开始就在框架中考虑它。
-
扩展 HTML 的模板语言是一个强大的抽象层。即使它有时可能有点笨拙,它也让你的 UI 与你的状态保持同步变得更加容易。
-
SPA 的性能是难点,而且 Web 有很多原生应用程序没有的额外限制。我们需要通过网络传输所有代码,使其 JIT,然后运行以启动我们的应用程序,而原生应用程序已经下载并编译。所以说,SPA 的性能优化一项艰巨的任务。
-
JavaScript 作为一门语言有很多问题,它确实需要好好改进——框架层单靠自己无法改善这一点。
-
为了大规模编写应用程序,我们绝对需要更好的构建工具、模块和打包流程。
总的来说,这个时代是硕果累累的。尽管存在这些缺点,但随着应用程序复杂性的增加,通过 API 将客户端与服务端分离的好处是巨大的,而且在许多情况下,由此产生的用户体验是惊人的。如果当时没有后面的变故的话,这个时代可能会继续下去,直到今天我们仍在迭代 MV* 风格的想法。
但随后一颗小行星不知从何而来,打破了现有的范式,并引发了一场小型灭绝事件,将我们推向了下一个时代——这颗小型星的名字叫 「React」 。
以组件为中心的视图层时代
我不认为 React 发明了组件,但老实说,我不太确定它们最初是从哪里来的。我知道 .NET 中的现有技术至少可以追溯到 XAML,并且 Web 组件也开始作为规范开发。最终这并不重要——一旦这个想法出现,每个主要框架都很快采用了它。
事后看来,这是完全合理的——扩展 HTML,减少 long-lived state,将 JS 业务逻辑直接绑定到模板(无论是 JSX 还是 Handlebars 或 Directives)。基于组件的应用程序消除了完成工作所必需的大部分抽象,并且还显著地简化了代码的生命周期——一切都与组件的生命周期相关联,而不是应用程序,这意味作为一个开发者,你可以考虑得更少。
然而,当时还有另一个转变:框架开始将自己吹捧为“视图层”,而不是成熟的框架。他们不会解决前端应用程序所需的所有问题,而是专注于解决渲染问题。其他问题,如路由、API 通信和状态管理,则由用户自己去寻找第三方解决方案。这个时代值得注意的框架包括:
还有很多很多其他的框架/类库。现在回想起来,我认为这是框架时代的流行框架,因为它确实做了两件主要的事情:
-
它大大缩小了范围。该框架的核心不是试图预先解决所有这些问题,而是专注于渲染,并且可以在更广泛的生态系统中探索许多不同的想法和方向以实现其他功能。在社区中,有很多糟糕的解决方案,但也有好的解决方案,为下一代从诸多方案中挑选最好的方案铺平了道路。(译者注:广开言路,激发创新)
-
它使采用它们变得更加容易。采用一个完整的框架来接管你的整个网页几乎意味着重写你的大部分应用程序,这对于现有的胖服务器端来说是一个非首选。使用 React 和 Vue 之类的框架,您可以一次将其中的一些小部件或组件放入现有的应用程序中,从而允许开发人员逐步迁移他们现有的代码。(译者注:渐进式的应用策略)
这两个因素导致第二代框架迅速增长并超过了第一代框架,事后看来,这一切似乎都非常有意义,并且是一个合乎逻辑的演变。但在当时,置身其中是一种非常令人沮丧的经历。
一方面,这不是在讨论在具体的框架本身在工作中遇到的问题,或者我们是否应该重写我们的应用程序。相反,它通常是在争论“某某框架更快!”或“某某框架更小!”或“某某框架就是你所需要的!”。还有关于函数式编程与面向对象编程的争论,很多人都在推动 FP 作为我们所有问题的解决方案。公平地说,所有这些事情都是真实的:仅视图层的框架更小(起初)和更快(起初)以及你所需要的一切(如果你自己构建或组合了很多东西)。毫无疑问,函数式编程模式解决了困扰 JavaScript 的大量问题,我认为平均而言,JS 也因此而变得更好。
然而,现实是没有银弹的——从来没有。应用程序仍然变得庞大、臃肿和复杂,状态仍然难以管理,路由和 SSR 等基本问题仍然需要解决。对于我们中的很多人来说,似乎人们想要的是放弃试图解决所有这些问题的解决方案,而把这个练习留给读者。根据我的经验,这在工程组织中也普遍存在,他们很乐意接受这种变化以发布新产品或功能,然后无法为充分开发所有这些额外功能所需的时间提供资金。
结果(根据我的经验,通常情况下)是发明了很多围绕这些视图层构建的组合型框架,这些视图层本身臃肿、复杂且非常难以使用。我认为人们在使用 SPA 时遇到的许多问题都来自这个支离破碎的生态系统,它恰逢 SPA 使用量激增的时候。我仍然经常遇到一个新站点无法正确路由或处理其他小细节,这绝对是令人沮丧的。
但另一方面,现有的全服务第一代框架在解决这些问题方面也做得不太好。部分原因在于大量的技术债务包袱。第一代框架是在 ES6 之前构建的,在模块之前,在 Babel 和 Webpack 之前,在我们想出这么多东西之前。迭代地进化它们是极其困难的(根据我作为前 Ember 核心团队成员的经验,我非常清楚这一点),像 Angular 对 Angular 2 所做的那样去完全重写它们,这将会扼杀了社区的大量跟随动力。因此,当谈到 JavaScript 框架时,开发人员处于两难境地 - 要么选择一个老一代的一体化解决方案,要么选择自己组合 DIY 出一个框架。
就像我说的那样,当时这非常令人沮丧,不过最终还是产生了很多创新。随着这些框架找出最佳实践,JavaScript 生态系统发展得非常迅速,并且发生了其他一些关键变化:
-
像 Babel 这样的编译器成为了规范,并帮助实现了语言的现代化。不必等待漫长的 API 标准化过程,我们就可以在今天使用这些特性,并且语言本身开始以更快和更频繁的迭代速度去添加功能。
-
ES 模块被标准化,让我们最终开始围绕它们创造现代构建工具,如 Rollup、Webpack 和 Parcel。基于导入的打包范式慢慢成为常态,即使对于 CSS 样式和图像等非 JS 资产也是如此,这极大地简化了构建工具的配置,并使它们变得更精简、更快、整体更好。
-
随着越来越多的 API 标准化,Node 和 Web 标准之间的差距逐渐缩小,但肯定会逐渐缩小。 SSR 开始成为一种真正的可能性,然后是每个严肃的应用程序都在做的事情,但每次它仍然是一个有点定制的设置。
-
边缘计算已发布,基于 JavaScript 的服务器应用程序在分发/响应时间方面具有 SPA 的优势(由于是存储在 CDN 上是静态文件,SPA 通常比以前可以更快地开始加载,即使它们需要更长的时间才能完全加载和渲染结束)。
到这个时代结束时,仍然存在一些问题。状态管理和反应性曾经(现在)仍然是棘手的问题,尽管我们的模式比以前好得多。性能仍然是一个难题,尽管情况有所改善,但仍然存在许多臃肿的 SPA。可访问性情况有所改善,但对于许多工程组织来说,它仍然是(现在仍然是)事后才想到的。但是这些变化为下一代框架铺平了道路。是的,我们才刚刚迈入这个时代。
全栈框架时代
就我个人而言,全栈框架时代真的悄悄地吸引了我。我认为这是因为我在过去 4 年左右的时间里一直在深入 Ember 渲染层的内部,试图清理上述技术债务(这些技术债务给它贴上了「第一代框架」的标签)。但这也是因为它更加微妙,因为所有这些第三代框架都是围绕上一代的视图层框架构建的。值得注意的框架包括:
- Next.js(视图层:React)
- Nuxt.js (视图层:Vue)
- Remix(视图层:React)
- SvelteKit(视图层:Svelte)
- Gatsby(视图层:React)
- Astro(视图层:任意)
这些框架随着视图层的成熟和固化而启动。既然我们都同意组件是一切上层建筑的基础单元,那么开始标准化应用程序的其他部分是有意义的——路由器、构建系统、文件夹结构等。这些元框架进展缓慢但坚定地开始构建开箱即用的,与第一代一体式解决方案相同的功能。它们都是从各自的生态系统中挑选最佳的模式,等待它们成熟之后整合它们进来,然后在此基础上更进一步地优化它。
到目前为止,SPA 一直专注于客户。 SSR 是每个框架都渴望解决的问题,但只是作为一种优化,因为 SSR 渲染的得到的页面最终还是被加载完毕的 JS 所接管。只有一个第一代框架敢于想象,Meteor.js,但它的“同构 JS”的想法从未真正起飞(译者注:Meteor.js 还不算 JS 同构?不知道作者此说法的依据是什么)。
译者注:我很早就接触了 Meteor.js, 也很早就知道 Meteor.js 的技术理念太先进了,步子迈得太大,前端业界肯定无法普遍接受。如今看来,最新这代框架要到达的目的地跟 Meteor.js 想要到达的目的地是一样的。
但随着应用程序规模和复杂性的增长,这个想法被重新审视。我们注意到,将后端和前端配对在一起实际上非常有用,这样您就可以执行诸如隐藏某些请求的 API 密钥、在返回页面时修改 HTTP 标头、代理 API 请求等操作。随着 Node 和 Deno 实现了越来越多的 Web 标准,随着服务器端 JS 和客户端 JS 之间的差距逐年缩小,这似乎并不是一个疯狂的想法。将其与边缘计算和令人惊叹的工具相结合,您将拥有令人难以置信的能力。
最新一代的框架充分利用了这种能力,将客户端和服务器无缝地融合在一起,这种感觉有多神奇,我怎么强调都不为过。在与 SvelteKit 合作的过去 9 个月中,我无数次感慨地坐下来,对自己说:“这就应该是我们一直坚持的方向。”
以下是我最近完成的一些任务,通过这种开箱即用的特性框架变得非常容易使用:
-
将服务器端 OAuth 添加到我们的应用程序,以便身份验证 token 永远不会离开服务器,以及一个 API 代理,该代理在向我们的 API 发送请求时添加 token。
-
将某些路由直接代理到我们的 CDN,以便我们可以托管通过任何其他框架中构建的静态 HTML 页面,允许用户制作自己的自定义页面(我们为某些客户提供的服务)。
-
当我们需要使用需要密钥的外部服务时,添加几个不同的一次性 API 路由(无需向我们的 API 添加全新的路由并与后端人员协调)。
-
逐步移除我们对 LaunchDarkly 服务器端的使用,以便我们可以加载更少的 JS 并降低总体成本。
-
通过后端路由代理我们的 Sentry 请求,这样我们就可以捕获由于广告拦截器而无法报告的错误。
而这只是冰山一角。这个模型真的有很多很酷的东西,其中最大的一个是它如何重振「渐进增强」的想法,利用服务器和客户端的组合特性,允许客户端在用户禁用 JavaScript 的情况下回退到基本的 HTML + HTTP。当我开始研究 SPA 时,我已经给自己灌输了 SPA 是未来的想法,我自己已经完全放弃了「[渐进增强]。如果我们可以看到一个「渐进增强」卷土重来的世界,那将会很酷。
这些让我感受最深刻的新特性,让我将这些框架归类为新一代。以前难以解决或无法解决的问题现在变得微不足道,只需更改一点 HTTP 响应处理逻辑即可。可靠的性能和 UX 开箱即用,无需任何额外配置。我们可以根据需要添加一些额外的 endpoint 或中间件,而不是建立全新的服务。开发者的生活已然发生了翻天覆地的变化。
我认为这一代也解决了第一代和第二代框架及其用户之间的一些主要关注点。它开始向“零配置/开箱即用”靠拢,但我最终还是认为它是由围绕第二代框架成熟和稳定的生态系统所驱动的,这是一种文化传承和转变。第三代框架现在正试图再次成为一体式解决方案,试图解决我们作为前端开发人员需要解决的所有基本问题——而不仅仅是渲染。
现在感觉社区比以往任何时候都更加一致地解决困扰 SPA 的所有许多问题,更重要的是,共同解决这些问题。
下一步,该去往何方?
总的来说,我认为 JavaScript 社区正朝着正确的方向前进。我们最终正在开发一套成熟的,完整的解决方案,一套可以从头开始构建完整的应用程序的方案,而不是“只是一个视图层”的解决方案。我们终于开始在与原生应用程序 SDK 相同的环境中去竞争:提供开箱即用的完整工具包。我们在这里还有很多工作要做。可访问性长期以来一直是 SPA 领域的事后考虑,在 GraphQL 之外,我仍然认为关于数据交互方面的工作还需要花点心思去琢磨(无论喜欢与否,大部分网络仍然在 REST 上运行)。但趋势是朝着正确的方向前进,如果我们继续朝着共享解决方案的模式前进的话,我认为我们可以用比以往更好的方式去解决这些问题。
我仍然对将这些模式进一步提升到 web 平台本身的潜力而感到兴奋。 Web componnet 仍在悄悄迭代,致力于解决 SSR 等问题并摆脱全局注册,这将使它们与这些第三代框架更加兼容。另一方面,WebAssembly 可以以一种令人难以置信的方式迭代这个模型。想象一下能够用任何语言编写一个全栈框架。同构 Rust、Python、Swift、Java 等最终可以将前端和后端之间的障碍减少到几乎为零 - 只需在系统边角添加一些 HTML 模板(具有讽刺意味的是,这给我们带来了几乎完整的循环,尽管也带来了更好的用户体验)。
我们正在超越碎片化时代 - 一个每天都有新的 JS 框架的时代,这让我对未来充满希望。自由和灵活性孕育了创新,但它们也导致了混乱、脱节且经常从根本上破坏的 web 技术给人带来的体验。当开发人员不得不在 50 多个方案中做出选择并自己组合起来时,在资源有限和期限紧迫的情况下,这是合理的,但这也导致了 web 应用产品质量的参差不齐:一些应用程序非常快速、用户体验一致、可靠且使用起来很有趣,而另一些应用程序则是加载缓慢,体验感是支零破碎,令人沮丧的。
如果我们可以让开发人员一上手就默认使用能做正确事情的工具,那么普通网站也可能会变得更好,平均的用户体验会更流畅一些。它不会使得每个站点变得完美——没有什么代码可以解决糟糕的用户体验设计。但这会在技术侧打下一个共同的基础,所以每个网站的开始都会好一点,与此同时每个开发人员都有更多的时间专注于其他事情。