大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
Checkpoint: 完整应用
你将完成什么
把前端 Todo 应用和后端 API 整合,实现完整的全栈应用:用户注册登录、Todo CRUD、数据持久化。
验收标准
- 前端 React + 后端 Express + 数据库 PostgreSQL
- JWT 认证保护 API
- 前端路由(React Router)
- 部署到服务器
架构
前端(React)
├── 登录页 /login
├── 注册页 /register
└── Todo 页 /todos(需登录)
后端(Express)
├── POST /api/register
├── POST /api/login
├── GET /api/todos(需认证)
├── POST /api/todos(需认证)
├── PUT /api/todos/:id(需认证)
└── DELETE /api/todos/:id(需认证)
数据库(PostgreSQL)
├── users 表
└── todos 表
核心代码
后端:认证中间件
const jwt = require('jsonwebtoken');
const SECRET = process.env.JWT_SECRET;
function authMiddleware(req, res, next) {
const token = req.headers.authorization?.substring(7);
if (!token) {
return res.status(401).json({ code: 401, msg: '未登录' });
}
try {
const payload = jwt.verify(token, SECRET);
req.userId = payload.userId;
next();
} catch (err) {
res.status(401).json({ code: 401, msg: 'Token 无效' });
}
}
module.exports = authMiddleware;
后端:Todo 路由
const auth = require('./authMiddleware');
// 获取当前用户的 todos
app.get('/api/todos', auth, async (req, res) => {
const result = await pool.query(
'SELECT * FROM todo WHERE user_id = $1 ORDER BY created_at DESC',
[req.userId]
);
res.json({ code: 0, data: result.rows });
});
// 创建 todo
app.post('/api/todos', auth, async (req, res) => {
const { text } = req.body;
if (!text) {
return res.status(400).json({ code: 400, msg: '内容不能为空' });
}
const result = await pool.query(
'INSERT INTO todo (text, user_id) VALUES ($1, $2) RETURNING *',
[text, req.userId]
);
res.status(201).json({ code: 0, data: result.rows[0] });
});
// 更新 todo
app.put('/api/todos/:id', auth, async (req, res) => {
const { id } = req.params;
const { text, completed } = req.body;
const result = await pool.query(
'UPDATE todo SET text = $1, completed = $2 WHERE id = $3 AND user_id = $4 RETURNING *',
[text, completed, id, req.userId]
);
if (result.rows.length === 0) {
return res.status(404).json({ code: 404, msg: 'Todo 不存在' });
}
res.json({ code: 0, data: result.rows[0] });
});
// 删除 todo
app.delete('/api/todos/:id', auth, async (req, res) => {
const { id } = req.params;
const result = await pool.query(
'DELETE FROM todo WHERE id = $1 AND user_id = $2 RETURNING id',
[id, req.userId]
);
if (result.rows.length === 0) {
return res.status(404).json({ code: 404, msg: 'Todo 不存在' });
}
res.status(204).send();
});
前端:API 封装
// api.js
const API_BASE = 'http://localhost:3000/api';
function getHeaders() {
const token = localStorage.getItem('token');
return {
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : ''
};
}
export async function login(username, password) {
const res = await fetch(`${API_BASE}/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
return res.json();
}
export async function getTodos() {
const res = await fetch(`${API_BASE}/todos`, {
headers: getHeaders()
});
return res.json();
}
export async function createTodo(text) {
const res = await fetch(`${API_BASE}/todos`, {
method: 'POST',
headers: getHeaders(),
body: JSON.stringify({ text })
});
return res.json();
}
export async function updateTodo(id, data) {
const res = await fetch(`${API_BASE}/todos/${id}`, {
method: 'PUT',
headers: getHeaders(),
body: JSON.stringify(data)
});
return res.json();
}
export async function deleteTodo(id) {
const res = await fetch(`${API_BASE}/todos/${id}`, {
method: 'DELETE',
headers: getHeaders()
});
return res.status === 204;
}
前端:Todo 页面
import { useState, useEffect } from 'react';
import { getTodos, createTodo, updateTodo, deleteTodo } from './api';
function TodoPage() {
const [todos, setTodos] = useState([]);
const [text, setText] = useState('');
useEffect(() => {
loadTodos();
}, []);
const loadTodos = async () => {
const res = await getTodos();
if (res.code === 0) {
setTodos(res.data);
}
};
const handleAdd = async (e) => {
e.preventDefault();
if (!text.trim()) return;
const res = await createTodo(text);
if (res.code === 0) {
setTodos([res.data, ...todos]);
setText('');
}
};
const handleToggle = async (id, completed) => {
const res = await updateTodo(id, { completed: !completed });
if (res.code === 0) {
setTodos(todos.map(t => t.id === id ? res.data : t));
}
};
const handleDelete = async (id) => {
const success = await deleteTodo(id);
if (success) {
setTodos(todos.filter(t => t.id !== id));
}
};
return (
<div>
<h1>我的 Todo</h1>
<form onSubmit={handleAdd}>
<input
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="输入待办事项"
/>
<button type="submit">添加</button>
</form>
<ul>
{todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => handleToggle(todo.id, todo.completed)}
/>
<span>{todo.text}</span>
<button onClick={() => handleDelete(todo.id)}>删除</button>
</li>
))}
</ul>
</div>
);
}
export default TodoPage;
部署
# 1. 前端打包
npm run build
# 2. 后端部署
pm2 start server.js --name todo-api
# 3. Nginx 配置
# /etc/nginx/sites-available/todo
server {
listen 80;
server_name your-domain.com;
location / {
root /var/www/todo/dist;
try_files $uri /index.html;
}
location /api {
proxy_pass http://localhost:3000;
}
}
常见卡点
- 跨域问题:后端加
cors中间件,或前端配置代理。 - Token 丢失:确认前端
localStorage有 token,请求头带Authorization。 - 路由 404:Nginx 配置
try_files $uri /index.html支持前端路由。
加分项
- 用 React Query 管理服务端状态
- 用 WebSocket 实现实时同步
- 用 Docker 容器化部署
下一步
完成本课后,进入「DevOps」阶段,学习 CI/CD、监控、日志。