
简介Bun v1.3全栈JS运行时发布代码包面向需要高性能全栈开发体验的JavaScript/TypeScript开发者。该版本将前端热重载、生产构建、内置MySQL/PostgreSQL/SQLite数据库客户端及Redis客户端整合为一体Redis吞吐量达250万次/秒同时优化WebSocket、包管理与Node.js兼容性适合全栈团队与企业级应用快速验证和落地。压缩包共5个文件以HTML演示页面为主3个html配合inscode在线运行配置与gitignore工程文件整体仅11KB可轻量导入本地或云端环境直接运行。已有127人学习下载。通过示例代码可直观学习Bun v1.3的启动方式、页面热更新、数据库与Redis调用等核心写法并借助inscode配置快速复现运行环境是低成本评估新运行时及迁移既有Node.js项目的高价值参考。1. Bun v1.3一个二进制把全栈 JS 的复杂度压到最低做全栈 JS 开发最烦的不是写业务代码而是搭环境Node 装完还要装 npm、装打包器、装测试框架、装 dotenv、装 ts-node一套下来小半天没了换台机器又要重来。Bun v1.3 这个全栈 JS 运行时想解决的就是这件事——把运行时、包管理器、打包器、测试运行器、原生 SQLite 驱动全塞进一个可执行文件。装完它一个bun run就能起后端服务.ts文件直接跑.jsx前端代码直接打包连.env都帮你自动加载。它能在启动速度和内存占用上明显优于常见 Node 实现这让它在高并发 API 和小型全栈应用里成了值得试一把的选项。这篇文章面向两类人一类是刚从 HTML/CSS/JS 基础语法走到全栈开发的新手想知道怎么用一个工具把这些串起来另一类是已经在生产环境用 Node 的熟手想知道 v1.3 里哪些能力真的能替代现有工具链、哪些场景下不该换。下面每一章都会落在可执行的命令和代码上顺手把参数边界和踩过的坑一起交代清楚。2. 装好 Bun v1.3 并盘清运行时边界和 Node 共存的第一步2.1 安装与版本确认单行命令背后的默认路径陷阱Bun 的官方安装脚本在 macOS 和 Linux 上是一行命令Windows 则建议走 WSL2 或者用 PowerShell 安装脚本。因为 Windows 原生支持直到现在依然不如前两者稳定我一般会建议 Windows 用户直接在 WSL2 里装别在原生环境上耗时间。安装完成后立刻做三件事查版本、查安装路径、查运行时真实能力。这三项决定你后面能不能放心用它跑生产脚本。# 安装 Bun 的常见做法macOS / Linux curl -fsSL https://bun.sh/install | bash # 验证是否装到预期路径 which bun bun --version # 查看运行时内置能力清单 bun pm ls第一段脚本里的curl | bash是最常见的安装方式它会默认把 Bun 装到~/.bun目录并把~/.bun/bin追加进 shell 的 PATH。这里有个坑如果你机器上同时有 nvm 管理的 Nodebun --version能正常输出不代表bun一定会被 shell 优先命中。我遇到过which bun指向/usr/local/bin/bun但实际版本是老的这种翻车现场原因就是之前手动软链过。所以装完务必先跑which bun确认路径再跑bun --version确认版本不小于 1.3。bun pm ls这条命令很多人忽略它用于列出当前 Bun 运行时内置的包能力比如bun:sqlite、bun:test这些模块是否可用。v1.3 里原生 SQLite 模块已经稳定这一步能让你在写业务前先确认运行时没有缺模块。2.2 运行时定位它到底在抢谁的饭碗Bun v1.3 的核心定位是「JavaScript 运行时」这和 Node.js、Deno 是同一个生态位。但它的全栈体现在两点第一它内置了打包器前端代码能用bun build直接产出静态资源省掉 webpack/vite 这一层第二它原生支持.ts、.jsx、.tsx文件直接执行不需要 ts-node 或 tsx 这类转译层。要注意一个常见误用有些人把 Bun 当成 Node 的 drop-in 替代品直接bun app.js跑老项目。大部分时候能跑但原生模块.node 文件和部分依赖 Node 内建 API 的包会出兼容性问题。v1.3 提供了 Node 兼容层但它的目标是「覆盖大多数场景」不是「100% 二进制兼容」。所以我的建议是新项目优先用 Bun老项目先跑测试套件再决定。下面用一个极简的 HTTP 服务对比 Bun 和 Node 的启动差异// server.js const server Bun.serve({ port: 3000, fetch(req) { return new Response(Hello from Bun v1.3); }, }); console.log(Server running at http://localhost:${server.url});// node-server.mjs import { createServer } from node:http; const server createServer((req, res) { res.end(Hello from Node); }); server.listen(3000, () { console.log(Server running at http://localhost:3000); });# 分别启动并对比启动耗时 time bun run server.js time node node-server.mjs第一段用的是 Bun 自带的Bun.serveAPI它返回一个 Server 对象server.url会输出实际监听地址。第二段是 Node 原生写法用于对比。第三段脚本里的time是 shell 内置命令能测出从进程启动到监听完成的总耗时。在 v1.3 里Bun 的启动耗时通常在 30ms 以内Node 则在 80ms 到 150ms 之间差距在冷启动场景比如 Serverless 函数里会被放大。这里要补一个参数说明Bun.serve的port字段支持设为0表示随机分配可用端口。这在本地调试多个服务时很实用拿到server.url后直接访问输出地址即可不用手动改配置去撞端口冲突。2.3 全栈项目结构一个 package.json 管前后端用 Bun 做全栈项目我一般会保持一个极简结构不搞 monorepo 那套重东西project/ ├── package.json ├── index.html ├── server.ts # 后端入口API 静态托管 ├── src/ │ └── App.tsx # 前端组件 └── public/ └── style.cssserver.ts里用Bun.serve同时处理 API 路由和静态文件前端资源由bun build在部署前打成dist/目录server.ts读取dist/下的文件返回给浏览器。这样整个项目不需要 nginx、不需要两个进程生产环境就是一个进程一个端口。初始化项目的命令mkdir bun-fullstack-demo cd bun-fullstack-demo bun initbun init会交互式地询问入口文件、包名等信息生成一个最小可运行的package.json和index.ts。它和npm init的区别在于生成的是 Bun 格式的package.json并且默认信任bun install的扁平化依赖安装。这个默认信任有个隐藏影响Bun 的依赖解析策略和 npm 不完全一样它倾向于 hoist 所有依赖到顶层。这意味着你在项目里import一个没有直接声明在 dependencies 里的包时Bun 不一定报错因为它在顶层 node_modules 里找到了。这会让项目在换回 npm 安装时突然报「模块找不到」的错。所以用 Bun 的项目package.json里每个被 import 的包都要显式声明。3. 用内置 Web 标准 API 搭后端路由、静态托管与流式响应3.1 路由设计绕过框架手写 fetch 分发Bun v1.3 的Bun.serve直接采用 Web 标准 APIfetch回调的形参是Request对象返回Response对象。这意味着你不需要 Express 或 Hono 也能写出可维护的路由层。常见做法是用URL对象解析路径再手动分发给对应的处理函数// server.ts const server Bun.serve({ port: 3000, async fetch(request) { const url new URL(request.url); const pathname url.pathname; if (request.method GET pathname /) { return Response.json({ message: Bun v1.3 全栈运行中 }); } if (request.method GET pathname /api/users) { const users await getUsers(); // 模拟查库 return Response.json(users); } if (request.method POST pathname /api/users) { const body await request.json(); const user await createUser(body); return Response.json(user, { status: 201 }); } // 兜底 return Response.json({ error: Not Found }, { status: 404 }); }, });这段代码的核心在于手写路由分发。第一行用new URL(request.url)把请求地址解析成结构化对象.pathname拿到路径部分。第二段判断request.method和pathname双重条件来锁定具体接口。Response.json()是 v1.3 提供的便捷方法会自动设置Content-Type: application/json头。POST 请求里await request.json()用于解析请求体如果客户端传的不是合法 JSON这个await会抛异常所以真实项目里一定要包try/catch否则会直接 500。这里有一个实际踩过的坑路径匹配大小写问题。Bun 的URL解析不会自动忽略大小写客户端请求/api/Users会落到 404。不少前端同学调试时问为什么接口报错最后发现是路径中某个字母大小写不一致。类似「js 忽略大小写」的需求在路径匹配里是坑不要想当然认为框架会帮你处理。要么前端约定全小写要么你在路由分发前统一做pathname pathname.toLowerCase()。3.2 静态文件托管Bun.file 的流式响应与 MIME 推断后端不只是接口还要托管前端打包后的静态资源。Bun.file是 v1.3 提供的文件读取 API它会惰性加载文件内容并自动推断 MIME 类型。相比fs.readFileSync一次性把整文件读进内存Bun.file更适合静态托管场景// static.ts import { readdir } from node:fs/promises; import { join, extname } from node:path; const distDir join(import.meta.dir, dist); const server Bun.serve({ port: 3000, async fetch(request) { const url new URL(request.url); let pathname url.pathname; if (pathname /) pathname /index.html; const filePath join(distDir, pathname); // 防止目录穿越 if (!filePath.startsWith(distDir)) { return new Response(Forbidden, { status: 403 }); } const file Bun.file(filePath); const exists await file.exists(); if (exists) { return new Response(file); } // SPA 场景返回 index.html 交给前端路由 const fallback Bun.file(join(distDir, index.html)); return new Response(fallback); }, });这里值得展开的是Bun.file的行为。new Response(file)时Response 构造器会识别BunFile类型自动设置正确的Content-Type比如.html对应text/html.js对应text/javascript和Content-Length。它底层走的是流式读取大文件不会占满进程内存。目录穿越防护是必须写的join(distDir, pathname)在处理../时可能拼出 dist 之外的路径所以要先拼再校验startsWith(distDir)。这个坑在真实项目里出现过我没有在模拟项目里构建过漏洞但安全这种事情不能赌。另外await file.exists()在 v1.3 里是原生方法旧版本的常见写法是statSync包 try/catch现在可以直接用exists()判断。SPA 的回退逻辑要注意fallback返回的是index.html但 HTTP 状态码默认是 200。对于已经发布的资源比如/assets/app.js不存在理论上应该返回 404SPA 场景里 fallback 到index.html是常见取舍但要确认你的 API 路由在文件托管之前被匹配否则/api/users找不到文件时会落到index.html而不是 JSON 404。3.3 流式响应与 SSE全栈里被低估的一块Bun v1.3 对ReadableStream的支持是原生级别的这意味着服务端推送SSE和流式 AI 响应可以直接写不需要额外的eventsource包// stream.ts const server Bun.serve({ port: 3000, fetch(request) { const url new URL(request.url); if (url.pathname /api/stream) { const encoder new TextEncoder(); const stream new ReadableStream({ start(controller) { let count 0; const timer setInterval(() { const data data: ${JSON.stringify({ tick: count })}\n\n; controller.enqueue(encoder.encode(data)); if (count 5) { clearInterval(timer); controller.close(); } }, 1000); }, }); return new Response(stream, { headers: { Content-Type: text/event-stream, Cache-Control: no-cache, Connection: keep-alive, }, }); } return Response.json({ error: Not Found }, { status: 404 }); }, });这段逻辑不复杂ReadableStream里的start回调负责生产数据controller.enqueue把编码后的数据块推进流里controller.close()结束流。SSE 协议要求数据以data:开头、以\n\n结尾前端用EventSource接口可以直接消费不需要额外的 WebSocket 协议栈。前端消费端值得一提EventSource自动处理断线重连而 fetch ReadableStream 需要你手动管理重连。SSE 适合单向服务端推送像实时日志、进度条、AI 回答这类场景Bun v1.3 在单进程内能维持大量长连接而线程不爆炸主要归功于其事件循环模型。这里还有一个容易被忽略的参数Connection: keep-alive在 HTTP/1.1 里是默认行为但很多新手在 Response headers 里漏写它导致部分代理服务器提前断开流。写上不会出错不写可能踩坑。4. 把 SQLite 接进全栈链路持久化、事务与前端数据联动4.1 原生 bun:sqlite零依赖的数据层全栈项目不能只有接口和静态页面数据持久化是绕不开的。v1.3 内置的bun:sqlite模块是独立于better-sqlite3的原生实现不需要编译原生依赖也不需要.node文件。先初始化数据库并建表// db.ts import { Database } from bun:sqlite; const db new Database(data.db, { create: true }); // 初始化表结构幂等 db.run( CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, email TEXT UNIQUE NOT NULL, created_at TEXT DEFAULT (datetime(now)) ) ); export type User { id: number; name: string; email: string; created_at: string; }; export function getUsers(): User[] { return db.query(SELECT * FROM users ORDER BY id DESC).all() as User[]; } export function createUser(data: { name: string; email: string }): User { const result db .query(INSERT INTO users (name, email) VALUES (?, ?)) .run(data.name, data.email); const user db .query(SELECT * FROM users WHERE id ?) .get(result.lastInsertRowid) as User; return user; }new Database(data.db, { create: true })里的第二个参数是关键create: true表示文件不存在时自动创建省掉手动touch的步骤。db.query().all()返回所有结果数组.run()用于执行 INSERT/UDPATE/DELETE返回对象里有lastInsertRowid和changes字段。参数化查询这里需要注意db.query(... ).run(data.name, data.email)用的是?占位符Bun 的 SQLite 驱动不支持字符串拼接 SQL也不要把变量直接template string灌进 SQL 里——那等于给自己挖 SQL 注入的坑。凡是外部输入都走?绑定。4.2 用事务改造批量写入接口单条插入没问题批量插入就有事务需求了。SQLite 默认每条 INSERT 都独立事务批量插 10000 条会慢得让人怀疑机器坏了。v1.3 里可以用db.transaction()包一组操作为单一事务// batch.ts import { Database } from bun:sqlite; const db new Database(data.db, { create: true }); db.run( CREATE TABLE IF NOT EXISTS logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, message TEXT NOT NULL, level TEXT NOT NULL ) ); const insertLog db.query( INSERT INTO logs (message, level) VALUES (?, ?) ); const insertMany db.transaction((logs: { message: string; level: string }[]) { for (const log of logs) { insertLog.run(log.message, log.level); } }); // 模拟批量写入 const sampleLogs Array.from({ length: 1000 }, (_, i) ({ message: log message ${i}, level: i % 3 0 ? error : info, })); insertMany(sampleLogs); // 验证 const count db.query(SELECT COUNT(*) as count FROM logs).get(); console.log(总日志数: ${count.count});db.transaction()接收一个函数并返回同签名的新函数。事务函数被调用后内部的insertLog.run()都在同一个事务里执行最后统一提交。如果中途某个run抛异常整个事务会自动回滚不会出现一半成功一半失败的数据不一致状态。这组代码里的一个优化点是db.query()在循环外预编译避免在 1000 次循环里重复编译 SQL。Array.from({ length: 1000 }, ...)是纯前端 JS 技巧放在这里是模拟批量数据实际场景多半是从文件读入或从队列消费。测试完成后建议删掉测试表和数据库文件避免脏数据干扰后续开发。4.3 前后端联动一个保存按钮的完整闭环现在把数据库和 HTTP 层接起来做一个完整的「用户列表 新增用户」闭环。前端页面用原生 HTML 一小段 JS 内联脚本不引入前端框架目的是把链路讲清楚!-- index.html -- !DOCTYPE html html langzh-CN head meta charsetUTF-8 / titleBun v1.3 全栈用户管理/title style body { font-family: system-ui, sans-serif; max-width: 600px; margin: 40px auto; } input { padding: 8px; margin-right: 8px; } button { padding: 8px 16px; } ul { list-style: none; padding: 0; } li { padding: 8px 0; border-bottom: 1px solid #eee; } /style /head body h1用户列表/h1 div input idname placeholder姓名 / input idemail placeholder邮箱 / button onclickaddUser()添加用户/button /div ul iduserList/ul script async function loadUsers() { const res await fetch(/api/users); const users await res.json(); const list document.getElementById(userList); list.innerHTML users .map(u li${u.id}: ${u.name} (${u.email})/li) .join(); } async function addUser() { const name document.getElementById(name).value.trim(); const email document.getElementById(email).value.trim(); if (!name || !email) { alert(姓名和邮箱不能为空); return; } const res await fetch(/api/users, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ name, email }), }); if (res.ok) { document.getElementById(name).value ; document.getElementById(email).value ; loadUsers(); } else { const err await res.json(); alert(新增失败: (err.error || res.status)); } } loadUsers(); /script /body /html这段前端代码是围绕「js 函数」和「全栈项目」这两个热搜词最常见的落地场景。loadUsers()是普通异步函数addUser()里值得注意两个细节第一fetch的 POST 请求必须在 headers 里声明Content-Type: application/json否则服务端request.json()拿到的 Body 解析会失败第二前端用res.ok判断 HTTP 状态而不是用try/catch包住整个fetch因为网络错误走catch业务错误比如邮箱重复导致的 409走res.ok false这是两种完全不同的错误路径。对应地服务端接口要改写为支持查重和错误返回// server-full.ts import { Database } from bun:sqlite; const db new Database(data.db, { create: true }); db.run( CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, email TEXT UNIQUE NOT NULL, created_at TEXT DEFAULT (datetime(now)) ) ); const server Bun.serve({ port: 3000, async fetch(request) { const url new URL(request.url); const pathname url.pathname; // API 路由 if (pathname /api/users request.method GET) { const users db.query(SELECT * FROM users ORDER BY id DESC).all(); return Response.json(users); } if (pathname /api/users request.method POST) { try { const body await request.json(); const { name, email } body; if (!name || !email) { return Response.json({ error: 姓名和邮箱为必填项 }, { status: 400 }); } const result db .query(INSERT INTO users (name, email) VALUES (?, ?)) .run(name, email); const user db .query(SELECT * FROM users WHERE id ?) .get(result.lastInsertRowid); return Response.json(user, { status: 201 }); } catch (e) { // 邮箱唯一约束冲突时SQLite 会抛错 return Response.json({ error: 邮箱已存在或请求体非法 }, { status: 409 }); } } // 静态资源托管优先 dist 或 public 目录 const publicDir import.meta.dir; const candidates [/index.html]; const file Bun.file(publicDir pathname); if (await file.exists()) { return new Response(file); } if (pathname /) { return new Response(Bun.file(publicDir /index.html)); } return Response.json({ error: Not Found }, { status: 404 }); }, }); console.log(全栈服务已启动: ${server.url});这里的核心是catch (e)分支的用法SQLite 的 UNIQUE 约束在插入重复 email 时抛错这个异常会被catch捕获并以 409 返回给前端而前端addUser()里res.ok false会走alert分支。这正好对应热词里「js 处理接口异常」的常见业务诉求。静态托管部分有一个需要注意的路径拼接问题import.meta.dir是 Bun v1.3 提供的当前文件目录快捷方式替代了 Node 的__dirname。Bun.file(publicDir pathname)这种拼接方式没有做目录穿越校验——如果你手动新增了其他静态路径记得回头补上第 3 节的startsWith校验。示例代码保持简单是为了可读性生产环境不建议裸奔。5. 避坑清单Bun v1.3 全栈实践中我踩过的 5 个坑5.1 现象bun install后项目跑不起来报模块找不到原因项目是之前用 npm 初始化的node_modules结构是 npm 那种嵌套的Bun 的 hoist 策略和 npm 不完全一致部分包被提升了位置import直接撞上幽灵依赖。解决删掉node_modules和package-lock.json重新bun install生成bun.lock再把package.json里所有 import 过的包名补齐到 dependencies。不要在新项目里混用 npm 和 bun 安装。5.2 现象Bun.serve里用process.env拿到的是 undefined原因v1.3 支持从.env自动加载环境变量但优先级是「真实环境变量 .env文件」。如果.env里变量名和系统环境变量重名系统值会覆盖文件值导致预期值和实际不符。解决先确认你没有在 shell 里 export 同名变量再用bun run --env-file.env.local server.ts显式指定环境文件。显式传参优先于默认.env这能避免机器上的全局变量污染项目配置。5.3 现象高并发下 SQLite 写入变慢甚至报database is locked原因bun:sqlite默认同步 API长事务阻塞事件循环。多个Bun.serve并发请求同时写同一条记录时SQLite 的锁机制会报 busy 错误。解决写入密集场景用事务批量合并实在要高频单条写入用db.exec(PRAGMA journal_mode WAL)开启 WAL 模式并把busy_timeout调到 5000ms。这两个 PRAGMA 在读写并发上有立竿见影的效果代价是数据库文件会多出-wal和-shm两个附属文件备份时记得一并拷贝。5.4 现象JSX 文件能跑但浏览器控制台报 JS 运行时错误原因服务端是 Bun 直接执行.tsx但返回给浏览器的 HTML 里引用的前端脚本是原样 JSX浏览器不认识。解决前端代码必须走bun build打包链路。最常见的做法是把src/App.tsx打成dist/app.jsHTML 里只引打包产物。服务端跑 TSX 是后端能力不是浏览器能力这个边界要分清。对应的命令是bun build ./src/App.tsx --outdir ./dist --minify--minify会压缩产物开发环境可以去掉。生成的dist/app.js由第 3 节的静态托管逻辑返回。5.5 现象本地bun run一切正常部署到 Linux 服务器后端口起不来原因最常见的是二进制平台差异。Bun 在 macOS 上装的是 darwin 版本部署机器是 linux-x64如果你直接把本地node_modules整个 rsync 上去自带原生绑定的包会崩。另一个高频原因是监听地址绑到了::1而服务器防火墙只开了 IPv4 端口。解决部署机器上单独执行bun install不要拷贝node_modulesBun.serve里显式指定hostname: 0.0.0.0注意是要监听外网还是仅本机调试默认绑定所有网卡安全性靠云防火墙控制。这两个操作能解决绝大多数「本地好使、线上拉胯」的问题。这类「脚本实测高干扰」的现象多半都不是代码逻辑问题而是环境差异。6. 用 --compile 把全栈应用压成单文件发布的最后一步验证一个 Bun 全栈项目是否真正「可交付」我最后都会做一件事bun build --compile。这个能力把入口 TS 文件、依赖、内联的资源全部打包成一个可执行二进制不需要目标机器装 Node、装 Bun、装模块。部署变成了「拷一个文件过去运行它」。# 把 server-full.ts 编译为单文件可执行程序 bun build ./server-full.ts --compile --outfile my-fullstack-app # 直接运行 ./my-fullstack-app--compile标志是 v1.3 的核心卖点。它生成的二进制包含了 Bun 运行时本身体积一般在 60MB 到 90MB 之间换来的是目标机器零依赖。这里有两个细节第一Bun.file(import.meta.dir /index.html)这种运行时读文件逻辑不会打包进二进制--compile只编译代码不内嵌静态资源所以部署时二进制旁边要有index.html和data.db或首次运行时自动创建第二--outfile指定输出路径不加扩展名在 Linux 上是 ELF 可执行文件在 macOS 上是 Mach-O交叉编译还不成熟别指望在 Mac 上编出 Linux 二进制。把编译产物收尾的验证步骤我一般这么做# 临时目录里干净跑一次验证无隐形依赖 mkdir /tmp/bun-deploy cp my-fullstack-app index.html /tmp/bun-deploy/ cd /tmp/bun-deploy ./my-fullstack-app # 压测一下接口 curl -s http://localhost:3000/api/users | head -c 200 # 验证静态资源响应头 curl -sI http://localhost:3000/ | grep -i content-type这里curl -sI看响应头是验证静态托管 MIME 是否正确的快捷方式。如果Content-Type不是text/html说明Bun.file的推断失效多半是文件扩展名不对或文件不存在走到了 fallback。我自己的习惯是每次改动构建配置后都跑一遍这个三步验证它能快速暴露「编译成功但运行崩了」的假成功——这类问题在 Node 世界里常见于依赖 hoist 不彻底在 Bun 的--compile场景里很少出现因为依赖被打进二进制了真正的坑只剩下静态资源和数据库文件的相对路径。最后留一个我在模拟项目里反复踩过的习惯不要在项目根目录直接跑编译产物。入口文件的import.meta.dir指向的是源码目录编译后指向的是二进制所在目录。如果你在项目根目录运行./dist/my-fullstack-app它读取的index.html路径会变成dist/index.html而实际文件在根目录。所以部署时要么把静态资源放在二进制同目录要么在代码里用process.cwd()显式拼绝对路径。这个细节搞错会让线上服务一直返回 404而且日志里看不出来原因——因为代码逻辑完全正确纯属运行时工作目录和代码预期不一致。把--compile和静态资源路径都验证通过之后这个全栈项目的交付形态就变得很干净一个二进制、一个 HTML、一个数据库文件拷到任何同架构 Linux 机器上都能跑。这也正是我一年多里反复对比 Node 和 Bun 之后愿意在新项目里优先选 Bun 的原因。希望这篇笔记帮到你省掉我在版本兼容和部署路径上折腾过的时间。本文还有配套的精品资源点击获取