🌟 RESTful Todo 任务清单 ------ 从零学习前后端
🎯 一篇带你从零掌握前后端交互的实战教程 · 适合初学者
📋 项目概述
这是一个极简的任务清单(Todo List) 项目,使用 Bun 作为后端服务器,纯 HTML + JavaScript 作为前端。
通过这个项目,可以学习到:
- ✅ TypeScript 类型系统(接口、类型约束)
- ✅ 面向对象编程思想(OOP)
- ✅ RESTful API 设计风格
- ✅ 前后端交互(Fetch API + HTTP 协议)
- ✅ Bun 运行时 的基本使用
📁 一、项目结构
bash
todo/
├── server.ts # 后端:提供 RESTful API
├── index.html # 前端:展示任务列表
└── readme.md # 项目说明(就是这个文件)
💡 零依赖! 不需要安装任何第三方包,只需要 Bun 一个运行时即可运行。
🧠 二、核心编程思想
1. 面向接口编程(Interface)🎯
面向对象编程(OOP)是基础,面向接口编程 是设计模式的基石。
什么是接口?
接口就像是"合同"或"模板"。它规定了一个对象必须要有哪些属性和方法,但不关心具体怎么实现。
typescript
interface Todo {
id: string;
title: string;
completed: boolean;
createdAt: Date;
}
这段代码的意思是:任何一个 Todo 任务,都必须有 id、title、completed、createdAt 这四个属性,缺一不可。
为什么要用接口?
| 优势 | 说明 |
|---|---|
| 🔒 规范 | 保证数据格式统一,不出错 |
| 📖 可读性 | 看一眼接口就知道数据长什么样 |
| 🤝 协作 | 前后端开发人员按同一份"合同"开发 |
类型体操小知识:
typescript
const todos: Todo[] = [ ... ]; // Todo[] 表示"这是一个数组,里面的每一项都必须是 Todo 类型"
todos.push({ id: "4", title: "学习" }); // ❌ 报错!缺少 completed 和 createdAt 属性
如果少写了属性或类型不对,TypeScript 在写代码时就会提示错误 ,不用等到运行才发现 ------ 这就是强类型的好处。
2. RESTful 风格 🔄
REST = Representational State Transfer(表现层状态转移)
核心思想:一切皆资源
- 🔗 URL 只使用名词 表示资源:
/todos - 🎬 用 HTTP 动词 表示操作:
| HTTP 方法 | 操作 | 示例 URL | 含义 |
|---|---|---|---|
| 🟢 GET | 查询列表 | /todos |
获取所有任务 |
| 🟢 GET | 查询详情 | /todos/1 |
获取 id=1 的任务 |
| 🟡 POST | 新增 | /todos |
创建新任务 |
| 🔵 PUT | 全量修改 | /todos/1 |
替换整个任务 |
| 🟣 PATCH | 部分修改 | /todos/1 |
修改部分字段 |
| 🔴 DELETE | 删除 | /todos/1 |
删除任务 |
⚠️ 当前项目只实现了 GET 方法,其余方法可作为练习自行补充。
类比理解:
- 🏪 资源 = 仓库里的货物
- 📦 URL = 货架位置(用名词命名)
- 🛠️ HTTP 方法 = 对货物做什么(查、增、改、删)
- 🧑💼 路由 = 仓库管理员(根据请求类型把任务分发给对应处理函数)
3. 异步编程(Async / Await)⏳
javascript
async function main() {
const res = await fetch("http://localhost:8080/todos");
const data = await res.json();
// ...
}
async:声明这是一个"异步函数"await:等待一个异步操作完成(比如网络请求)- 可以理解为:"我先去拿数据,你等着我,拿到之后再继续往下走"
对比回调方式(.then() 链),async/await 让异步代码看起来像同步代码,更易读。
💡 代码中保留了
Promise.then()的注释版本,方便对比两种写法的区别。
4. CORS 跨域 ------ 浏览器的一道"安检" 🛡️
typescript
const headers = {
'Access-Control-Allow-Origin': '*', // 允许所有来源访问
};
为什么会用到它?
- 📄 你的前端文件
index.html是在本地直接打开的(file://协议) - 🖥️ 后端跑在
http://localhost:8080(http://协议) - 🚫 协议不同 → 跨域了!浏览器会拦截响应
简单理解:
浏览器就像一个小区的保安。前端是住户,后端是快递站。
住户(前端)想去快递站(后端)拿包裹,保安(浏览器)要检查 ------ 快递站是否允许这个住户取件?
Access-Control-Allow-Origin: '*'就相当于快递站贴了告示:"所有人均可取件"。
📝 三、逐文件语法 & 知识点解析
📄 server.ts
typescript
// 1. 接口定义 ------ 约束数据格式
interface Todo {
id: string;
title: string;
completed: boolean;
createdAt: Date;
}
| 语法 | 说明 |
|---|---|
interface Todo { } |
定义一个接口,描述 Todo 对象的结构 |
id: string |
属性名: 类型 ------ 强类型约束,id 必须是字符串 |
Date |
TypeScript 内置类型,表示日期时间。new Date() 创建"当前时刻" |
typescript
// 2. 类型注解 ------ 给变量"贴标签"
const todos: Todo[] = [ ... ];
| 语法 | 说明 |
|---|---|
: Todo[] |
声明 todos 是一个数组,且数组里的每个元素都必须是 Todo 类型 |
typescript
// 3. Bun.serve ------ 启动 HTTP 服务器
const server = Bun.serve({
port: 8080,
async fetch(req) { ... }
});
| 语法 | 说明 |
|---|---|
Bun.serve() |
Bun 内置方法,创建一个 HTTP 服务器(无需额外安装 Express) |
port: 8080 |
监听端口号,浏览器通过 http://localhost:8080 访问 |
async fetch(req) |
每当有请求进来,都会调用这个函数处理 |
req |
Request 对象 ------ 包含请求方法、URL、头信息等 |
🚪 什么是端口?
一台服务器只有一个 IP 地址(比如
127.0.0.1),但可以提供多个服务。端口号就像"房间号":
127.0.0.1:8080表示"去 8080 号房间找 HTTP 服务"。
typescript
// 4. URL 解析与路由判断
const url = new URL(req.url);
if (req.method === 'GET' && url.pathname === "/todos") {
return Response.json(todos, { headers });
}
| 语法 | 说明 |
|---|---|
new URL(req.url) |
将请求的 URL 字符串解析成 URL 对象,方便取 pathname 等 |
req.method |
获取 HTTP 方法(GET / POST / PUT / DELETE) |
url.pathname |
获取 URL 中的路径部分(例如 /todos) |
路径解析拆解:
请求 http://localhost:8080/todos/1 时:
perl
url.pathname → "/todos/1"
url.pathname.split("/") → ["", "todos", "1"]
索引: 0 1 2
url.pathname.split("/")[2] → "1" ← 这就是我们要的 id
特别注意 startsWith 的坑:
typescript
url.pathname.startsWith("/todos/") // ✅ 正确!匹配 /todos/1、/todos/abc 等
url.pathname.startsWith("/todos") // ❌ 有问题!/todosABC 也会匹配上
路由流程图:
bash
浏览器请求 → Bun.serve → fetch(req) 处理
├── GET /todos → 返回所有任务
├── GET /todos/:id → 返回指定任务
└── 其他 → 返回 { msg: 'hello world' }
typescript
// 5. 数组查找 ------ find() 方法
const todo = todos.find((t) => t.id === id);
| 语法 | 说明 |
|---|---|
todos.find(条件函数) |
遍历数组,找到第一个满足条件的元素,返回该元素 |
(t) => t.id === id |
箭头函数 ------ 对每个元素 t,判断 t.id 是否等于变量 id |
find() 找不到怎么办? |
返回 undefined,不会报错,但前端接收到的是 null |
📄 index.html
html
<ul id="todos"></ul>
<script>
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();
</script>
| 语法 / 概念 | 说明 |
|---|---|
fetch(url) |
浏览器内置 API,发送 HTTP 请求,返回 Promise 对象 |
res.json() |
把响应体解析成 JSON 对象(也是一个异步操作) |
data.map(todo => ...) |
遍历任务数组,把每个任务转成 <li> HTML 字符串 |
.join('') |
把数组拼接成一个字符串(避免逗号) |
todos.innerHTML = ... |
把生成的 HTML 放入 <ul> 中,页面就看到了 |
fetch 的两种写法对比:
javascript
// 写法一:Promise 链式调用(传统)
fetch("/todos")
.then(res => res.json())
.then(data => { /* 处理数据 */ });
// 写法二:async/await(推荐,更清晰)
const res = await fetch("/todos");
const data = await res.json();
⚡ 关键区别 :
.then()需要在回调函数里继续写逻辑;await则可以让代码"停下来等结果",按顺序往下写即可。
为什么用 join('')?
javascript
["<li>吃饭</li>", "<li>睡觉</li>", "<li>打豆豆</li>"]
// .join('') 之后变成:
"<li>吃饭</li><li>睡觉</li><li>打豆豆</li>" // ✅ 一个完整的字符串
// 如果不加 .join(''),数组 toString() 会自动加逗号:
"<li>吃饭</li>,<li>睡觉</li>,<li>打豆豆</li>" // ❌ 页面会显示逗号!
🌐 四、HTTP 协议速通
请求(Request) → 响应(Response)
这是整个 Web 世界最基本的运作模式:
你(浏览器) 服务器
│ │
├── ① 输入 URL,发送请求 ─────→ │
│ ├── ② 处理请求(查数据库等)
│ │
│ ←─── ③ 返回响应 ────────────┤
│ │
├── ④ 浏览器解析响应,渲染页面 │
常见的 HTTP 状态码
后端虽然没有显式设置状态码,但
Response.json()默认返回 200(成功)。
| 状态码 | 含义 | 说明 |
|---|---|---|
| 200 ✅ | OK | 请求成功 |
| 201 🆕 | Created | 创建成功(POST 新增时使用) |
| 204 🗑️ | No Content | 删除成功,没有返回体 |
| 400 ❌ | Bad Request | 客户端请求有误(比如参数不对) |
| 404 🔍 | Not Found | 资源不存在 |
| 500 💥 | Internal Server Error | 服务器内部出错 |
🚀 五、Bun 运行时简介
Bun 是一个新兴的 JavaScript/TypeScript 运行时,可以理解为 Node.js 的"升级替代品"。
| 特性 | Bun | Node.js |
|---|---|---|
| ⚡ 启动速度 | 极快(毫秒级) | 较慢(需要加载大量内置模块) |
| 🛠️ 内置工具 | 打包器、转译器、测试运行器、包管理器全内置 | 需要额外安装(webpack、tsc 等) |
| 📘 TypeScript 支持 | 开箱即用,无需配置 tsconfig | 需要 ts-node 或 tsc 编译 |
| 🌐 HTTP 服务器 | Bun.serve() 零配置启动 |
需要 http 模块或 Express 框架 |
在这个项目中,Bun 让你 无需安装 Express、无需配置 tsconfig、无需 nodemon,一个文件 + 一行命令就跑起来了。
▶️ 六、运行方式
bash
# 确保已安装 Bun(https://bun.sh)
# 在 todo 目录下执行:
bun run server.ts
看到终端输出类似 listening on http://localhost:8080/ 即表示启动成功。
然后浏览器访问:
- 📄 任务列表页 :打开
index.html(直接用浏览器打开即可,无需额外服务器) - 🔌 API 测试 :
http://localhost:8080/todos - 🔍 任务详情 :
http://localhost:8080/todos/1
💡 查看详情时,注意浏览器地址栏下方的控制台(F12 → Console),会打印出找到的 todo 对象。
🗺️ 七、学习路线图
如果这个项目你已经理解了,下一步可以尝试:
| 阶段 | 学习目标 | 难度 |
|---|---|---|
| 1️⃣ | 新增功能:实现 POST(新增任务)、DELETE(删除任务)、PATCH(修改完成状态) | ⭐⭐ |
| 2️⃣ | 数据持久化:把数据存到 JSON 文件或 SQLite 数据库,而不是内存数组(重启服务数据会丢失) | ⭐⭐⭐ |
| 3️⃣ | 前端增强:添加新增输入框、删除按钮、勾选完成复选框 | ⭐⭐ |
| 4️⃣ | 使用框架:前端用 React/Vue,后端用 Express/Koa 重构 | ⭐⭐⭐⭐ |
| 5️⃣ | 错误处理 :当 find() 找不到任务时,返回 404 状态码而不是空数据 |
⭐⭐ |
| 6️⃣ | UUID 生成 :新增任务时用 crypto.randomUUID() 替代手动指定 id |
⭐ |
📊 八、总结
这个 Todo 项目虽小,但"五脏俱全":
| 知识点 | 应用场景 |
|---|---|
| 🎯 TypeScript 接口 | 约束数据结构,保证类型安全 |
| 🔄 RESTful API 设计 | 规范 URL 和 HTTP 方法 |
| ⚡ Bun.serve | 零配置启动 HTTP 服务器 |
| ⏳ Async/Await | 优雅处理异步网络请求 |
| 🌐 Fetch API | 前后端数据交互 |
| 🎨 DOM 操作 | 把数据渲染到页面 |
| 🛡️ CORS 跨域 | 解决不同源的请求限制 |
| 📡 HTTP 状态码 | 表示请求的处理结果 |
编程思想总结:
- 🧬 OOP(面向对象):用接口定义对象的"形状"
- 📐 面向接口编程:不关心具体实现,只关心"必须有什么"
- 🏗️ RESTful:用资源 + 动词来组织 API
- 🔄 异步编程:用 async/await 让异步代码像同步一样清晰
🎉 学完这个项目,你就掌握了前后端交互的最小闭环!
麻雀虽小,但每一个知识点都是后续深入学习 Web 开发的基石。
💬 写在最后
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、转发🔄!
有任何问题或建议,欢迎在评论区留言交流~ 💬
Happy Coding! 🚀