• Node.js、Deno与将要发布1.0的Bun
  • 发布于 2个月前
  • 196 热度
    0 评论
  • 离人愁
  • 0 粉丝 23 篇博客
  •   
JS三剑客
Node.js
JavaScript最初的设计仅在浏览器内运行,由SpiderMonkey和Google的V8等引擎提供支持。2009年,Ryan Dahl将V8引擎嵌入到一个C++程序中,称之为Node.js,旨在提供基于事件驱动的非阻塞I/O模型的服务器端运行环境。

Node.js在过去的十余年里取得了巨大的成功,成为了构建高性能网络应用程序的首选工具之一。它以一己之力改变了前端的游戏规则,现在几乎所有新出的前端框架无不是基于Webpack、Vite、Rollup等工具构建,而这些工具又都是用Node.js开发的,更不用说与之配套的工具如Eslint、Babel等等了。

天不生Node,前端万古如长夜。

在十年前,或许你只需要一个文本编辑器就可以开发HTML页面,而现在则没有哪个前端程序员会不安装Node.js,它已经成为业内标配了。Node.js促进了前端领域的蓬勃发展,解放了前端人员的生产力,从而也让前端卷出了天际。

Deno
由于JavaScript语言发展的滞后(ES4的难产,有兴趣的可以看看这篇《前端简史》),Node.js在设计之初并没有模块化的概念,只能自起炉灶,采用了当时社区刚提出的CommonJS规范。而随着2015年ES6模块化(ESM)的正式推出,仍在坚持CommonJS的Node.js陷入一个尴尬的境地,只能渐进式地支持ESM,先是要求文件名后缀为.mjs,后来的版本才逐渐加入package.json的type属性,但对旧的npm包的转换、支持,两种截然不同的规范,无不大大增加了开发者的学习成本和心智负担。

2012年,Node.js之父Ryan认为Node.js已经达到了他预期的目标,就离开了如日中天的Node.js团队。时间到了2018年,Ryan发表了一场演讲,讲述了他对Node.js感到遗憾的事情:
1.没有坚持使用Promise。2009年加入到Node.js中,不到一年又去掉了……
2.安全问题。V8本身是个非常好的安全沙箱,如果当初在某些模块上点儿心,Node.js会比其它语言更有安全保障。
3.使用老旧的GYP构建系统。
4.引入package.json,导致出现一个中心化的甚至私有的模块管理仓库(npm平台)。
5.node_modules极大提高了模块解析算法的复杂度。
6.舍弃了文件扩展名。指的是require("./main")不需要添加扩展名,与ESM规范有出入,比如浏览器中是要求必须有.js后缀的(准确说是不会降级修改路径去查找.js后缀的文件)。
7.用index.js实现模块解析,增加了模块加载的复杂度。
8.他的遗憾激发了他创建Deno[1]的灵感。

Deno一开始是用Go编写的,不过,由于担心重复运行时间和垃圾收集压力,又切换成了以内存安全著称、没有GC回收的Rust,并于2020年发布了1.0版本。

Deno旨在解决Node.js的所有缺陷,例如安全性、性能、TypeScript支持、原生ESM支持(去中心化)、顶级await、浏览器API(比如可以在服务端直接使用fetch、FormData、localStorage、WebSocket等API)、内置的全家桶工具(代码校验、格式化、打包、编译、测试等)。

看得出来,Deno在加强安全性的同时,一直在试图抹平服务器与浏览器之间的差异,提升开发体验。以前端使用度非常高的Axios为例,它之所以能有如此高的流行度,除了其对AJAX出色的封装外,还用Node.js的API实现了同样的功能,让开发者可以在前后端无缝切换,对SSR的框架而言更是一种利好。换作Deno开发,则不需要这么复杂,直接用fetch就包打天下了。

但Deno的缺点也同样明显,拥抱了浏览器的生态,则无可避免地要放弃Node.js积累了十余年的npm生态圈。Deno或许指望npm包作者都能参与到Deno建设中(对于大部分包理论上切换并不复杂),但毫无疑问,它失败了。

痛定思痛,在2022年,Deno加紧了对Node.js API的实现和对npm包的原生支持。这样又带来一个新的后果,它不得不着手解决Node.js复杂的包管理(遗留的大量CommonJS包转换、npm hooks等),体积逐渐变的臃肿,从初出茅庐、轻装上阵的年轻人,变成被现实毒打、棱角磨平的中年男。

Deno对Node.js API的支持已接近尾声,官方团队还在紧锣密鼓地修复Bug,相信距离2.0的发布为期不远了。

Bun
在Deno被现实毒打的同时,破屋又逢连夜雨,更糟心的事情来了——2022年7月,一个新的竞争对手Bun[2]横空出世了。

Deno从名字上看,它是Node这4个字母的重新整理,意为Detroy Node。所以它的目标一开始并不是成为Node.js的替代品,而是一种替代方案。即使是现在对npm的支持(比如已经可以运行一个Vite项目),也需要开发者对工程做一些改造。

而Bun不一样,它的目的性很强,专注于成为Node.js的更快的直接替代品。精华我吸收,糟粕我也来者不拒,这态度可比Deno务实多了。

于是,JS运行时抢蛋糕的玩家又多一个,而且是个实力强劲的搅局者。

Bun由Jarred Sumner创建,使用Zig[3]作为其主要开发语言。后者视C语言为竞争对手,于2016年发布,比起已经算年轻的Rust要更年轻10岁,这也是Bun的一大致命缺点,Zig年轻到太小众了(自己1.0都没发,目前版本是0.11.0),很难找到具有强大Zig技能的贡献者和开发人员,也没有成熟的产品(Bun应该算是目前其知名度最高的作品了)、出色的战绩,其生态、安全性都让人深深怀疑,所以能否经得住考验,还是个问号。好的一点是据说Zig之父也知道Bun是它的一个标杆项目,所以会随时支持(忘了哪篇文章看到了)。

Bun被社区称为Bun.js,其实并不准确,它与Deno一样,都将TypeScript内置为一等公民,所以正式称呼应该是Bun(其Logo是翻译过来的包子,就像Deno的Logo是恐龙一样)。

Deno与Node.js的JS引擎是Chrome的V8,背靠谷歌,而Bun将注压到苹果的JavaScriptCore引擎身上,大大减少了启动时间和内存使用量。而这两大引擎到底孰优孰劣,就留给时间验证吧。以下是官方的首页宣传,性能上是吊打Node.js与Deno的(Deno维护者认为单基准的指标对比没有意义,详见GitHub这个issue《Why is Deno that slow?[4]》,也可以看看我2022年7月的文章《Deno的一些现状[5]》):

再看Bun官方文档中宣传的优势:
1.速度。目前Bun进程的启动速度比Node.js快4倍。
2.TypeScript和JSX支持。可以直接执行.jsx、.ts和.tsx文件;Bun的transpiler在执行之前将这些转换为普通的JavaScript(它与Deno一样,本质上并不是TS的运行时,都是将TS编译为JS,再用浏览器引擎执行的,毕竟直接写个TS运行时的成本太高了)。
3。ESM和CommonJS兼容性。ESM已经是主流发展方向,但是数以百万计的npm包仍然需要CommonJS。这个选择,其实就冲着直接替代Node.js去的,不像Deno那样碰壁之后才回头支持npm。

4.Web标准API。Bun实现了标准的Web API,比如fetch、WebSocket和ReadableStream等。


Bun不仅仅是一个运行时。长期目标是成为一个有凝聚力的基础设施工具包,用于使用JavaScript/TypeScript构建应用程序,包括包管理器(替代npm)、转译器、捆绑器(替代)、脚本运行器、测试运行器等。

Bun被设计为Node.js的更快、更精简、更现代的替代品。Bun本身实现了数百个Node.js和Web API,包括fs、path、Buffer等等。Bun的目标是运行大多数世界上的服务器端JavaScript,并提供工具来提高性能,减少复杂性和提高开发者的生产力。

看出它与Deno的区别了吗?

像TS、JSX、Web API、ESM、Bundle之类Deno也都应有尽有,而Deno激进地将package.json抛弃了,在两年后又不得不添加了deno.json作为补充,再后来又不得不支持了package.json的部分功能(Deno1.31开始支持,目前只支持scripts和依赖部分,未来或许会完全支持?);

Deno去除了node_modules,与Node.js进行切割,而Bun则保留了Node.js的一切,除了多了几个额外的文件外,你几乎看不出来工程与Node.js的差别(当然,与Deno一样,作为服务器时没有package.json也可以,以下只是个Bun支持的Nextjs模板工程,不代表可以完全运行原始的Node.js项目):

而Deno支持创建的Vite工程,一眼仍能看出风格的:

Bun为了支持Node.js生态,实现了自己的npm包的管理,安装速度比较可观,居然能辗压pnpm(我用某个后端项目验证过,确实快,缺点是不能读取.npmrc[6],需要自己配置bunfig.toml,对私有仓库不是很友好):

Deno要求引入路径必须带后缀(这与浏览器的实现是一致的,不进行复杂的规则查找):
import { hello } from "./hello.ts";
而Bun除了支持以上格式外,面对这样的引入:
import { hello } from "./hello";
则会按照以下规则来查找(比Node.js的还要复杂,因为多了TS部分):
./hello.ts
./hello.tsx
./hello.js
./hello.mjs
./hello.cjs
./hello/index.ts
./hello/index.js
./hello/index.json
./hello/index.mjs
同时也不得不实现了对以下各个规则的解析(Deno也在面临这个考验,虽然底层实现起来可能没有什么技术难度,就是恶心):
{
  "name": "foo",
  "exports": {
    "bun": "./index.js",
    "worker": "./index.js",
    "node": "./index.js",
    "require": "./index.js", # if importer is CommonJS
    "import": "./index.mjs", # if importer is ES module
    "default": "./index.js",
  }
}
Bun还自己实现了打包器,野心比Deno大太多了,最起码是要替代掉esbuild了:

Deno虽然也有bundle功能(很不好用,一直有Bug,应该是SWC的锅),但现在已经放弃了,让有需要的同学自己选择其它工具(不过我认为没有必要,除非SWC的bundling[7]功能不准备往前演进了):
$ deno bundle mod.ts > aa.js
Warning "deno bundle" is deprecated and will be removed in the future.
Use alternative bundlers like "deno_emit", "esbuild" or "rollup" instead.
再看看Bun内置的这些模板,这是要砸Webpack这些工具的饭碗啊:
$ bun create
Welcome to bun! Create a new project by pasting any of the following:
  bun create apollo-server ./apollo-server-app
  bun create bun-bakery ./bun-bakery-app
  bun create discord-interactions ./discord-interactions-app
  bun create elysia ./elysia-app
  bun create elysia-buchta ./elysia-buchta-app
  bun create hono ./hono-app

  bun create kingworld ./kingworld-app

  bun create next ./next-app

  bun create preact ./preact-app

  bun create react ./react-app

  bun create react-ssr ./react-ssr-app

  bun create svelte-kit ./svelte-kit-app

  bun create websi ./websi-app

# You can also paste a GitHub repository:

  bun create ahfarmer/calculator calc

This command is completely optional. To add a new local template, create a folder in /Users/jw/.bun-create/. To publish a new template, git clone https://github.com/oven-sh/bun, add a new folder to the "examples" folder, and submit a PR.
有一说一,虽然我欣赏Bun的勇气,但这些与Deno运行Vite一样,暂时仍属于Demo性质(相反的是,Deno的几款SSR框架Fresh[8]、Aleph[9]、Ultra[10]已经比较成熟了),距离生产使用还差了十万八千里。我认为与其这样玩,不如先老老实实把Node.js API兼容完以后,让大家从Node.js直接迁移到Bun来的要简单些。

当然,Bun可能也有它的考量,它提供了插件的能力,可以很方便地将各类型的资源(比如yaml、sass、json、toml之类)转换为JS,从这个意义上讲,抛弃Webpack好像也没有什么不可以。 

只不过像模块解析、插件、代码分割、缓存、热更新等等,都是需要水磨工夫才能搞好的。想想Vite还在马不停蹄地更新,你就知道了。不过Bun比Vite更靠近底层,如果做成了会更有优势。

我在Bun路线图[11]里找到这样一段:
Bun 是一个范围非常大的项目,而且仍处于早期阶段。从长远来看,Bun 的目标是提供一种一体化的工具来取代当今常见的复杂、分散的工具链:Node.js、Jest、Webpack、esbuild、Babel、yarn、PostCSS 等。

有兴趣的同学可以到https://github.com/oven-sh/bun/issues/159看看:

全面对比
2023年8月18日 Node.js Deno Bun
底层语言 C++ Rust Zig
JS引擎 V8 V8 JavaScriptCore
版本号 20.5.1 1.36 0.7.3
GitHub star 97.1K 90.4K 43.1K
Web API ✓(正在支持中)
TypeScript/JSX
Bundle ✓(准备放弃)
Compile(可执行文件) ✓(实验性质,正在支持中)
本地ESM
远程ESM(URL导入)
CommonJS
package.json ✓(部分支持)
.npmrc ❌(可能会支持) ❌(好像不准备支持)
安全校验(权限) ✓(正在支持中)
代码校验、格式化
内置测试库
[12]
插件(转换其它类型文件) [13]
其中,Web API、安全权限、Compile、内置测试库都是Node.js最近才引入的,可见Node.js也感觉到压力,也在进步,只是不知道它有没有支持TypeScript的计划;Deno独有的功能有远程ESM(Node.js可能因为安全问题不愿意引入[14])、全家桶的代码校验与格式化命令,目前有成熟的SSR框架Fresh、Aleph、Ultra等;

Bun独有的功能有宏、插件系统,未来可能会替代Webpack之类打包工具,实现前后端工具链一体化。

彩蛋:Bun 1.0要来了?
在撰写本文时,突然注意到Bun官网上拉了条幅:

点开后是个页面https://bun.sh/1.0[15]:

点击注册:

收到一封邮件:

看来是9月8日要开发布会,正式发布1.0了。好奇的是,Bun真的准备好了吗?不过想想Deno 1.0是2020年发布的,到现在都1.36了,又好像没什么了。。。

总结
本文先简介了Node.js、Deno和Bun的发展历史,对他们的特色功能做了详细的对比。从目前的情况来看,他们处于一个相互竞争、共同进步的节奏中。Node.js参考Deno引入了Web API、安全权限、Compile、内置测试库等功能,它那成熟、稳定、庞大的生态系统是它最大的护城河,但这点儿可能被Deno与Bun逐渐蚕食。Deno与Bun也有一些特色功能,是吸引开发者转投阵营的关键点。

在未来的三年内,Node.js是不太可能被淘汰的,除非Deno与Bun有了革命性的进步,比如性能、功能在某方面有了压倒性的优势。从某种层面来说,这几位彼此间哪有什么无法逾越的技术壁垒啊,更多可能是你抄我我抄你(相互借鉴)。

最后,Bun的1.0版本将于9月8日发布,有兴趣的同学可以预约关注啦。
用户评论