ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Bun一站式全栈JS运行时:内置SQLite与Redis的实战指南

Bun一站式全栈JS运行时:内置SQLite与Redis的实战指南 最近这几天Bun 算是把前端圈子里外都炸了一遍。这个主打“一站式全栈 JS 运行时”的新版本把数据库、Redis 客户端、HTTP 服务器、打包器全给你塞进一个二进制文件里。以前我们用 Node 写个全栈小项目得先安 Express、配 nodemon、折腾 env、再装 mysql2 或者 ioredis前置准备工作长到劝退换成 Bun 之后bun init一把梭SQLite 开箱即用Redis 也是内置操作开发一个小工具的效率完全不是一个量级。如果你平时写 JavaScript / TypeScript又想省掉一大串依赖安装和配置这篇内容可以让你少踩很多坑。我会直接从 Bun 的核心定位讲起然后分模块拆解内置数据库、Redis 客户端、前端开发相关能力最后带一个完整的全栈项目实操把 Windows 和 macOS 下的安装、常见报错也一并说清楚。1. 为什么说 Bun 是“一站式全栈运行时”1.1 从 Node.js 的碎片化谈起先聊一个让我这类老 Node 用户很头疼的现状。Node 本身只是一个 JavaScript 运行时它只解决“代码怎么跑”的问题至于跑起来之后怎么处理请求、怎么连数据库、怎么读环境变量、怎么写 TypeScript都需要开发者自己去拼装。我前几年搭一个后端服务光是准备阶段就要装一堆东西用nodemon监听文件变更并自动重启用ts-node或者tsx来运行 TypeScript用dotenv加载.env配置用express或fastify起 HTTP 服务用mysql2/pg/ioredis连接外部存储用webpack或vite构建前端资源。理论上这些东西都没问题但它们彼此之间的版本兼容、配置项写法、不同操作系统的路径差异都会消耗大量精力。更别提从“本地跑通”到“部署上线”之间还夹着一个编译、构建、环境变量替换的过程。你写的是 JavaScript但整天在跟工具链搏斗这种感觉很不“JavaScript”。1.2 一个二进制解决整条链路Bun 的设计思路就和 Node 完全不一样它的目标是当一个足够自洽的工具。它把运行时、包管理器、打包器、测试运行器全部整合到一起而且新版还把数据库和 Redis 客户端也内建了。我实测下来最大的感受是新建项目只需要一个bun init后续写代码时基本不需要再为了“基础设施”分心。比如你要写一个带数据库的 APIBun 里可以直接这样启动import { Database } from bun:sqlite; const db new Database(app.db); db.run(CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)); const server Bun.serve({ port: 3000, async fetch(req) { const url new URL(req.url); if (url.pathname /users) { const users db.query(SELECT * FROM users).all(); return Response.json(users); } return new Response(Not Found, { status: 404 }); }, }); console.log(Server running on http://localhost:${server.port});这段代码一步到位内置 SQLite 数据库、内置 HTTP 服务器、原生支持 TypeScript没有任何额外的 import 来自 npm 包。这就是“一站式”的实际体验少装了十几个包少写了几百行脚手架代码。1.3 与 Node.js、Deno 的差异化定位有人可能会问那 Deno 不也很全能吗这里我想说一个更具体的差异——Bun 的思路是“兼容 内聚”而不是“重造 隔离”。Bun 底层用的是 JavaScriptCore 引擎而不是 Node 的 V8但它在 API 层面做了大量 Node.js 兼容。也就是说你现有的 npm 包大部分可以直接在 Bun 项目里跑bun install也能直接读取package.json和node_modules迁移成本比想象中低。Deno 更强调安全和标准库但很多 Node 生态的包仍然需要 polyfill 或者兼容层。对于想从 Node 平滑迁移、又想提升开发效率的团队Bun 上手几乎就是零负担。另外一个让我愿意换过去的原因是速度。Bun 的启动速度、依赖安装速度在本地开发时感知特别明显。它不是那种“快一点点”的优化而是真的能让bun install在几秒内完成HMR 响应也很快改完代码保存浏览器或者终端几乎是瞬时刷新。工具链不卡顿之后写代码的心情都不一样。2. 内置数据库bun:sqlite 的实用姿势2.1 零配置创建一个本地数据库Bun 内置的bun:sqlite模块本质上是一个同步 API 的 SQLite 数据库驱动不需要单独安装任何 npm 包。这里之所以给开发者带来非常大的便利是因为本地开发阶段很大一部分数据需求其实用一个单文件数据库就足够了。创建一个数据库就一行import { Database } from bun:sqlite; // 传入一个文件路径就会自动创建对应的数据库文件 const db new Database(mydb.sqlite); // 如果想要临时数据库可以传 :memory:数据只存在于内存中 const memoryDb new Database(:memory:);注意bun:sqlite默认是同步操作和 Node 里常见的异步数据库驱动不太一样。刚上手的人可能会担心“同步会不会阻塞”实际上 SQLite 本身就是嵌入式数据库绝大多数查询都是在毫秒级内的本地 I/O同步写起来反而让逻辑更容易追踪。如果你要做非常高频的并发写入服务后面我再聊替代方案。2.2 建表、增删改查与参数绑定我把最常用的一组操作写下来你可以直接复制到项目里跑import { Database } from bun:sqlite; const db new Database(app.db); // 建表 db.run( CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, email TEXT UNIQUE ) ); // 插入数据使用 ? 作为占位符避免 SQL 注入 const insert db.prepare(INSERT INTO users (name, email) VALUES (?, ?)); insert.run(张三, zhangsanexample.com); // 查询数据 const user db.query(SELECT * FROM users WHERE email ?).get(zhangsanexample.com); console.log(user); // 更新数据 db.run(UPDATE users SET name ? WHERE email ?, [李四, zhangsanexample.com]); // 删除数据 db.run(DELETE FROM users WHERE email ?, [zhangsanexample.com]); // 查询所有数据 const allUsers db.query(SELECT * FROM users).all(); console.log(allUsers);这里有几个细节值得提一下占位符统一用?Bun 的 SQLite 绑定和 Node 里的better-sqlite3很像都是?作为位置参数。如果你用过mysql2的?语法几乎可以无缝切换。.get()返回单行对象.all()返回数组.run()用于执行写操作。方法语义清晰不需要记忆什么execute、query的区别。参数绑定不要用字符串拼接比如INSERT INTO users (name) VALUES (${name})一旦name里出现单引号或者特殊字符轻则报错重则被 SQL 注入。Bun 的占位符绑定能直接规避这类问题。2.3 什么时候用内置 SQLite什么时候换 MySQL/PostgreSQL很多人会陷入一个纠结Bun 内置了 SQLite那我是不是不用部署 MySQL 了我个人建议是要看你的场景来区分。内置 SQLite 适合的场景个人工具、内部管理系统、自动化脚本原型验证阶段快速把业务逻辑跑通单机部署、低并发写入、数据量不大的应用比如个人博客的阅读统计、定时任务的结果落库、爬虫抓取数据的临时存储。单元测试环境用:memory:数据库可以极大简化测试数据准备。建议使用 MySQL/PostgreSQL 的场景需要多进程、多实例并发写入需要网络层面访问数据库需要更细粒度的权限控制、主从复制、数据备份方案数据量达到 GB 级别以上或查询复杂度过高。好在 Bun 并没有强迫你只用内置 SQLite。你依然可以bun add mysql2 pg然后在代码里正常连接外部数据库。只是对于很多轻量场景内置方案已经足够真的不需要一上来就上重型数据库。3. Redis 客户端内置缓存与队列不再依赖第三方库3.1 连接 Redis 并完成基础读写新版 Bun 另一个让我很兴奋的点是内置了 Redis 客户端。以前用 Node 写 Redis 操作多半要装ioredis或者redis并且要自己处理连接池、重连、超时。Bun 内置的客户端把这些细节收敛了连接和读写都走原生 API。基础用法如下import { Redis } from bun; const redis new Redis(process.env.REDIS_URL ?? redis://localhost:6379); // 写字符串 await redis.set(user:1:name, 张三); // 读字符串 const name await redis.get(user:1:name); console.log(name); // 设置过期时间单位秒 await redis.set(temp:code, 123456, EX, 300); // 删除键 await redis.del(temp:code); redis.close();这里new Redis(...)直接接受 Redis 连接串与市面上主流客户端的连接配置一致没有额外学习成本。set命令支持的EX参数可以直接用来设置过期时间这在做验证码、临时 Token、缓存击穿防护时非常常用。3.2 常用数据类型操作对比Redis 一共有五种基本数据类型Bun 内置客户端也完整覆盖了。我整理了一张速查表方便你打开这篇内容直接照着写数据类型常见命令Bun 调用示例适用场景StringSET, GET, SETEXawait redis.set(key, value, EX, 60)缓存、计数器、验证码HashHSET, HGET, HGETALLawait redis.hset(user:1, { name: 张三, age: 18 })存储对象字段ListLPUSH, RPUSH, LRANGEawait redis.lpush(queue, task1)消息队列、近期列表SetSADD, SISMEMBER, SMEMBERSawait redis.sadd(tags, js)去重、标签、关注关系Sorted SetZADD, ZRANGE, ZSCOREawait redis.zadd(rank, 100, player1)排行榜、延时队列我在实际项目中用 Hash 存用户资料、用 String 存接口缓存、用 List 做简单的任务队列基本覆盖了 90% 的业务场景。如果只是想做一个“轻量队列”不引入 RabbitMQ / KafkaBun 内置 Redis 客户端配合 List 的lpush/brpop就足够撑起一个小型异步任务系统。3.3 在 HTTP 服务里实现缓存策略以下是我很常写的一个“先查缓存、再回源数据库”的接口模式在 Bun 里实现起来非常顺滑import { Database } from bun:sqlite; import { Redis } from bun; const db new Database(app.db); const redis new Redis(redis://localhost:6379); const server Bun.serve({ port: 3000, async fetch(req) { const url new URL(req.url); if (url.pathname /user) { const id url.searchParams.get(id) ?? 1; const cacheKey user:${id}; // 1. 先查 Redis const cached await redis.get(cacheKey); if (cached) { return Response.json({ source: redis, data: JSON.parse(cached) }); } // 2. 缓存未命中查 SQLite const user db.query(SELECT * FROM users WHERE id ?).get(id); if (!user) { return Response.json({ error: User not found }, { status: 404 }); } // 3. 回源后写入缓存设置 60 秒过期 await redis.set(cacheKey, JSON.stringify(user), EX, 60); return Response.json({ source: database, data: user }); } return new Response(Not Found, { status: 404 }); }, }); console.log(Server running on http://localhost:${server.port});这段代码里有一个非常关键的逻辑回源之后必须写回缓存而且一定要带过期时间。如果不带过期时间脏数据会一直留在 Redis 里用户查到的信息永远是旧的。EX 60是我在大多数项目里的默认值既能缓解数据库压力又不会导致缓存内容长期不更新。缓存穿透、缓存击穿、缓存雪崩这三个经典问题在这里也有一个简单的应对思路穿透查数据库也不存在的数据可以在 Redis 里存一个空值并设置短过期比如 60 秒避免恶意请求反复打到数据库。击穿针对某一个热点 key在回源时加一个简单的互斥锁比如用setnx命令避免大量请求同时打到数据库。雪崩不要把缓存过期时间都设成同一个值可以在EX 60的基础上加一个随机秒数例如60 Math.floor(Math.random() * 30)分布过期时间。4. 前端开发的内建“武器库”4.1 内置 HTTP 服务器Bun.serve 是一个零依赖、零配置的 HTTP 服务器它取代了“Express node:http”的组合。它的 API 设计很像现代 Web 标准fetch函数接收原生Request返回Response完全走 Web 标准因此也很容易迁移到 Cloudflare Workers、Deno Deploy 等平台。一个最基础的例子const server Bun.serve({ port: 3000, fetch(request) { return new Response(Hello from Bun!); }, });和 Express 相比Bun.serve 不需要app.listen(3000)也不需要app.get()这种路由定义方式。多路由可以用一个if/else或者switch自己处理也可以手动抽一个极简路由函数。对于小项目这种自由度反而更清爽。它还支持 WebSocketconst server Bun.serve({ port: 3000, fetch(req, server) { if (server.upgrade(req)) return; return new Response(WebSocket only, { status: 400 }); }, websocket: { open(ws) { ws.send(Welcome to the chat!); }, message(ws, message) { ws.send(You said: ${message}); }, }, });在做实时推送、协作编辑、在线小游戏这类应用时内置 WebSocket 能让你少引入一个库。4.2 前端打包与构建Bun 还内置了一个打包器Bun.build。它可以把 TypeScript、JSX、CSS、图片资源统一打包成浏览器可运行的文件。最常用的场景是你不想为了一个简单的页面就引入 Vite也不想折腾 Webpack 配置。const result await Bun.build({ entrypoints: [./src/index.tsx], outdir: ./dist, minify: true, target: browser, }); if (!result.success) { console.error(result.logs); }这里的entrypoints和outdir是核心参数。target可以设置为browser或bun前者产物用于浏览器环境后者可以打包成在 Bun 运行时跑的服务端代码。如果你是 React 或 Vue 项目Bun.build 会自动识别 JSX 语法不需要额外配置 Babel。4.3 热更新与脚本运行效率本地开发时Bun 自带--hot参数文件保存后会做热替换。这里的“热”不只是简单重启而是尽量保留进程状态。实测下来React 组件改动后页面刷新速度相当可观基本感知不到等待。bun --hot run index.ts同时你还可以直接在命令行里跑单行 JSbun -e console.log(hello bun)对于快速验证某个 npm 包是否可用、试一段正则、算一个随机数这个命令非常好使。而bunx则对标npx可以直接运行未安装的 CLI 工具bunx prettier --write .说句大实话Bun 这个“速度”并不是玄学。它用了 JavaScriptCore 引擎启动开销小很多依赖安装又用了全局缓存和并发下载机制快得不像 JS 生态的传统体验。这部分你只要亲自跑一次马上就能感受到差别。5. 实操过程从零初始化一个全栈项目5.1 安装 BunmacOS / Linux / Windows不同操作系统的安装方式如下。如果你用的是 macOS 或 Linux官方推荐直接用安装脚本curl -fsSL https://bun.sh/install | bash需要注意脚本默认会安装到~/.bun/bin安装完成后可能需要手动把 Bun 加到 PATH。终端里执行export BUN_INSTALL$HOME/.bun export PATH$BUN_INSTALL/bin:$PATH如果你是 Windows 用户而且用的是 Win11最简单的方式其实是 npm 安装npm install -g bun或者用 PowerShell 安装脚本powershell -c irm bun.sh/install.ps1 | iex安装完成后验证一下版本bun --version如果输出了类似1.2.x的版本号说明安装成功了。我自己的经验是先把bun --version跑通再进入项目目录避免后面出现命令找不到的挫败感。提示Windows 下如果后续在终端里运行bun提示执行策略限制可以先用管理员身份打开 PowerShell执行Set-ExecutionPolicy RemoteSigned -Scope CurrentUser。这是 Windows 常见的脚本执行策略问题和 Bun 本身无关。5.2 初始化项目结构与依赖执行mkdir bun-demo cd bun-demo bun initbun init会询问一些基础问题比如入口文件、包名等如果不想交互式问答可以一路回车使用默认值。它最终会生成以下文件结构bun-demo/ ├── .gitignore ├── README.md ├── bun.lock ├── index.ts ├── package.json └── tsconfig.json值得注意的是Bun 会生成自己的bun.lock锁文件它比 npm 的package-lock.json更小解析也更快。团队协作时只要大家都用 Bun这个锁文件就能保持依赖一致性。5.3 一个带数据库、Redis、静态页面的全栈 Demo因为这篇文章的主角是“一站式”我直接写一个同时使用内置 SQLite、内置 Redis、内置 HTTP 服务器并且还能托管前端页面模板的完整示例。先把index.ts改成这样import { Database } from bun:sqlite; import { Redis } from bun; const db new Database(app.db); const redis new Redis(redis://localhost:6379); // 初始化数据表 db.run( CREATE TABLE IF NOT EXISTS posts ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, content TEXT NOT NULL, created_at TEXT DEFAULT CURRENT_TIMESTAMP ) ); // 造几条初始数据避免页面空白 const count db.query(SELECT COUNT(*) AS count FROM posts).get() as { count: number }; if (count.count 0) { const insert db.prepare(INSERT INTO posts (title, content) VALUES (?, ?)); insert.run(第一篇博客, 你好Bun); insert.run(第二篇博客, 内置数据库真方便。); insert.run(第三篇博客, Redis 缓存也安排上了。); } function renderPage(posts: Array{ id: number; title: string; content: string }) { const list posts .map( (post) li h3${post.title}/h3 p${post.content}/p /li ) .join(); return !DOCTYPE html html langzh-CN head meta charsetUTF-8 / titleBun 全栈 Demo/title style body { font-family: system-ui, sans-serif; max-width: 640px; margin: 40px auto; } li { border-bottom: 1px solid #eee; padding: 12px 0; } /style /head body h1Bun 全栈 Demo/h1 ul${list}/ul psmall缓存状态span idcache-status检查中.../span/small/p script fetch(/api/cache-status) .then((r) r.json()) .then((data) { document.getElementById(cache-status).textContent data.source; }); /script /body /html; } const server Bun.serve({ port: 3000, async fetch(req) { const url new URL(req.url); if (url.pathname /) { // 先查 Redis 缓存 const cacheKey posts:page; const cached await redis.get(cacheKey); if (cached) { return new Response(cached, { headers: { Content-Type: text/html; charsetutf-8 }, }); } // 没有缓存则查数据库 const posts db.query(SELECT id, title, content FROM posts ORDER BY id DESC).all() as Array{ id: number; title: string; content: string; }; const html renderPage(posts); // 写回 Redis缓存 30 秒 await redis.set(cacheKey, html, EX, 30); return new Response(html, { headers: { Content-Type: text/html; charsetutf-8 }, }); } if (url.pathname /api/cache-status) { // 这个接口用于在页面上展示缓存是来自 Redis 还是数据库 const cacheKey posts:page; const cached await redis.get(cacheKey); return Response.json({ source: cached ? redis : database }); } return new Response(Not Found, { status: 404 }); }, }); console.log(Bun fullstack server running at http://localhost:${server.port});运行bun run index.ts打开浏览器访问http://localhost:3000第一次看到页面后刷新一次观察右上角的缓存状态会变成redis。如果 30 秒内你手动改了数据库内容页面还是旧数据这就是缓存生效的表现等过期后再刷新就会重新回源读取。这个 Demo 麻雀虽小五脏俱全。它已经把内置 HTTP 服务、内置数据库、内置 Redis 客户端、HTML 模板渲染、前端简单脚本全部串起来了。你甚至可以在这个基础上扩展接口、加登录、加 WebSocket把它当成一个小型全栈应用的启动模板。6. 常见问题与排查技巧实录6.1 Windows 下安装或运行 Bun 报错我在 Win11 上实测最常见的报错有两种第一种是运行bun命令提示“无法识别”原因通常是 PATH 没有配置好。如果是通过 npm 安装全局 bin 目录一般已经在 PATH 里如果是通过脚本安装需要手动把%USERPROFILE%\.bun\bin加进系统环境变量。第二种是 PowerShell 执行策略限制。解决办法是用管理员身份运行Set-ExecutionPolicy RemoteSigned -Scope CurrentUser然后重启终端再执行bun --version。这个报错和 Bun 本身没有关系是 Windows 对脚本执行的安全策略。6.2 内置 SQLite 文件读写问题bun:sqlite在创建数据库文件时默认相对当前工作目录写入。比如你在项目根目录运行bun run index.ts在代码里写new Database(app.db)那么app.db就会出现在项目根目录。我踩过一个坑部署时把整个项目目录设置为只读结果运行时报 “unable to open database file”。排查思路是确认两点数据库文件所在目录是否有写权限是否有其他进程占用了该文件比如用可视化工具打开了同一个 SQLite 数据库导致锁冲突。如果只是临时验证逻辑可以用:memory:数据库这样不落盘、无权限问题但缺点就是数据进程结束后会丢失。6.3 Redis 连接失败时怎么排查Bun 内置 Redis 客户端需要一个已经运行的 Redis 服务。如果连接不上先按顺序检查redis-cli ping如果返回PONG说明 Redis 服务本身正常。接下来检查连接串是否正确尤其是密码、端口、宿主这几个维度的配置。比如远程 Redis 必须确认是否开启了保护模式或者是否允许当前 IP 访问。一个很容易忽略的问题Redis 默认只监听本地回环地址127.0.0.1。如果你在 Docker 容器里跑 Bun 应用容器内访问宿主机的 Redis 不能用localhost需要换成宿主机在 Docker 网络中的 IP或者用host.docker.internal。6.4 与其他 JS 运行时混用的注意事项Bun 对 Node.js API 做了相当大的兼容但并不是 100%。某些依赖了 Node.js 原生模块的 npm 包可能会出问题比如bcrypt、sharp这类包含 C/C 扩展的包可能在 Bun 环境下需要预编译二进制兼容。碰到这类包我的建议是先在项目里跑一下bun test或者启动服务确认是否有明确的兼容性报错如果某个包无法工作优先搜索它的官方文档里有没有 Bun 支持说明现在很多主流库已经为 Bun 做了适配实在不行再用 Node 作为兜底运行时项目代码本身保持跨运行时兼容即可。我实际使用中的体会是Bun 目前的定位更像“大多数人大多数场景下的默认选择”而不是“所有场景的唯一选择”。遇到兼容性问题时不要死磕换个思路往往能更快解决问题。最后再分享一个小技巧如果你在 Bun 项目里写 TypeScripttsconfig.json其实可以非常精简。Bun 原生支持 TypeScript 运行不需要moduleResolution改来改去也不需要在执行前先编译一把。把jsx设为react-jsx然后放心大胆地直接写.ts/.tsx文件Bun 跑起来会自动处理。这个特性平时看不出来等你经历过 Node 生态那套“写代码两分钟、配编译环境两小时”的流程以后就会明白它到底省了多少事。
返回列表