在这里插入图片描述

设计思考

一、为什么做这个功能

2024 年 Bun 1.0 发布后,前端工程师手里多了一把瑞士军刀——它同时是包管理器、打包器、打包器,还是一台高性能 HTTP 服务器。跟 Node.js 比,Bun 开箱支持 TypeScript、启动快 4 倍、内置 SQLite,做全栈项目不用再拼凑一堆工具链。

另一方面,AI 大模型应用越来越热,"怎么把 LLM 接入自己的项目"成了高频话题。但大部分教程要么只讲调 Prompt,要么一笔带过 API 调用,从搭服务器到接 AI 的中间地带几乎没人讲清楚

所以我从零搭了一个 Bun + TS 的 RESTful TodoList,作为地基。然后基于这个地基,去推演"如果要做成 AI 问答系统,会踩哪些坑、怎么设计"。这篇文章就把这条线完整走一遍。

目标:用 Bun 这把新刀,从最简单的增删查改开始,一路推到 AI 接入的工程设计。


二、目标:这个模块要解决什么问题

分两层来看:

层次 目标 解决的问题
已实现:地基 Bun + TS RESTful API 搭一个能跑的后端,理解接口、类型、异步
推演:进化 AI 问答系统工程设计 文件上传、安全、日志、超时等工程问题怎么思考

地基阶段交付的是一个经典的 TodoList——提供任务列表查询和详情查询。代码约 50 行,但覆盖了 Bun 服务器、TypeScript 类型约束、RESTful 路由、CORS 跨域、前后端异步通信等核心知识点。


三、设计:前端、后端怎么设计

3.1 整体架构——当前实现
┌──────────────┐   HTTP GET /todos   ┌───────────────┐
│   Browser    │ ◄─────────────────► │  Bun Server    │
│  index.html  │   JSON 响应          │  server.ts     │
│  fetch API   │                     │  port: 8080    │
└──────────────┘                     └───────────────┘

非常简单:浏览器通过 fetch 发 GET 请求,Bun 服务器返回 JSON 数据,前端渲染成列表。没有数据库——数据存在内存数组里,服务重启就没了,但对理解 HTTP 交互来说够用。

3.2 后端设计:一切皆资源

RESTful 的核心思想很简单:URL 定位资源(名词),HTTP 动词表达操作。就像快递员看地址送货——/todos 是一个仓库地址,GET 是"取货",POST 是"存货"。

把路由想象成警察分派案件:不同的"报案"(URL),分给不同的"辖区"(路由处理函数),各管各的,互不干扰。

当前项目的资源定义:

资源 URL 方法 说明
任务列表 /todos GET 获取全部任务
单个任务 /todos/1 GET 获取 id 为 1 的任务详情

这里用到了 TypeScript 的 interface——可以说它是整个后端设计的基石。

面向对象编程有三大支柱:封装、继承、多态。而 interface(接口) 是这三者的"合同书"——它声明一个对象必须有哪些属性和方法,但不关心具体怎么实现。抽象类负责"满足合同条款",具体类负责"干活"。

interface  →  约定(合同)
    ↓
abstract class → 满足约定(框架)
    ↓
class → 具体实现(干活)

从"面向对象编程"走到"面向接口编程",这是设计模式的第一步。 比如你写了一个 interface Todo,后面不管数据来自内存数组、SQLite、还是远端 API,只要满足这个接口,上层代码一行不用改——插拔式替换

项目中的核心类型:

// 一个任务就是一个资源,interface 约定了这个资源的"形状"
interface Todo {
    id: string;         // 身份证号,唯一标识
    title: string;      // 任务名
    completed: boolean; // 做完没有
    createdAt: Date;    // 什么时候建的
}

数据目前硬编码在数组里:

const todos: Todo[] = [
    { id: "1", title: "吃饭", completed: false, createdAt: new Date() },
    { id: "2", title: "睡觉", completed: false, createdAt: new Date() },
    { id: "3", title: "打豆豆", completed: false, createdAt: new Date() },
];
3.3 前端设计:两种异步方式的对决

前端只有一个 index.html,零框架。核心是一个 <ul> 列表 + 一段 JS 脚本。关键在于代码里留下了两种异步调用方式的对比——注释掉的 .then() 链和实际使用的 async/await

方式一:Promise 链式调用(传统 .then()

// 这是注释掉的老写法
// fetch("http://localhost:8080/todos")
// .then(res => res.json())  // 第一层异步:等响应回来 → 解析 JSON
// .then(data => {           // 第二层异步:拿到解析后的数据
//     todos.innerHTML = data.map(todo => `<li>${todo.title}</li>`).join("");
// })

每一步 .then() 都是一个新的回调函数。逻辑简单时还好,一旦要加错误处理、条件判断、循环调用,就会变成臭名昭著的回调地狱(Callback Hell)——层层嵌套,代码像"俄罗斯套娃"。

方式二:async/await(现代语法)

// 这是实际使用的写法——像写同步代码一样写异步逻辑
async function main() {
    const res = await fetch("http://localhost:8080/todos");
    const data = await res.json();
    todos.innerHTML = data.map(todo => `<li>${todo.title}</li>`).join("");
}
main();

对比一眼就能看出差别:

维度 .then() async/await
可读性 链式调用,步骤多了眼花 像读菜谱一样自上而下
错误处理 最后加 .catch(),定位不精确 try/catch 包裹,哪步出错清清楚楚
调试 断点难打(链式跳转) 断点逐行走,跟同步代码一样
条件分支 要在 .then() 里写 if,很丑 直接写 if/else,自然

结论.then() 的可读性确实没有特别好。三步以内的异步用哪个都行,但超过三步、或者有分支和循环时,async/await 是压倒性的优势。后续所有异步代码一律 async/await


四、实现:核心代码和流程

4.1 Bun 服务器骨架

Bun.serve() 是整台服务器的发动机。不需要 Express、不需要 Koa,一个函数就启动:

const server = Bun.serve({
    port: 8080,

    // fetch 是所有请求的总入口——就像商场前台
    // 不管谁来、来干嘛,都先到这里登记,再由前台分派到对应柜台
    async fetch(req) {
        // CORS 头:告诉浏览器"我允许跨域请求"
        const headers = {
            'Access-Control-Allow-Origin': "*"
        };

        // 解析用户访问的地址
        // URL = 协议://主机:端口/路径?参数
        // 比如 https://baidu.com:443/search?q=bun
        const url = new URL(req.url);

        // 路由 1:GET /todos —— 列出所有任务
        if (req.method === 'GET' && url.pathname === "/todos") {
            return Response.json(todos, { headers });
        }

        // 路由 2:GET /todos/:id —— 查看单个任务详情
        // startsWith 做动态路由匹配
        if (req.method === 'GET' && url.pathname.startsWith("/todos/")) {
            const id = url.pathname.split("/")[2];
            // "/todos/3".split("/")["", "todos", "3"],取下标 2 就是 id
            const todo = todos.find(t => t.id === id);
            console.log(todo, '-----');
            return Response.json(todo);
        }

        // 兜底:没匹配到任何路由,打个招呼
        return Response.json({ msg: 'hello world' });
    }
});

关键知识点逐条讲

  1. IP 对应一台物理机器,端口区分不同服务:一台服务器只有一个 IP 地址(就像一栋楼只有一个门牌号),但可以开很多端口(就像楼里有很多房间)。HTTP 服务一般用 80 端口,邮件服务用 25 端口,音乐服务用别的端口——各走各的门,互不干扰。

  2. 服务器处于"伺服状态"Bun.serve() 一旦调用,返回的 server 对象就一直竖着耳朵监听 8080 端口,等待客户端来敲门。这就是"伺服"——一直在服务,随时待命。

  3. HTTP 就是请求-响应的回合制游戏:用户发一个 Request(“我要看任务列表”),服务器回一个 Response(“给你 JSON”)。一来一回,一问一答,简单直接。

  4. fetch 是 Bun.serve 的内置方法:所有请求——不管什么方法、什么路径——都会从这个函数过。所以你的路由逻辑(一个个 if/else 判断)本质上就是在这个入口函数里做"分流"——像医院挂号台,根据症状(URL)分到不同科室(处理分支)。

  5. async/await 控制异步流程fetch 函数前有 async,意味着里面可以用 await 等待异步操作完成。比如以后如果要从数据库读数据,直接 await db.query(...) 就行,代码结构不变。

4.2 前端数据渲染

浏览器拿到 JSON 后,用 map 把数据转成 HTML 字符串,一把塞进 DOM:

todos.innerHTML = data.map(todo => `<li>${todo.title}</li>`).join("");

这行代码虽短,完成了三件事:

  1. data.map(...) —— 把 3 个 todo 对象映射成 3 段 <li>吃饭</li><li>睡觉</li><li>打豆豆</li>
  2. .join("") —— 把数组拼成一个长字符串(不加分隔符)
  3. todos.innerHTML = ... —— 整个替换 <ul> 的内容,浏览器一次性重绘

五、踩坑:开发中遇到的实际问题

5.1 CORS 跨域

开发时如果用 file:// 协议直接打开 HTML,或者前端跑在 localhost:3000 而后端在 localhost:8080,浏览器会报经典的红色错误:

Access to fetch at 'http://localhost:8080/todos' from origin 'null' has been blocked by CORS policy

这是浏览器的同源策略——协议、域名、端口三者任何一个不同,就视为"跨域",浏览器默认拦截。

解决:后端响应里加 CORS 头,告诉浏览器"我允许跨域":

const headers = {
    'Access-Control-Allow-Origin': "*"
};

* 表示允许任何来源——开发阶段方便,生产环境要收紧到具体域名。

5.2 动态路由匹配

RESTful 风格里,获取单个资源用 /todos/:id 这种带参数的路径(如 /todos/1/todos/2)。但 Bun.serve 没有内置路由,得手动解析。

做法:用 startsWith("/todos/") 匹配所有详情请求,然后把 URL 拆开取 id:

const id = url.pathname.split("/")[2];
// "/todos/3"["", "todos", "3"]"3"

这是最简单的手工动态路由。接口多了以后应该抽象成路由注册函数,但两三个接口时这样写最直接。

5.3 async/await 必须放在 async 函数里

新手容易犯的错——直接在顶层写 await

// ❌ 报错:await is only valid in async functions
const res = await fetch("http://localhost:8080/todos");

解决:包一个 async function main(),在里面用 await,然后调用它:

async function main() {
    const res = await fetch("http://localhost:8080/todos");
    const data = await res.json();
    // ...
}
main();  // 别忘了调用!我就是踩了这个坑

最后

这篇文章从一个 50 行的 Bun + TS TodoList 出发,先讲清楚 RESTful、interface 约束、async/await 异步这些地基知识。

核心想表达的是:Bun + TypeScript 把全栈开发的门槛拉到了历史最低。前端工程师不需要学 Go 也能写出高性能服务端,不需要学 Python 也能接入 AI 大模型。先用小项目跑通全流程,再逐步加复杂度——这是一条被验证有效的路线。

地基打好了,盖楼就是一层一层往上加的事。

更多推荐