ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Bun v1.3全栈JS运行时:内置SQLite、WebSocket与JSX渲染

Bun v1.3全栈JS运行时:内置SQLite、WebSocket与JSX渲染

简介:Bun v1.3 全栈 JavaScript 运行时源码演示包围绕核心特性展开,面向需要在前端、后端及数据层统一工具链的全栈开发者,旨在解决频繁切换构建工具、数据库驱动和 Redis 客户端带来的效率损耗,让开发更聚焦于业务逻辑。包体仅为 11KB,zip 压缩后共 5 个文件:3 个 HTML 示例页面用于演示 Bun 对前端热重载与生产构建的原生支持,1 个 inscode 在线运行配置便于快速启动调试,1 个 .gitignore 文件规范版本管理。当前已有 127 人浏览学习该资源。示例中包含内置 MySQL、PostgreSQL、SQLite 客户端的连接与查询代码,以及吞吐量可达 250 万次/秒的 Redis 客户端调用,可直观对比 ioredis 约 7.9 倍的性能提升;同时涵盖 WebSocket 优化、包管理与测试调试增强的代码片段,并展示 Node.js 兼容性改动。适合全栈团队用作 Bun 功能验证与入门脚手架,也适合评估 Bun 替代现有 Node.js 技术栈的可行性。

1. Bun v1.3 全栈 JS 运行时:一个前端也能直接写服务端的运行时

如果你平时用 Node 写接口、又要用 Vite 跑前端,还要为脚本单配一个解释器,来回切换工具链的时间可能比写业务还长。Bun v1.3 把这件事收拢成了一个二进制:内置打包器、测试器、原生 SQLite、密码学 API,还直接支持用 JSX 写 HTML 服务端渲染,前端工程师上手成本极低。这个版本值得关注的点不只是“更快”,而是它把全栈开发里最常见的几条链路——启动服务、跑脚本、解析请求、渲染页面、操作数据库——全部收敛进一个运行时里,命令和参数比 Node 生态更简洁。适合两类人:想减少开发环境依赖的独立开发者,以及想用一套语言维护前后端的团队。

2. 先立住概念:Bun 到底解决了什么,以及 v1.3 的关键能力边界

2.1 不只是“快”:Bun 的运行时定位与适用边界

很多人第一次接触 Bun 是因为它的启动速度比 Node 快一个量级。基准测试里,一个空脚本在 Node 下要几十毫秒,Bun 通常个位数。但真正支撑“全栈”这个说法的,是它把开发链路里的多个工具合并成了一个运行时进程:Bun 可以当包管理器(bun install),可以当打包器(bun build),可以当测试器(bun test),也可以当服务端(Bun.serve)。v1.3 在这个基础上补齐了日常全栈开发里最容易卡壳的几块:原生支持读取 .env 文件、内置 SQLite 驱动、强化了 WebSocket 的稳定性,以及对 JSX/TSX 的零配置支持。

这也意味着一个常见误用需要澄清:Bun 并不是“Node 的替代品”这种简单说法。它实现了 Node 兼容层,能跑大部分 npm 包,但原生模块和某些依赖 Node 内建 API 的包仍可能表现不一致。我的建议是:新项目直接用 Bun,老项目迁移前先在 CI 里跑一遍测试。v1.3 的兼容性比早期版本好很多,但“能跑”和“全部特性都对齐”之间还有距离,尤其是涉及 worker_threads 和 cluster 这类底层模块时,它的实现和 Node 并不完全一样。

2.2 全栈能力盘点:Bun.serve、SQLite、JSX 渲染与文件读写

v1.3 全栈开发的核心组合拳是三件套:Bun.serve 提供 HTTP 服务和 WebSocket,bun:sqlite 提供数据库操作,JSX 或 HTML 字面量提供页面渲染。再加上 Bun.file 和 Bun.write 这对文件读写 API,一个最小的全栈闭环就不再需要额外引入 Express、Knex 或模板引擎。

常见做法是,生产环境里用 Bun.serve 的 routes 配置直接定义 REST 接口,数据层用 bun:sqlite 同步读写,页面层用 TSX 组件返回 HTML 字符串。这套组合最大的价值是依赖极少,一个服务端文件可能只需要 import 三个东西。对于中小型项目,这种精简能显著降低部署和调试成本,尤其是服务器内存有限时,单进程比“Node + 数据库 + 前端静态服务”三进程省得多。

2.3 v1.3 版本升级里最值得关注的 4 个变化

结合 v1.3 发布说明,我实际体验后认为最值得关注的四个变化分别是:

  • .env 文件原生支持:此前需要手动加载或者靠命令行参数,现在 Bun 会自动读取项目根目录的 .env,无需任何配置。
  • bun init 交互式初始化:新项目脚手架更友好,可以直接生成 package.json、tsconfig.json 和入口文件。
  • Bun.serve 路由改进:支持更灵活的路由匹配,路径参数获取方式更直观。
  • 原生 SQLite 性能提升:读写链路做了优化,批量插入场景比早期版本有明显提升。

另外,v1.3 对 TypeScript 的支持也更完整了,类型检查可以交给 tsc,运行时不再需要单独编译。这一点对全栈项目尤其重要,因为前端用 TS 写组件、后端也用 TS 写接口,最终都由 Bun 直接执行,省去了 ts-node、tsx 这些中间层。

3. 本地环境搭建与最小全栈命令跑通

3.1 安装 Bun v1.3:macOS、Linux、Windows 三平台差异

安装 Bun 最常见的方式是使用官方安装脚本,Windows 用户建议优先用 WSL 或者 PowerShell 安装脚本,因为 Bun 在 Windows 上的原生支持目前仍是实验性质量,多数全栈开发场景在 WSL 里跑更稳。

# macOS / Linux curl -fsSL https://bun.sh/install | bash # Windows PowerShell(管理员模式) powershell -c "irm bun.sh/install.ps1 | iex" # 验证版本 bun --version

安装完成后,命令行工具会默认写入用户目录的 .bun/bin,需要确保该目录在 PATH 中。macOS 用户如果之前用过 Homebrew 装过旧版 Bun,建议先卸载再安装,避免出现两个版本互相覆盖。Windows 用户我一般会额外执行一次 bun upgrade 确认版本号是最新的 v1.3.x,因为安装脚本有时会拿到缓存版本。

验证是否安装成功有几个维度:bun --version 输出版本号、bun --help 能看到子命令列表、bun install 能正常解析 package.json。如果 version 输出空或者报找不到命令,绝大多数情况是 PATH 没配好,检查 ~/.bashrc 或 ~/.zshrc 里是否有导出 bun 路径的语句。

3.2 用 bun init 搭建一个前后端同目录的全栈项目

Bun v1.3 的 bun init 命令会引导你创建一个最小可运行项目,生成的目录结构包含 package.json、tsconfig.json 和一个 index.ts 入口。对于全栈项目,我通常会在初始化后再补两个目录:src/client 放前端页面代码,src/server 放后端接口代码。下面是完整初始化流程:

mkdir bun-fullstack-demo cd bun-fullstack-demo bun init -y mkdir -p src/client src/server echo 'console.log("hello bun")' > src/server/index.ts bun run src/server/index.ts

这段命令做的事情是创建一个空项目、以默认参数初始化、然后手动建立前后端目录分层。bun init 默认入口是 index.ts,如果你打算把服务端和前端分开跑,建议把 package.json 里的 scripts 改一下,分别声明 dev:server 和 dev:client。另外需要注意,bun init 生成的 package.json 里 "module" 和 "type" 字段可能和你之前 Node 项目的习惯不一致,如果遇到导入报错,检查一下 type 是否为 module。

3.3 第一个 HTTP 服务:Bun.serve 的最小可运行代码

Bun.serve 是 Bun 内置的 HTTP 服务器,不需要引入第三方框架。v1.3 里路由写法更简洁,直接传一个 routes 对象就能处理不同路径的请求:

// src/server/index.ts const server = Bun.serve({ port: 3000, routes: { "/": () => new Response("Hello from Bun"), "/api/health": () => Response.json({ status: "ok" }), "/api/user/:id": (req) => { const id = req.params.id; // 路径参数直接取 return Response.json({ userId: id }); }, }, }); console.log(`Server running at http://localhost:${server.port}`);

这里 routes 的关键点在于,路径参数用 :id 声明,回调里通过 req.params.id 获取,不用再手动解析 URL。更复杂的 POST 请求需要读取请求体,Bun.serve 的回调参数是标准的 Request 对象,可以直接用 await req.json() 拿 JSON 数据。生产环境里如果想支持静态文件,常见做法是用 Bun.file 读取文件内容返回给客户端,Bun 会根据文件扩展名自动设置 Content-Type。

// 静态文件示例:返回前端页面 const home = Bun.file("./src/client/index.html"); return new Response(home);

这样写的好处是省掉了单独配静态服务器或者 Nginx 的成本。但注意生产环境建议加缓存头,否则每个请求都读磁盘,性能不如专业静态服务器。这个取舍在后续避坑章节会展开。

4. 数据库与接口联动:用 bun:sqlite 和 WebSocket 做全栈核心链路

4.1 原生 SQLite 接入:建表、插入与查询的完整代码

全栈项目最常见的存储方案是 SQLite,Bun 内置的 bun:sqlite 模块不需要安装任何依赖,直接 import 就能用。它提供的是同步 API,写起来比回调或异步数据库驱动更直观,适合中小项目的读写量。

// src/server/db.ts import { Database } from "bun:sqlite"; const db = new Database("./data/app.db"); // 建表 db.run(` CREATE TABLE IF NOT EXISTS todos ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, done INTEGER DEFAULT 0, created_at TEXT DEFAULT CURRENT_TIMESTAMP ) `); // 插入数据 const insert = db.query("INSERT INTO todos (title) VALUES (?) RETURNING id"); const result = insert.get("写一篇 Bun 全栈笔记"); console.log("新增记录 ID:", result.id); // 查询 const select = db.query("SELECT * FROM todos ORDER BY created_at DESC"); const rows = select.all(); console.log(rows);

这里有两个地方值得说明。第一,db.query 返回的是一个可复用的语句对象,重复执行时性能优于每次 db.run 拼接 SQL。第二,RETURNING id 是 SQLite 3.35 以上版本支持的语法,Bun 内置的 SQLite 版本较新,所以可以直接用。如果项目里还有其他历史数据库文件,建议用 PRAGMA journal_mode = WAL 提升并发读写性能,Bun 同样支持通过 db.exec 执行 PRAGMA。

4.2 把 SQLite 封装成 REST 接口:请求、参数化查询与错误处理

有了数据库层,下一步就是把它接到 HTTP 接口上。这里我以 todo 应用为例,提供一组完整的增删改查接口。注意 SQL 注入预防和类型转换这两个细节,它们决定了接口能不能安全上生产。

// src/server/todo.ts import { Database } from "bun:sqlite"; const db = new Database("./data/app.db"); const server = Bun.serve({ port: 3000, routes: { "GET /api/todos": () => { const todos = db.query("SELECT * FROM todos ORDER BY id DESC").all(); return Response.json(todos); }, "POST /api/todos": async (req) => { const body = await req.json(); // 参数校验:避免脏数据写入 if (!body.title || typeof body.title !== "string") { return Response.json({ error: "title is required" }, { status: 400 }); } const result = db.query("INSERT INTO todos (title) VALUES (?) RETURNING id") .get(body.title); return Response.json(result, { status: 201 }); }, "PATCH /api/todos/:id": async (req) => { const id = Number(req.params.id); const body = await req.json(); db.query("UPDATE todos SET done = ? WHERE id = ?").run(body.done ? 1 : 0, id); return Response.json({ ok: true }); }, "DELETE /api/todos/:id": (req) => { const id = Number(req.params.id); db.query("DELETE FROM todos WHERE id = ?").run(id); return Response.json({ ok: true }); }, }, }); console.log(`API server: http://localhost:${server.port}`);

这种 routes 写法里,HTTP 方法用字符串前缀声明,不用在每个回调里做 if 判断,代码结构更清晰。路径参数转 Number 后要注意 NaN 的情况,否则 SQL 语句里会传一个非法 ID。我在实际项目里习惯于写一个 parseInt 的辅助函数,统一处理参数转换和默认值。另外,PATCH 接口的 body.done 判断,前端可能传布尔值也可能传 0/1,后端统一转换一次可以避免脏数据进库。

4.3 WebSocket 全栈联动:服务端推送与前端订阅的最小示例

全栈应用里 WebSocket 是高频场景,Bun.serve 对 WebSocket 的支持在 v1.3 里变得更稳定。实现思路是:服务端在同一个服务器上同时监听 HTTP 和 WebSocket,浏览器通过 ws:// 协议连接,服务端主动推送消息。

// src/server/ws.ts const server = Bun.serve({ port: 3000, fetch(req, server) { // 协议升级判断 if (server.upgrade(req)) { return; // 交给 websocket handler } return new Response("Not a websocket request"); }, websocket: { open(ws) { ws.send("连接成功,欢迎进入全栈演示"); }, message(ws, message) { // 收到前端消息,广播给所有连接 const text = message.toString(); server.publish("room", text); }, close(ws) { console.log("连接关闭"); }, }, }); // 服务端定时推送示例 setInterval(() => { server.publish("room", `当前时间: ${new Date().toISOString()}`); }, 5000);

注意这里 server.upgrade(req) 的调用方式,它会把请求升级为 WebSocket 通信,然后交给 websocket 处理器。server.publish 是向所有订阅了 "room" 频道的客户端广播消息,前端订阅的方式是在 WebSocket 的 open 回调里发送一个特殊消息,或者服务端在 open 事件里直接调用 ws.subscribe("room")。为了避免新手踩坑:如果记得要同时支持普通 HTTP 和 WebSocket,fetch 回调里的返回值顺序要注意,upgrade 成功后不能 return Response 对象,否则会导致握手失败。

5. 前端的最后一公里:用 Bun 直接渲染页面并接入交互

5.1 JSX 服务端渲染:一个返回 HTML 的 TSX 文件

全栈不能只有接口,页面也得能渲染。Bun 的一个特色就是原生支持 JSX/TSX,服务端可以直接写组件然后输出 HTML 字符串,省去了单独搭前端模板引擎的步骤。下面是完整示例:

// src/server/page.tsx function Layout({ title, children }: { title: string; children: any }) { return ( <html> <head><title>{title}</title></head> <body>{children}</body> </html> ); } function TodoList({ items }: { items: { id: number; title: string; done: number }[] }) { return ( <ul> {items.map(todo => ( <li key={todo.id} style={{ textDecoration: todo.done ? "line-through" : "none" }}> {todo.title} </li> ))} </ul> ); } const server = Bun.serve({ port: 3000, routes: { "/": async () => { const items = await readTodos(); // 复用前文 db 查询 const html = "<Layout><TodoList items={items} /></Layout>"; return new Response(html, { headers: { "Content-Type": "text/html" }, }); }, }, });

TSX 文件需要 .tsx 后缀,Bun 会直接编译执行,不需要额外配置。上面代码里的 和 实际上会被编译成函数调用,最终拼接成 HTML 字符串。注意 style 属性在服务端渲染时接受的是对象,Bun 的这个实现保持了和 React 一致的属性映射规则。如果你对这个过程感到陌生,可以先从一个最简单的

开始,确认返回的 HTML 在浏览器里正常显示后再逐步加上组件嵌套。

5.2 静态资源与前端脚本:不用配第二个服务器的方案

页面渲染出来后,通常还需要配套的 CSS 和 JavaScript。Bun 可以用一行代码实现静态目录映射,把 src/client 目录下的文件作为静态资源供前端引用。

import { serveStatic } from "bun"; const server = Bun.serve({ port: 3000, routes: { "/": renderHomePage(), "/static/*": serveStatic({ directory: "./src/client" }), }, });

serveStatic 是 Bun 为静态文件提供的内置路由处理,后面的路由规则 /static/* 表示所有以 /static/ 开头的请求都去 src/client 目录下找文件。常见的坑是目录写错,运行时会直接返回 404,建议先用一个简单的绝对路径确认文件存在再调参数。另外,前端脚本如果是模块化依赖,可以直接在 HTML 里用

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进