简介:一个围绕Bun v1.3全栈JavaScript运行时发布的源码示例包,适合全栈开发者、Node.js迁移者以及希望低成本评估新型JS运行时能力的团队。压缩包共5个文件,包含3个HTML演示页、1个InsCode配置文件和1个gitignore文件,整体仅11KB,结构精简易读。演示页覆盖了Bun v1.3的前端热重载、生产构建、内置MySQL/PostgreSQL/SQLite数据库客户端、Redis客户端、WebSocket优化等核心特性,可直接在浏览器中打开对比效果;配置文件则有助于快速搭建体验环境,便于读者验证Bun在吞吐量、并发处理等方面相对第三方方案的优势。目前已有116人学习下载,适合需要快速上手Bun、进行技术选型评估或迁移Node.js项目时参考借鉴。 上周我把一个跑了大半年的 Node.js 全栈项目迁到 Bun v1.3 上,整个过程比预想顺很多:依赖从 node_modules 三层深拷贝变成 bun install 的一次性解压,测试从 jest 的配置地狱切到 bun test 开箱即用,前后端从两个端口两个进程变成同一个 Bun.serve 撑起来。这篇文章不吹 Bun 多伟大,而是把 v1.3 这一版作为全栈 JS 运行时的定位、实际用法和踩坑记录整理出来。适合正在评估"前端转全栈""要不要把手头项目切 Bun"的开发者,以及已经在本地试过 Bun、想把它推到真实项目里的人。
1. 为什么说 Bun v1.3 能叫"全栈"运行时:先看清定位
1.1 一个进程里装下整个工具链,才是全栈的关键
我见过很多朋友对"全栈 JS 运行时"的第一反应是:不就比 Node 快一点嘛。这个理解其实窄了。一个典型 Node 全栈项目,光工具链就要装 node、npm 或 pnpm、webpack 或 vite、ts-node 或 esbuild、jest 或 vitest、nodemon,每个组件自带配置文件、依赖树和升级节奏,前端一套后端一套,中间还有构建产物、环境变量、路径兼容这些破事。
Bun 从 1.0 开始就在做一件事:把运行时、包管理器、打包器、转译器、测试器和热重载全部收进同一个二进制文件。bun install、bun test、bun run、bun build、bun pm 全是内置命令,不需要额外安装任何东西。到了 v1.3,这个拼图基本闭合了——你用它跑后端 API、跑前端构建、跑集成测试、跑静态文件托管,全程只有一个依赖。这才是"全栈运行时"的真正含义:不是能在服务器上跑 JS,而是从写代码到上线,一整条链路里没有第二个运行时。
1.2 它和 TypeScript、Node 生态之间是什么关系
Bun 默认支持 TypeScript,不需要 tsconfig、不需要单独装 typescript 编译器,更不需要 ts-node 这种胶水层。你写const n: number = 1直接bun run index.ts就能跑。对很多前端转全栈的朋友来说,这省掉了一整类"JS 和 TS 配置打架"的问题。但注意,Bun 不是"又一个 JS 运行时",它本身是 Node.js 生态的超集思路——你原来写的 Express、Koa、Prisma 代码,大部分可以直接在 Bun 里跑,因为 v1.3 对 Node 内置模块的兼容已经做得相当全面。这一点后面我会专门讲坑。
另外要澄清一个概念:Bun 和 Deno 在哲学上有本质差异。Deno 强调"重新发明规范",Bun 走的是"兼容 Node、替代 Node"的路线。这决定了它作为全栈平台迁移成本极低,你可以把现有 Express 项目一步步切进来,而不是推翻重写。我个人判断,这也是 Bun 在社区里升温比 Deno 快的原因。
2. 从零把一个前后端项目跑起来:核心动作拆解
2.1 安装、升级与卸载,一起说清楚
安装就一行:
curl -fsSL https://bun.sh/install | bash装完重启终端,bun --version验证。升级用:
bun upgrade很多热搜词里总有人问 "bun 怎么卸载",我在本地也折腾过好几次。Bun 卸载的方式非常粗暴但有效 —— 删目录:
rm -rf ~/.bun然后打开你的 shell 配置文件(.bashrc、.zshrc或.profile),把里面对~/.bun/bin的 PATH 导出那一行删掉。Windows 用户去用户目录删.bun文件夹,同时检查环境变量。这个操作不会有特殊残留,因为 Bun 不写系统注册表,不装全局服务,这一点比很多开发工具干净得多。
2.2 一个进程同时扛 API 和静态资源
Bun 的全栈体验核心在Bun.serve。它不仅能起 HTTP 服务,还能直接声明静态路由和 API 路由。这里我直接给一个刚初始化完就能跑的最小例子:
mkdir bun-notes && cd bun-notes bun init -y然后写server.ts:
const server = Bun.serve({ port: 3000, static: { "/": new Response("Hello from Bun!", { headers: { "Content-Type": "text/html" }, }), }, routes: { "/api/ping": () => Response.json({ pong: true }), }, }); console.log(`listening on http://localhost:${server.port}`);跑bun run server.ts,打开 localhost:3000 看到页面,再访问 /api/ping 拿到 JSON。就这么简单,一个进程里前后端都活了。老手一眼能看出来好处:没有跨域问题、没有端口协调问题、没有两套服务部署问题,本地联调效率直接上一个台阶。
2.3 路由参数、请求体和响应工具的几个细节
routes 里支持路径参数,写法是冒号开头:
routes: { "/api/users/:id": { GET: (req) => Response.json({ userId: req.params?.id }), DELETE: (req) => new Response("deleted", { status: 204 }), }, }处理请求体时有个容易踩的坑:req.json()只能调用一次,调用完 body 流就消耗完了。如果你既想读原始文本又想做 JSON 解析,得先用await req.text(),再手动JSON.parse。另外Bun.serve里有 body 大小限制,默认 128MB,改法是maxRequestBodySize字段,这个对上传文件场景很重要。
3. 这一版里最值得吃的几个能力点
3.1 HTMLRewriter:服务端改 HTML 的瑞士军刀
Bun 内置了 HTMLRewriter,这玩意儿在 Node 生态里是个稀缺品。它能用类似 jQuery 选择器的方式对 HTML 做流式转换,不用把整个页面解析成 DOM。做微前端、做页面注入、做 SEO 改造特别好用:
import indexHtml from "./index.html"; const home = new HTMLRewriter() .on("title", { text(text) { text.replace("Bun Notes 官方示例"); }, }) .transform(indexHtml);这里import indexHtml from "./index.html"不是魔法,是 Bun 在编译期自动把 HTML 转成文本模块。用 HTMLRewriter. transform 之后,你可以把处理完的 HTML 直接塞进静态路由。我在项目里就是靠它统一给所有页面注入统计脚本,不用动模板引擎。
3.2 Node.js 兼容面:现在能跑很多以前跑不了的包
v1.3 在 Node 兼容层上下了不少功夫,重点补了node:http、node:https、node:fs、node:crypto等模块的边界情况。实测我原来用的 Express 4 中间件几乎全部能跑,Prisma 在 Linux 和 macOS 上也能正常工作。但要注意,兼容不等于完全等价,比如node:net底层和依赖原生 socket 特性的库(某些数据库驱动、SSH 库)偶尔会出问题,这类包大概率跑不起来。
我的建议是:先跑,别预先判死刑。Bun 生态已经过了"啥都用不了"的阶段,直接bun run试一试,报错再查https://bun.sh/docs/runtime/nodejs-apis的兼容清单。我迁项目时就是靠这个页面排查出两个坑,后面单独讲。
3.3 SQLite、S3 和内置 API:少装一堆 npm 包
Bun 把很多以前要靠第三方包的活都内置了。最典型的是bun:sqlite—— 一个同步的、零依赖的 SQLite 驱动,API 简洁,性能比很多 ORM 底层驱动还快。还有Bun.s3()客户端,给 AWS S3 兼容的对象存储提供了一套原生读写接口,不只是 AWS,MinIO、Cloudflare R2 这些兼容 S3 的服务都能连。这些东西对全栈项目的意义很大:你不需要为了一个数据库查询去拉一大堆依赖,整个项目依赖树直接瘦一圈。
另外Bun.$这个 shell 工具也值得一提,它能让你在 JS 里直接跑 shell 命令并拿到结果,做部署脚本、做预处理任务都很顺手。全栈项目里经常要写文件操作、跑外部命令,用Bun.$能少写一大坨 child_process 样板代码。
4. 实战:用 Bun 写一个可运行的 notes 全栈小应用
光讲能力不落地没用。下面我完整带一遍:一个笔记应用,后端是 SQLite 存的 CRUD API,前端是一个原生 HTML 页面,跑在同一个 Bun.serve 上。
4.1 项目结构设计
bun-notes/ ├── server.ts # 服务入口,API + 静态资源 ├── server.test.ts # 集成测试 ├── package.json └── tsconfig.json # 可选,不写也能跑这个结构有意保持极简。真实项目里你可能会拆 handlers、db、lib,但核心思想是:入口文件同时声明静态路由和 API 路由。
4.2 后端 API + SQLite 数据层
// server.ts import { Database } from "bun:sqlite"; const db = new Database("notes.db", { create: true }); db.run(` CREATE TABLE IF NOT EXISTS notes ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, body TEXT, created_at TEXT DEFAULT (datetime('now')) ) `); // 演示用:每次启动清空数据 db.run("DELETE FROM notes"); const listNotes = () => { const rows = db.query("SELECT * FROM notes ORDER BY id DESC").all(); return Response.json(rows); }; const createNote = async (req: Request) => { const { title, body } = await req.json(); if (!title) return Response.json({ error: "title is required" }, { status: 400 }); db.run("INSERT INTO notes (title, body) VALUES (?, ?)", [title, body ?? ""]); return Response.json({ ok: true }, { status: 201 }); }; const deleteNote = (req: Request) => { const id = (req.params as { id: string }).id; db.run("DELETE FROM notes WHERE id = ?", [id]); return Response.json({ ok: true }); }; Bun.serve({ port: 3000, static: { "/": new Response(indexHtml(), { headers: { "Content-Type": "text/html; charset=utf-8" }, }), }, routes: { "/api/notes": { GET: listNotes, POST: createNote, }, "/api/notes/:id": { DELETE: deleteNote, }, }, }); function indexHtml() { return `<!doctype html> <html lang="zh-CN"> <head> <meta charset="utf-8" /> <title>Bun Notes</title> <style>body{font-family:system-ui;max-width:720px;margin:48px auto;padding:0 16px} #list div{border-bottom:1px solid #eee;padding:12px 0} button{margin-left:8px}</style> </head> <body> <h1>Bun Notes</h1> <form id="form"> <input name="title" placeholder="标题" required /> <input name="body" placeholder="内容" /> <button type="submit">添加</button> </form> <div id="list"></div> <script type="module"> const list = document.getElementById("list"); const form = document.getElementById("form"); async function load() { const data = await fetch("/api/notes").then(r => r.json()); list.innerHTML = ""; for (const n of data) { const div = document.createElement("div"); div.innerHTML = "<strong>" + n.title + "</strong> " + n.body; const del = document.createElement("button"); del.textContent = "删除"; del.onclick = async () => { await fetch("/api/notes/" + n.id, { method: "DELETE" }); load(); }; div.appendChild(del); list.appendChild(div); } } form.onsubmit = async (e) => { e.preventDefault(); const fd = new FormData(form); await fetch("/api/notes", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ title: fd.get("title"), body: fd.get("body") }) }); form.reset(); load(); }; load(); </script> </body> </html>`; } console.log("listening on http://localhost:3000");bun run server.ts之后,打开页面添加几条笔记,再新建一个终端跑curl http://localhost:3000/api/notes,能直接看到 JSON 数据。整个过程没有配 CORS、没有起两个服务、没有装任何 ORM,这就是全栈运行时该有的样子。
4.3 跑测试和热更新
Bun 内置测试器,集成测试直接写:
// server.test.ts import { describe, expect, test } from "bun:test"; const BASE = "http://localhost:3000"; describe("notes api", () => { test("创建笔记后能在列表里查到", async () => { await fetch(`${BASE}/api/notes`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ title: "第一篇", body: "hello bun" }), }); const list = await fetch(`${BASE}/api/notes`).then((r) => r.json()); expect(list.some((n: any) => n.title === "第一篇")).toBe(true); }); });跑bun test,Bun 会自动把测试探测出来执行。这里要提醒一句:先手动启动服务再跑测试,因为测试脚本本身不负责起服务。想更省事可以写个 beforeAll 在测试里 spawn 服务进程,但初学者我建议先保持简单。
开发时用bun --hot server.ts启动,修改文件后不用重启,Bun 会基于模块图做局部热更新。这个热更新不是简单重启进程,而是尽量复用没变的模块,实测改一个 API handler 之后,已有连接不会断开。提交生产前用bun build ./server.ts --compile --outfile bun-notes-app,能打出一个包含运行时和代码的单一可执行文件,目标机器不用装 Bun,我拿它做小工具分发给同事,体验非常好。
5. 实际跑起来之后,值得说的坑和处理经验
5.1 两个 Node 兼容性的坑
第一个坑是回调式 API。Bun 对node:fs的 Promise 版本支持很完善,但某些老库还在用 callback 风格,比如传入一个(err, data) => {}回调。v1.3 大部分情况能兜住,但个别极端参数组合会直接抛 "运行时错误"。我的处理方式是:优先用内置的Bun.file()读写文件,或者改用 promise 版 API,别跟老库硬刚。
第二个坑是process.env的读取时机。Bun 在启动时读取.env文件并把变量合并进环境,但如果你在某个模块顶层就读取环境变量,而这个模块之前被其他测试用例带加载过,缓存可能导致变量读不到最新值。解决方案是不要在模块顶层解构整个process.env,而是在用的时候再读,或者统一走一个config.ts在入口函数里初始化。
5.2 运行时错误的完整排查链路
全栈项目最常见的生产报错是连接问题。我踩过的一个具体案例:API 有时候返回 500,日志只有一行 "error: connection closed prematurely"。我当时的排查链路是这样的:
- 先确认是不是代码问题:本地
bun --hot server.ts复现,发现本地稳定跑几个小时没问题。 - 排查是不是部署环境网络配置问题:检查了服务端口监听、防火墙,没发现异常。
- 抓生产进程的状态:用
ctrl + c杀掉进程后看日志尾部,发现是日志输出进程先行退出,导致外层服务误报连接关闭。 - 最终确认是进程管理器把 stdout 管道 buffer 满了,服务端还没崩,观察者先以为崩了。
排查结果是:不是 Bun 的 bug,是我自己的进程管理脚本没处理子进程的退出状态。我想说的是,Bun 的报错一般比较直白,看到 "panic" 或 "Segmentation fault" 用bun upgrade升级一下版本,大概率是修复了某个已知问题;看到网络类错误,优先怀疑自己的部署层,别急着甩锅给运行时。
5.3 什么时候适合把项目迁到 Bun
最后说点主观判断。经过这一轮迁移,我认为这几类项目最值得切 Bun:一是全栈小团队的项目,人员本来就要写前端和后端,Bun 能砍掉一半工具链学习成本;二是对冷启动敏感的服务,Bun 的启动速度比 Node 快一个数量级,Serverless 场景尤其划算;三是想给团队推广 TypeScript 的项目,Bun 零配置跑 TS,能让"JS 还是 TS"的争论直接消失。
不太适合的也有两类:重度依赖原生 Node 扩展(node-gyp 编译出的 .node 模块)的项目,因为 Bun 的 native ABI 兼容还不完美;以及已经深度用上 pnpm workspace + 复杂 monorepo 工具链的项目,迁移收益有限,除非你想要单一二进制部署。
我在实际使用里学到最重要的一招:迁移时不要一把梭,先挑一个非核心服务,用bun run启动看看日志有没有兼容告警,再逐步扩大范围。Bun v1.3 的兼容层已经足够成熟,但"足够成熟"和"完全等价"之间,永远差一个你的业务边界。
本文还有配套的精品资源,点击获取