
简介一个围绕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.tsconst 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 兼容的对象存储提供了一套原生读写接口不只是 AWSMinIO、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; charsetutf-8 }, }), }, routes: { /api/notes: { GET: listNotes, POST: createNote, }, /api/notes/:id: { DELETE: deleteNote, }, }, }); function indexHtml() { return !doctype html html langzh-CN head meta charsetutf-8 / titleBun Notes/title stylebody{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 h1Bun Notes/h1 form idform input nametitle placeholder标题 required / input namebody placeholder内容 / button typesubmit添加/button /form div idlist/div script typemodule 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 testBun 会自动把测试探测出来执行。这里要提醒一句先手动启动服务再跑测试因为测试脚本本身不负责起服务。想更省事可以写个 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 的兼容层已经足够成熟但足够成熟和完全等价之间永远差一个你的业务边界。本文还有配套的精品资源点击获取