开场白
前端写久了,有时候会被框架惯坏------Vue 一把梭,React 一把梭,反正尤大和脸书都给你封装好了,干就完了。但写后端的时候,有些概念就绕不过去了,比如接口(Interface) ,比如 RESTful ,比如面向对象编程到底面向了个啥。
这篇文章是我在学 Bun 的时候随手写的一个 TodoList 的复盘。功能很简单------就一个增删查(删还没写,别急),但里面串起来的东西不少:TypeScript 的类型约束、Bun 的原生 HTTP 服务、RESTful 的 URL 设计、前端 fetch 的两种写法。一个一个来。
一、Interface------"先定规矩,再写代码"
ts
interface Todo {
id: string;
title: string;
completed: boolean;
createdAt: Date;
}
这是整个项目的第一段代码。可能有人会觉得:不就定义了一个对象长什么样吗,有啥好说的?
但 Interface 其实是 OOP(面向对象编程)里最容易被人忽略的核心概念。OOP 三大件------封装、继承、多态------大家都背得滚瓜烂熟,但接口才是设计模式的地基。
你可以这样理解:
- 面向对象编程是地基------教你用类和对象来组织代码;
- 面向接口编程是盖楼的图纸------它不关心你用什么砖、什么水泥,只关心"这个楼必须有电梯、消防通道、停车场"。
换句话说,Interface 声明的是约束 ,而不是实现。上面的 Todo 接口就是在说:任何一个 Todo 对象,必须有 id、title、completed、createdAt 这四个属性,少一个都不行,类型错了也不行。
这和抽象类有什么区别?简单粗暴地说:
| Interface | 抽象类 | |
|---|---|---|
| 能不能有实现 | ❌ 不能,纯声明 | ✅ 可以有方法体 |
| 一个类能实现几个 | 多个 | 只能继承一个 |
| 本质 | 契约/规范 | 模板/基类 |
所以在这个项目里,我们用 Interface 定义 Todo 的形状,TypeScript 编译器帮你在写代码的时候就检查------少个字段、类型写错,根本跑不起来。这就是编译时的安全感。
二、RESTful------"一切皆资源"
RESTful 的核心思想其实就一句:一切皆资源。
RESTful 不是什么高深的架构,它本质上就是一种定义 URL 的规则:
资源的名词 + 资源的操作(HTTP 动词)
拿我们这个 TodoList 举例:
| 操作 | HTTP 动词 | URL |
|---|---|---|
| 获取所有任务 | GET | /todos |
| 获取单个任务 | GET | /todos/:id |
| 创建任务 | POST | /todos |
| 更新任务 | PUT/PATCH | /todos/:id |
| 删除任务 | DELETE | /todos/:id |
注意到了吗?URL 里只有名词 (todos),操作全靠 HTTP 动词来表达。这就是 RESTful 的精髓------URL 代表资源,动词代表操作 ,不搞那些 /getTodoList、/createTodo 这种 RPC 风格的鬼东西。
代码里长这样:
ts
// 获取所有 todos
if (req.method === 'GET' && url.pathname === '/todos') {
return Response.json(todos, { headers });
}
// 获取单个 todo
if (req.method === 'GET' && url.pathname.startsWith('/todos/')) {
const id = url.pathname.split("/")[2];
const todo = todos.find((todo) => todo.id === id);
return Response.json(todo);
}
两个分支,用 req.method 和 url.pathname 组合判断------这就是最小可行路由。真正的项目你肯定会用路由库,但手写过一次,你才知道那些框架到底帮你做了什么。
至于"路由"这个词,有个很土的比喻------路由器就像一个交警,站在路口根据请求的目的地把流量导向不同的处理函数。挺土的,但确实好懂。
三、Bun.serve------"一个对象起一个服务"
Node.js 起一个 HTTP 服务要写多少行?
js
const http = require('http');
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ msg: 'hello' }));
});
server.listen(3000);
Bun 呢:
ts
const server = Bun.serve({
port: 8081,
async fetch(req) {
// 在这里处理请求
}
});
一个对象字面量,一个 fetch 函数,搞定。没有 createServer,没有 listen,没有 writeHead......Bun 把这些都包进了 Bun.serve,你只需要关心端口号和请求处理逻辑。
这里有一个很多人容易踩的坑:fetch 函数里的 req 是请求对象 ,url.pathname 是路径,url.searchParams 是查询参数。拿 URL 来说:
bash
https://baidu.com:8080/pathname?page=1&size=10
protocol: httpshostname: baidu.comport: 8080pathname: /pathnamesearch: ?page=1&size=10
这些在 new URL(req.url) 之后全能拿到。别自己用正则去拆,太费劲了。
另外,服务端返回 JSON 的时候,记得带上 CORS 头:
ts
const headers = {
'Access-Control-Allow-Origin': '*'
};
不然后端跑起来了,浏览器一个跨域报错糊你脸上,排查半天才发现是忘了加 header。这种亏我吃过。
四、前端------从 Promise 链到 async/await
前端部分很简单,就是一个 HTML 文件里嵌了一段 JS。但这段代码展示了两种写法,刚好能看出进化过程:
写法一:Promise 链
js
fetch("http://127.0.0.1:8081/todos")
.then(res => res.json())
.then(data => {
todos.innerHTML = data.map(todo => `<li>${todo.title}</li>`).join('');
});
写法二:async/await
js
async function main() {
const res = await fetch("http://127.0.0.1:8081/todos");
const data = await res.json();
todos.innerHTML = data.map(todo => `<li>${todo.title}</li>`).join('');
}
main();
两种写法做的事情一模一样,但可读性天差地别。Promise 链写多了嵌套,就是经典的"回调地狱"青春版;async/await 把异步代码写成了同步的样子,逻辑流一目了然。
我个人习惯:超过两个 .then() 就直接换 async/await,别折磨未来的自己。
五、这个项目还缺什么?
老实说,这个 TodoList 目前只实现了 R(Read),CRUD 里 C、U、D 都没写。如果你准备把它补全,可以考虑:
- POST /todos ------ 从
req.body里解析 JSON,往数组里 push,返回 201; - PUT /todos/:id ------ 找到对应的 todo,更新 completed 状态(勾选/取消);
- DELETE /todos/:id ------ splice 掉指定 id 的任务;
- 数据持久化 ------ 目前数据在内存里,服务重启就没了。可以用 Bun 的 SQLite 支持或者直接写 JSON 文件;
- 前端交互 ------ 加个输入框和按钮,别只会展示了。
最后几句话
回过头看,这个不到 100 行的小项目串起来的东西不少:
- TypeScript 的 Interface 教会你"先定契约,再写逻辑";
- RESTful 教会你"用 URL 描述资源,用 HTTP 动词描述操作";
- Bun 让你知道起一个后端服务可以有多简单;
- async/await 把你从 Promise 链里解救出来。
小项目有小项目的价值------它不会让你迷失在目录结构和中间件里,能让你专注地把几个核心概念吃透。收工。