
🤔 关于 Bun.js
为什么是 Bun.js?
以前我用 JavaScript 写后端,用的组合拳是 Node.js + Fastify + better-sqlite3,用起来还是非常顺手的。但是会出现一些问题:
- better-sqlite3 需要构建为
.node二进制文件,与操作系统和 CPU 架构绑定。例如,Windows 下生成的 .node 文件无法在 Linux 或 macOS 上运行; - 在 macOS 下构建上述文件时会出错(node 版本
22.x,better-sqlite3 版本11.x/12.x)。
Bun.js 自1.2.21版本开始自带 sqlite3 模块,非常方便👍,同时 Bun.js 带来了比 node.js 更高的性能,还自带绝绝子的包管理器(代替 pnpm)以及看不到车尾灯的构建速度(代替 webpack/vite 等),于是我决定投入 Bun.js 的怀抱。
还有一个重要的原因是,node.js 启用 module 模式后,引入其他js文件路径填写非常严格,比如:
js
// a.js
export const add = (x,y)=> x+y
// b.js
import { add } from './a.js'
// 不能写成 import { add } from './a'
// 也不支持自动引入目录下的 index.js
// 上述都是 commonjs 模式下非常实用的引入机制😄
如何兼容 commonjs 引入机制
我们可以编写自定义 loader:
js
// commonjs-loader.js
import { register } from "node:module";
import { fileURLToPath } from 'url';
import { existsSync } from 'fs';
import { dirname, join } from 'path';
// 拦截模块解析的钩子
async function resolve(specifier, context, defaultResolve) {
// specifier 是导入路径(如 "./a")
// context 包含父模块路径等信息
const { parentURL } = context;
// 仅处理相对路径(以 ./ 或 ../ 开头),避免影响第三方模块
if (specifier.startsWith('./') || specifier.startsWith('../')) {
// 将父模块的 URL 转为文件路径
const parentPath = fileURLToPath(parentURL);
const parentDir = dirname(parentPath);
// 拼接可能的文件路径(尝试添加 .js)
const candidatePath = join(parentDir, specifier + '.js');
// 如果带 .js 的文件存在,则修改 specifier 为带后缀的路径
if (existsSync(candidatePath)) {
specifier += '.js';
}
// 如果 index.js 文件存在
else if(existsSync(join(parentDir, specifier, "index.js"))){
specifier += '/index.js'
}
}
// 调用默认解析逻辑
return defaultResolve(specifier, context, defaultResolve);
}
// 注册当前模块为 loader
register(new URL(import.meta.url), { parentURL: import.meta.url });
export { resolve };
然后按照 node --import ./commonjs-loader.js src/app.js 的启动方式即可。
1.3 版本大更新
Bun.js 在 2025年10月10日发布了1.3.0版本,从 "高性能 JS 运行时" 升级为 "一站式全栈开发解决方案",不仅原生支持前端开发全流程(热重载、打包构建),还新增了 MySQL 客户端、Redis 客户端等企业级工具,同时大幅提升 Node.js 兼容性。
这就意味着,我们可以使用 Bun.js 来开发前端项目了。
1.3.0 的小 BUG
2025-10-11 发布的1.3.0版本,在 windows 下无法正常执行脚本(package.json 定义),详见:Bun.js cannot run scripts correctly on Windows 11。

该问题在1.3.1中已修复👍。
与 Node.js 的兼容性
- Bun 明确表示其目标是成为 Node.js 的「可替换」运行时,并且在官网强调它在 "Node-style 模块解析、全局变量(例如
process,Buffer)及核心模块(如fs,path,http)" 方面已有广泛支持; - 对于 Node-API (原生扩展接口,即 N-API) 的支持也已经达到较高水平------文档提到 Bun 实现了约 95% 的 Node-API 接口,从而大部分用来构建 Node 原生模块(*.node 文件)在 Bun 上"开箱即用";
- 在许多主流基于 Node 的项目/框架(比如 Express、Next.js)上,Bun 已经能较好运行,迁移成本低。
我原本的代码可以直接迁移到 Bun.js 环境下运行,十分省心😄。
我的开发方式
使用 Bun.js 作为包管理器及打包工具,按需配合 vite/webpack 完成前端开发。
🛴 CURD 应用
这是一个简单的用户管理演示,包含以下接口:
- /create:创建新用户
- /delete:删除指定用户
- /query?id=:查询指定用户
- /all:列出全部用户
js
import { Elysia } from "elysia";
import { Database } from "bun:sqlite";
// 初始化 SQLite 数据库(文件自动创建)
const db = new Database("user.db");
// 创建 user 表(若不存在)
db.run(`
CREATE TABLE IF NOT EXISTS user (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
phone TEXT NOT NULL
);
`);
// 初始化 Elysia 应用
const app = new Elysia()
// 创建用户
.post("/create", async ({ body }) => {
const { name, phone } = body;
if (!name || !phone) {
return { code: 400, msg: "缺少参数 name 或 phone" };
}
const stmt = db.prepare("INSERT INTO user (name, phone) VALUES (?, ?)");
const result = stmt.run(name, phone);
return { code: 200, msg: "创建成功", id: result.lastInsertRowid };
})
// 删除用户
.get("/delete", ({ query }) => {
const id = Number(query.id);
if (!id) return { code: 400, msg: "缺少参数 id" };
const stmt = db.prepare("DELETE FROM user WHERE id = ?");
const result = stmt.run(id);
if (result.changes === 0) {
return { code: 404, msg: "未找到该用户" };
}
return { code: 200, msg: "删除成功" };
})
// 查询用户
.get("/query", ({ query }) => {
const id = Number(query.id);
if (!id) return { code: 400, msg: "缺少参数 id" };
const stmt = db.prepare("SELECT * FROM user WHERE id = ?");
const user = stmt.get(id);
if (!user) {
return { code: 404, msg: "未找到该用户" };
}
return { code: 200, data: user };
})
// 列出所有用户
.get("/list", () => {
const stmt = db.prepare("SELECT * FROM user ORDER BY id DESC");
const users = stmt.all();
return { code: 200, data: users };
})
// 监听端口
.listen(9000, v=>{
console.debug("LISTEN CALLBACK", v)
});
console.log(`✅ Elysia app running at http://localhost:9000`);
Bun.js SQLite3 简单封装
js
import { Database } from 'bun:sqlite'
import logger from './common/logger'
/**@type {Database} */
let db = undefined
export const setupDB = ()=>{
if(db == undefined){
db = new Database("db.file")
}
}
/**
* 遍历 tables 的定义语句进行建表
* @param {Array<String>} tables - 建表语句合集
*/
export const initDB = tables =>{
setupDB()
const regex = /CREATE TABLE IF NOT EXISTS\s+(\w+)\s*\(/i
for(let table of tables){
let m = table.match(regex)
if(m && m[1]){
db.run(table)
}
}
}
/**
* 数据库执行方法枚举
* @typedef {'run' | 'all' | 'get'} DBMethod
*
*
* @param {DBMethod} method
* @param {String} sql
* @param {...any} ps
* @returns
*/
const withDB = (method, sql, ...ps)=>{
let stmt = db.prepare(sql)
return stmt[method](...ps)
}
export const exec = (sql, ...ps)=> withDB('run', sql, ...ps)
export const query = (sql, ...ps)=> withDB('all', sql, ...ps)
export const findByID = (table, id, idField='id')=> {
return withDB('get', `SELECT * FROM ${table} WHERE ${idField}=?`, id)
}
export const delByID = (table, id, idField='id')=> withDB('run', `DELETE FROM ${table} WHERE ${idField}=?`, id)
export const findFirst = (sql, ...ps)=> withDB('get', sql, ...ps)
/**
* 获取指定条件的数据行数
* @param {String} table
* @param {String} condition
* @param {...any} ps
* @returns {Number}
*/
export const count = (table, condition, ...ps)=> {
let obj = withDB('get', `SELECT count(*) FROM ${table} WHERE ${condition}`, ...ps)
return obj['count(*)']
}
/**
*
* @param {String} table
* @param {Object} bean
* @param {Array<String>} ignores
*/
export const insertNew = (table, bean, ignores=[])=>{
let fields = Object.keys(bean)
if(ignores && ignores.length)
fields = fields.filter(f=>!ignores.includes(f))
return exec(`INSERT INTO ${table} (${fields.map(f=>f).join(",")}) VALUES (${fields.map(()=>"?").join(",")});`, fields.map(f=>bean[f]))
}
📚 附录
windows 下更新 Bun.js
shell
# 官网是推荐使用 bun upgrade 升级
# 可是我在 windows 下通过 cmd 执行会出现长时间的卡顿而导致无法升级
# 最后还是通过重新安装的方式完成升级😔
powershell -c "irm bun.sh/install.ps1 | iex"
Bun.js 如何判断环境
js
await Bun.build({
define:{
"Bun.env.NODE_ENV": JSON.stringify("production"),
"process.env.NODE_ENV": JSON.stringify("production")
}
})
通过上述方案打包后的文件,可获取process.env.NODE_ENV的值用于判断当前运行环境。
打包
经过一番摸索,我整理了一份简单实用的打包脚本,仅供参考。
将下方代码保存到 build.js 文件,然后执行 bun build.js 即可。
js
# build.js
import { formatFileSize } from "./src/common/tool"
import pc from 'picocolors'
const VERSION = ()=>{
let now = new Date
return `v${now.getUTCFullYear() - 2000}.${now.getUTCMonth() + 1}.${now.getUTCDate()}`
}
const ENV = "production"
const started = Date.now()
const result = await Bun.build({
entrypoints:["./src/server.js"],
minify: true,
outdir:"./dist",
naming:"[dir]/ai-naming.js",
target: 'bun',
env: 'disable',
define:{
"APP_VERSION": JSON.stringify(VERSION()),
"Bun.env.NODE_ENV": JSON.stringify(ENV),
"process.env.NODE_ENV": JSON.stringify(ENV)
}
})
if(!result.success){
console.debug(`Build fail:`, result.logs)
process.exit(-1)
}
let cwd = process.cwd()
console.debug(pc.green(`\n✅ 构建完成(env=${ENV}),耗时 ${Date.now()-started} ms\n`))
for(let item of result.outputs){
console.debug(pc.cyan(`${item.path.replace(cwd, " ")}\t${item.hash}\t${formatFileSize(item.size)}`))
}
