Bun.js + Elysia 框架实现基于 SQLITE3 的简单 CURD 后端服务

🤔 关于 Bun.js

为什么是 Bun.js?

以前我用 JavaScript 写后端,用的组合拳是 Node.js + Fastify + better-sqlite3,用起来还是非常顺手的。但是会出现一些问题:

  1. better-sqlite3 需要构建为 .node 二进制文件,与操作系统和 CPU 架构绑定。例如,Windows 下生成的 .node 文件无法在 Linux 或 macOS 上运行;
  2. 在 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 的兼容性

  1. Bun 明确表示其目标是成为 Node.js 的「可替换」运行时,并且在官网强调它在 "Node-style 模块解析、全局变量(例如 process, Buffer)及核心模块(如 fs, path, http)" 方面已有广泛支持;
  2. 对于 Node-API (原生扩展接口,即 N-API) 的支持也已经达到较高水平------文档提到 Bun 实现了约 95% 的 Node-API 接口,从而大部分用来构建 Node 原生模块(*.node 文件)在 Bun 上"开箱即用";
  3. 在许多主流基于 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)}`))
}
相关推荐
2501_938773996 小时前
Objective-C 类的归档与解档:NSCoding 协议实现对象持久化存储
开发语言·ios·objective-c
无敌最俊朗@6 小时前
SQlite:电影院售票系统中的主键(单列,复合)约束应用
java·开发语言·数据库
今日说"法"6 小时前
Rust 代码审查清单:从安全到性能的关键校验
开发语言·安全·rust
wydaicls7 小时前
C语言 了解一下回调函数(钩子函数)的使用
c语言·开发语言
im_AMBER7 小时前
JavaScript 03 【基础语法学习】
javascript·笔记·学习
java1234_小锋7 小时前
PyTorch2 Python深度学习 - 数据集与数据加载
开发语言·python·深度学习·pytorch2
千码君20167 小时前
Go语言:常量计数器iota的意义
开发语言·后端·golang·状态码·const·iota·常量
永远有缘7 小时前
四种编程语言常用函数对比表
java·开发语言·c++·python
Pocker_Spades_A7 小时前
Python快速入门专业版(五十三):Python程序调试进阶:PyCharm调试工具(可视化断点与变量监控)
开发语言·python·pycharm