JS 社区掀起了运行时狂潮:Jarred Sumner 孵化的 Bun 1.0 官宣了。话虽如此,还是有一大坨道友很好奇:Bun 的本质是什么鬼物?为什么每个人都将其与百炼成钢的 Node 相提并论?Bun 只是另一种昙花一现的过眼云烟,还是会重新定义这场游戏?在本文中,让我们深入 Bun,瞄一下其功能,并了解它与成熟的 Node 的异同点。
Bun 是什么鬼物?
Bun 是一款适用于 JS/TS App 的超快速一体化工具包。Bun 的魅力在于它能够简化开发流程,比以往更丝滑高效。这并非不可能事件,因为 Bun 不仅仅是运行时,它还是包管理器/打包器/测试运行器。请想象一下,JS 开发有了一把瑞士军刀;此乃 Bun 馈赠与君之福利。
Bun 解决了什么问题?
Node 于 2009 年横空出世。虽然但是,与一大坨技术一样,随着技术的发展,其复杂性也与日俱增。请将其想象成一座城市。随着城市的扩张,交通拥堵可能会成为一个问题。
Bun 旨在成为缓解这种拥堵的新基建,让一切运行得更丝滑。这绝非反复造轮子,而是对其优化,确保我们在获得速度和简单性的同时,不会丧失 JS 独特和强大的本质。
Bun 被设计为比 Node 更快、更精简、更现代的竞品,所以让我们仔细瞄一下其比较。但首先让我们按下不表。
Node vs Deno vs Bun
在讨论 JS 运行时的进化史时,很难无视 Deno。“Node 之父”Ryan Dahl 推出了 Deno 作为一个新的运行时,旨在解决在 Node 中发现的某些挑战和遗憾。
Deno 是 JS/TS 的安全运行时。它直接解决了 Node 的一大坨短板。举个栗子,Deno 原生支持 TS,无需外部工具。与 Node 不同,Node 的脚本默认具有广泛的权限,Deno 采用安全第一的方法,要求开发者显式授予潜在敏感操作的权限,比如文件系统访问或网络连接。
虽然 Deno 提供了一个引人注目的 Node 竞品,但它还没有达到 Node 的广泛采用程度。因此,本文主要将 Bun 与成熟的 Node 对比。
入门
有了 Bun,我们可以使用 bun init -y 命令构建一个空项目。我们生成了若干文件,并在 index.ts 中添加一行 console.log('Hello, Bun!'')。在终端中运行命令 bun index.ts,即可看到打印了 'Hello, Bun!'。
JS 运行时
JS 运行时是一个提供使用和运行 JS 程序所需的所有组件的环境。
Node 和 Bun 都是运行时。Node 主要使用 C++ 编写,而 Bun 则使用一种名为 Zig 的低级通用编程语言编写。但这只是冰山一角。让我们仔细瞄一下 Bun 单独作为运行时的其他差异。
JS 引擎
JS 引擎是一种将我们编写的 JS 代码转换为机器码的程序,使计算机能够执行特定任务。
Node 使用为 Chrome 浏览器提供支持的谷歌 V8 引擎,而 Bun 使用 JSC(JavaScriptCore),这是苹果为 Safari 浏览器开发的开源 JS 引擎。
V8 和 JSC 有不同的架构和优化策略。JSC 优先考虑更快的启动时间和减少内存占用,以及稍慢的执行时间。另一方面,V8 优先考虑快速执行,并进行更多运行时优化,这可能会导致更多的内存占用。
这使得 Bun 速度更快,启动速度比 Node 快 4 倍。
图片
总结:Bun 的运行速度比 Deno 快 2.19 倍,比 Node 快 4.81 倍。
转译器
虽然 Node 是一个强大的 JS 运行时,但它本身并不支持 TS。要在 Node 环境中执行 TS,需要外部依赖。一种常见方案是使用构建步骤将 TS 转换为 JS,然后运行生成的 JS 代码。这是使用 tsx 包的基本设置:
- 安装
pnpm add -D typescript tsx
- 在 package.json 中,您可以设置脚本来简化流程。
{
"scripts": {
"dev": "tsx hello-bun.ts"
}
}
- 执行
使用上述脚本,您可以轻松运行 TS 文件:
pnpm dev
相比之下,Bun 提供了一种更精简的方法。它自带集成到运行时的 JS 转译器。这允许您直接运行 .js/.ts/.jsx/.tsx 文件。Bun 的内置转译器将这些文件无缝转换为普通 JS,无需额外步骤即可立即执行。
bun hello-bun.ts
运行 TS 文件时,速度差异会被放大,因为 Node 在运行前需要一个转译步骤。
图片
ESM 和 CJS 兼容性
模块系统允许开发者将代码组织成可复用片段。在 JS 中,两个主要的模块系统是 CJS(CommonJS)和 ESM(ES 模块)。CJS 源自 Node,使用 require/module.exports 处理同步模块,十分适合服务器端操作。
ES6 引入的 ESM 采用 import/export 语句,提供更加静态和异步的方案,并针对浏览器和现代构建工具进行了优化。让我们诉诸 CJS 使用 colors,诉诸 ESM 使用 chalk,这两个流行的包用于向控制台添加彩色输出,并更好地理解模块系统。
Node 传统上与 CJS 模块系统相关联。这是一个经典用法:
// Node 中的 CJS(index.js)
const colors = require('colors')
console.log(colors.green('Hello, Bun!'))
对于 Node 中的 ESM,您可以二选一:
- 您需要在 package.json 中包含 "type": "module"。
- 使用 .mjs 扩展名。
// Node 中的 ESM(index.mjs)
import chalk from 'chalk'
console.log(chalk.blue('Hello, Bun!'))
从 CJS 过渡到 ESM 是一个复杂的过程。在 ESM 推出后,Node 花了 5 年时间才在没有实验标志的情况下支持它。尽管如此,CJS 在生态系统中依然人气爆棚。
Bun 通过支持两者而无需任何特殊配置,简化了模块系统。Bun 的亮点功能是,它能够在同一文件中同时支持 import/require(),这在 Node 中是不可能事件:
// Bun 中的混合模块(index.js)
import chalk from 'chalk'
const colors = require('colors')
console.log(chalk.magenta('Hello from chalk!'))
console.log(colors.cyan('Hello from colors!'))
Web APIs
Web API 是浏览器筑基的 App 的组成部分,提供用于 Web 交互的工具,比如 fetch/WebSocket。虽然这些已经成为浏览器标准,但它们对 Node 等服务器端环境的支持却不一致。
在 Node 的早期版本中,浏览器中常用的 Web 标准 API 本身并不支持。开发者必须依赖 node-fetch 等第三方包来拷贝此功能。虽然但是,从 Node 18 开始,对 fetch API 提供了实验性支持,可能消除了这些包的需求。
Bun 通过为这些 Web 标准 API 提供内置支持来简化这一过程。开发者可以直接使用稳定的 fetch/Request/Response/WebSocket 等类似浏览器的 API,无需任何额外的包。此外,Bun 对这些 Web API 的原生实现,确保它们比第三方备胎更快、更可靠。
下面是一个兼容 Node(v18 及更高版本)和 Bun 的示例。虽然它在 Node 中是实验性的,但同款功能在 Bun 中是稳定的:
// Node(v18 及更高版本)的实验性 fetch,以及 Bun 内置的 fetch
async function fetchUserData() {
const response = await fetch('https://jsonplaceholder.typicode.com/users/1')
const user = await response.json()
console.log(user.name)
}
fetchUserData() // Leanne Graham
热重载
热重载是一项功能,可在代码更改时自动刷新或实时重新加载 App 的某些部分,提高开发者的工作效率,而无需完全重新启动。
在 Node 的生态系统中,您有一大坨选择来实现热重载。一种流行的工具是 nodemon,它会硬重启整个过程:
nodemon index.js
或者,从 Node 18 开始,引入了一个实验性的 --watch 标志:
node --watch index.js
这两种方法的目的都是在代码更改时提供 App 的实时重新加载。虽然但是,它们的行为可能一龙一猪,尤其是在某些环境或场景下。
举个栗子,nodemon 可能会导致中断,比如断开 HTTP 和 WebSocket 连接,而 --watch 标志处于实验阶段,可能无法提供全套功能。
Bun 的热重载更进一步。通过使用 --hot 标志运行 Bun,可以启用热重载:
bun --hot index.ts
与可能需要重新启动整个进程的 Node 方法不同,Bun 会就地重新加载代码,而不会终止旧进程。这可确保 HTTP 和 WebSocket 连接保持不间断,并保留 App 状态,提供更丝滑的 DX(开发体验)。
Node 兼容性
当过渡到新的运行时或环境时,兼容性通常是开发者最关心的问题。Bun 通过将自己定位为 Node 的同款竞品来解决此问题。这意味着,现有的 Node App 和 npm 包无需任何修改即可与 Bun 无缝集成。确保这种兼容性的关键功能包括但不限于:
- 支持内置 Node 模块,比如 fs/path/net。
- 识别全局变量,比如 __dirname/process。
- 遵守 Node 模块解析算法,包括熟悉的 node_modules 结构。
Bun 仍在茁壮成长。它专为增强开发工作流程而定制,十分适合资源有限的环境(比如 serverless 函数)。Bun 背后的团队正在努力实现全面的 Node 兼容性以及与流行框架的更好集成。
虽然 Bun 确保兼容 Node,但它不止于此。Bun 附带高度优化的标准库 API,可以满足开发者的刚需。
Bun APIs
- Bun.file()
延迟加载文件并访问各种格式的内容。此方法比 Node 对应的方法快 10 倍。
// Bun (index.ts)
const file = Bun.file('package.json')
await file.text()
// Node.js (index.mjs)
const fs = require('fs/promises')
const fileContents = await fs.readFile('package.json', 'utf-8')
- Bun.write()
用于将数据(从字符串到 Blob)写入磁盘的多功能 API。它的写入速度比 Node 快 3 倍。
// Bun (index.ts)
await Bun.write('index.html', '
')
// Node.js (index.mjs)
const fs = require('fs/promises')
await fs.writeFile('index.html', '
')
- Bun.serve()
使用 Web 标准 API 设置 HTTP 服务器或 WebSocket 服务器。它每秒处理的请求比 Node 多 4 倍,处理的 WebSocket 消息比 Node 的 ws 包多 5 倍。这种后端功能让人想起开发者在 Node 中使用 Express 的方式,但还具有 Bun 性能优化的额外优势。
// Bun (index.ts)
Bun.serve({
port: 3000,
fetch(request) {
return new Response('Hello from Bun!')
}
})
// Node.js (index.mjs)
import http from 'http'
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' })
res.end('Hello from Node.js!')
})
server.listen(3000)
Bun 还内置了对 sqlite 和密码的支持。
包管理器
Bun 不仅仅是一个运行时;它还是一个高级工具包,其中包括但不限于功能强大的包管理器。如果您发现自己在依赖安装过程中耐心等待,Bun 提供了一种令人耳目一新的更快竞品。即使您不使用 Bun 作为运行时,其内置的包管理器也可以加快您的开发工作流程。
瞄一下该表格,将 Bun 命令与 Node 的包管理器 npm 比较:
Bun | npm | 目的 |
|
| 安装 |
|
| 将新依赖添加到项目中 |
|
| 添加新的开发依赖 |
|
| 从项目中删除依赖 |
|
| 将指定包更新到最新版本 |
|
| 从 |
乍一看,Bun 的命令似曾相识,但体验却非同寻常。Bun 拥有比 npm 快几个数量级的安装速度。它利用全局模块缓存来实现这一点,消除从 npm 注册表的冗余下载。此外,Bun 采用每个操作系统可用的最快系统调用,确保最佳性能。
以下是从缓存安装入门 Remix 项目依赖的速度比较,Bun vs npm:
图片
bun CLI 包含一个兼容 Node 的包管理器,旨在以更快的速度替代 npm/yarn/pnpm。
此外,bun run
打包器
打包是获取多个 JS 文件,并将它们合并到一个或多个优化包中的过程。此过程还可能涉及转换,比如将 TS 转换为 JS,或者压缩代码减小体积。
在 Node 的生态系统中,打包通常由第三方工具而不是 Node 本身处理。Node 世界中某些人气爆棚的打包器包括但不限于 Webpack、Rollup 和 Vite,提供了代码分割、树摇优化和热模块替换等功能。
另一方面,Bun 不仅仅是一个运行时和包管理器,而且它本身也是一个打包器。它旨在打包各种平台的 JS/TS 代码,包括浏览器中的前端 App(Vue/React App)和 Node。
要使用 Bun 打包,您可以使用一个简单的命令:
bun build ./index.ts --outdir ./build
此命令打包 index.ts 文件,并将结果输出到 ./build 目录中。打包过程非常快,Bun 比 esbuild 快 1.75 倍,并且直接对 Parcel 和 Webpack 等其他打包器“降维打击”。
图片
Bun 的一个天秀功能是它引入了 JS 宏。这允许在打包期间执行 JS 函数,并将结果直接内联到最终打包中。这种机制为打包提供了全新的视角。
瞄一下此栗子,其中在打包过程中利用 Bun 的 JS 宏来获取用户名。该宏不是运行时 API 调用,而是在打包时获取数据,将结果直接内联到最终输出中:
// users.ts
export async function getUsername() {
const response = await fetch('https://jsonplaceholder.typicode.com/users/1')
const user = await response.json()
return user.name
}
// index.ts
import { getUsername } from './users.ts' with { type: 'macro' }
const username = await getUsername()
// build/index.js
var user = await 'Leanne Graham'
console.log(user)
虽然 Node 拥有成熟的打包工具,但 Bun 提供了一种可集成的、更快的、新颖的竞品,可以重塑打包格局。
测试运行器
测试是软件开发的一个重要方面,它确保代码如期运行,并在落地生产之前捕获潜在问题。除了作为运行时、包管理器和打包器之外,Bun 也是一个测试运行器。
虽然 Node 开发者传统上依赖 Vitest/Jest 来满足测试需求,但 Bun 引入了一个内置的测试运行器,它承诺了速度、兼容性和一系列满足现代开发工作流程的功能。
Bun 的测试运行器 bun:test 被设计为完全兼容 Jest,Jest 是一个以“expect”风格的 API 闻名的测试框架。这种兼容性确保熟悉 Jest 的开发者可以无缝衔接到 Bun,而无需经历陡峭的学习曲线。
import { test, expect } from 'bun:test'
test('2 + 2', () => {
expect(2 + 2).toBe(4)
})
使用 bun test 命令执行测试易如反掌。此外,Bun 的运行时支持开箱即用的 TS/JSX,无需额外的配置或插件。
从 Vitest/Jest 迁移
Bun 对兼容性的承诺体现在对 Jest 全局导入的支持。举个栗子,从 @jest/globals 或 vitest 导入将在内部重新映射到 bun:test。这意味着,现有的测试套件可以在 Bun 上运行,无需任何代码修改。
// index.test.ts
import { test } from '@jest/globals'
describe('test suite', () => {
test('addition', () => {
expect(1 + 1).toBe(2)
})
})
性能基准
Bun 的测试运行器不仅涉及兼容性;还涉及速度。在针对 Zod 测试套件的基准测试中,Bun 比 Jest 快 13 倍,比 Vitest 快 8 倍。Bun 的匹配器进一步凸显了这种速度优势,它是在快速原生代码中实现的。举个栗子,Bun 中的 expect().toEqual() 比 Jest 快 100 倍,比 Vitest 快 10 倍。
无论您是想迁移现有测试还是启动新项目,Bun 都可以提供符合现代开发需求的强大测试环境。
完结撒花
Node 古往今来一直是 JS 世界的基石,设定基准并引领开发者。虽然但是,Bun 正以一位不容小觑的挑战者身份粉墨登场,破而后立。
虽然 Bun 还处于早期发展阶段,但它名噪一时毋庸置疑。目前,它针对 MacOS 和 Linux 进行了优化,虽然 Windows 的支持正在进行中,但某些功能即将推出。凭借它提供的一切,Bun 无疑是您应该考虑探索的工具包。
免责声明
本文属于是语冰的直男翻译了属于是,略有删改,仅供粉丝参考,英文原味版请传送 Bun vs Node.js: Everything you need to know[1]。