相比其他语言,我觉得 JavaScript 的生态系统太奇怪了,实际运行的代码已经不再是开发者编写的 JavaScript,而是各种编译工具最终生成的产物。这个问题有一定的历史原因,也是因为 JavaScript 社区对各种 “滥用” JavaScript 的包容性造成的,新的 React Server Components 进一步推动了这种情况,下面轻听我细细道来。
历史背景
10 年前,JavaScript 还是非常糟糕的。它没有 Import、Class、async、箭头函数、模板字面量、解构赋值、默认参数等能力。而且它主要运行的环境只有浏览器的 DOM。JQuery 的出现让它变得稍微好了一些,但仍然很弱,但在那个时候,相对来说也还算是合理的。
将 JS 打包在浏览器中运行是它开始奇怪的第一个迹象。在这个过程中,我们还要同时缩减和压缩源代码,甚至可能还要进行代码拆分。一般来说,这个过程会同时读取多个 JavaScript 源代码作为输入,然后生成一个或多个 JavaScript 文件作为输出。这就意味着我们执行的代码并不是我们自己编写的代码,还需要借助 Sourcemap 才能还原。
然后,CoffeeScript 出现了。我们可以使用一种可以编译成 JavaScript 的语言来编写代码,而不是直接编写 JavaScript。这和 Elixir、Kotlin 这样的语言不太一样,后者会编译成与另一种语言相同的字节码,而 CoffeeScript 实际上是编译成另一种语言。C++ 最初也是这样开始的。
然后在 2015 年出现了 ECMAScript 6,JavaScript 在接下来的几年里迅速改进,这也促使 CoffeeScript 被淘汰了,但又产生了一些新的问题:短时间内,浏览器的兼容性跟不上,所以像 Babel 这样的转译器应运而生,将当前和未来版本的 JavaScript 编译成可以在支持的环境中运行的较旧版本的 JavaScript。当前,esbuild 作为新一代 JavaScript 打包器/转译器正在迅速崛起。
一路走来,emscripten 又出现了,它使用了 LLVM 编译器框架来将 C/C++ 代码转换成中间代码,然后使用 Emscripten 工具链将中间代码转换成 JavaScript 代码。简单来讲,它将实际的机器代码编译成 JavaScript 的一个子集,尽管现在这个工具的新目标通常是 Wasm。
最近,JavaScript 生态的创新速度似乎放缓了,JavaScript 的实现也做得越来越好,所以我们会认为对于在服务器端没有打包器的情况来说,转译器的需求会逐渐减少。但实际情况并非如此,原因是一个有趣的故事。
没人再写原生的 JavaScript
上面这个标题显然是有点夸张了,但是现在确实开始有这个趋势了。如果你在编写 Rails 应用程序,则可以使用 Ruby 编写。如果你在编写 Django 应用程序,则可以使用 Python 编写。Phoenix,Elixir,Lavavel,则使用 PHP。Rails 因为使用了元编程进行了很多魔幻操作而受到了很多批评,而 Elixir 具有宏,但是所有上述的内容都是在语言可以完成的范围内的。
但是,JavaScript 就不一样了。虽然它的标准实际上由 EMCA TC39 标准化制定,但如果你使用的是像 Next.JS、Remix 或 Svelte 这样的流行框架,你的编码标准可能就不是 ECMA TC39 标准化的 ECMAScript 了。下面是一些例子:
1.曾几何时,将近20年前,ECMA 委员会标准化了 E4X,使 XML 能够被视为一种数据类型。随后,它又失去了支持,被弃用并存档。多年后,Facebook 有了类似的需求,然后发明了 JSX。它与 E4X 的不同之处在于,它会编译成 JS。
2.ECMA TC39 没有成功的标准化过类型注释,但是微软使用 TypeScript 实现了,它也会最终编译成 JS。
3.Svelte 有自己的编译器,甚至故意误用 JavaScript 标签语法来把语句标记为响应式。
4.现实还不止于此,当打包器/转译器遇到 import 语句时,它们不一定认为被导入的文件是 JavaScript,甚至不是上述任何变体之一。如果配置正确并且您想导入 CSS 或 PNG 文件,它会很乐意为你完成。
我之前提到过 Rails 因为使用元编程而受到很多批评。但是大多数人对上述 JavaScript 语言的任何 “滥用” 都视而不见。JavaScript 生态系统就像是一个大帐篷派对。
“使用服务器”
最新打包器的滥用是 React Server Components(RSC)。这种方式首先在 express 上演示过了,现在又被 Next.js 采用了。
“use server” 和 “use client” 除了是一段有效的 JavaScript 语句,其他的啥也不做,它们改变了接下来代码的含义。这个行为现在褒贬不一,但是在我看来,这非常符合 “use strict” 的精神,后者也改变了接下来代码的含义。
虽然 JSX 通常会编译成 JS,但 Server React DOM API 支持编译成 HTML 。RSC 采用了不同的方式,并编译成了标记的 JSON 流。这对我们来说都是非常透明的,但它确实启用了一种不同的编程风格。许多人将其与 PHP 甚至 Rails 进行比较:
我不确定这些比较是不是意味着更积极的方式,但我会说从我的角度来看,这是一件非常好的事情。
从 fly.io 的角度来看,RSC 实现的更新(重新获取)序列是非常有趣的。我们一直特别青睐那些受地理分布影响的框架,例如 Elixir 的 LiveView、Laravel 的 Livewire 和 Ruby on Rail 的 Hotwire。我们希望这些框架能够成功,因为它们越成功,我们就越有价值。现在我们可以将 React 的 RSC 添加到这个列表中了。
回到手头上的话题,令人印象深刻的是,这样的功能只有通过与打包器的合作才能实现,这相当于对 JavaScript 语言本身进行了更改,意义深远,而且我敢说,也是令人愉快。
另一个维度
Dan Abramov 在 RemixConf 上发表了题为 React from Another Dimension 的演讲:
在 Dan 的演讲中,他想象了一个替代宇宙,其中 React 于 90 年代末首次在服务器上实现,并且仍然设法收敛到今天的状态。在演讲期间,他启动了一个 Windows 95 模拟器并使用 React 运行 Internet Explorer(特别是 IE6)。他甚至设法在使用该操作系统和浏览器组合的情况下完成了十个步骤中的九个。
但是这个演讲中令人费解的部分是他首先利用 use server 来实现客户端表单操作,然后才使用 use client.他最后说,这需要新一代路由器和新一代打包器。
想想所有这一切之所以成为可能,是因为我们编写的 JavaScript 不仅不是我们运行的 JavaScript,而且在仔细检查下甚至根本不是 JavaScript。
对此你怎么看?