TOP项目案例:任务管理应用的前后端实现
你还在为项目管理效率低下而困扰吗?
作为开发者,你是否经常遇到这些问题:团队任务分配混乱,进度跟踪困难,项目 deadlines频频延误?传统的Excel表格和纸质清单早已无法满足现代开发团队的协作需求。本文将从零开始,基于The Odin Project课程体系,实现一个功能完备的全栈任务管理应用,帮助你掌握前后端整合的核心技能。
读完本文后,你将能够:
- 设计并实现响应式任务管理界面
- 构建RESTful API处理任务数据
- 实现用户认证与权限管理
- 掌握前后端数据交互的最佳实践
- 部署全栈应用到生产环境
项目概述:技术栈选型与架构设计
功能需求分析
本任务管理应用(TodoMaster)需实现以下核心功能:
| 模块 | 核心功能 | 技术挑战 |
|---|---|---|
| 用户系统 | 注册/登录/权限控制 | JWT认证实现 |
| 任务管理 | CRUD操作/优先级排序 | 状态管理与数据持久化 |
| 项目协作 | 多用户任务分配 | 实时数据同步 |
| 数据可视化 | 任务进度统计 | 前端图表渲染 |
技术栈选型
基于The Odin Project课程体系,选择以下技术栈:
选型理由:
- 前后端统一JavaScript生态,降低技术切换成本
- React组件化开发提高UI复用率
- MongoDB灵活的数据模型适合任务管理场景
- Express轻量级框架便于快速开发API
系统架构设计
前端实现:从UI组件到状态管理
项目初始化与环境配置
# 创建React应用
npx create-react-app todo-master --template typescript
cd todo-master
# 安装核心依赖
npm install axios react-router-dom redux react-redux @reduxjs/toolkit
npm install antd # 使用国内CDN的UI库
国内CDN配置(public/index.html):
<!-- 引入国内CDN资源 -->
<link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/antd/5.4.7/reset.css">
<script src="https://cdn.bootcdn.net/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
核心组件设计
任务卡片组件(components/TaskCard.tsx):
import React from 'react';
import { Card, Tag, Button, Checkbox } from 'antd';
import { EditOutlined, DeleteOutlined } from '@ant-design/icons';
interface Task {
_id: string;
title: string;
description: string;
priority: 'low' | 'medium' | 'high';
status: 'todo' | 'inProgress' | 'done';
dueDate: string;
assignee: string;
}
interface TaskCardProps {
task: Task;
onToggleComplete: (id: string) => void;
onEdit: (task: Task) => void;
onDelete: (id: string) => void;
}
const TaskCard: React.FC<TaskCardProps> = ({ task, onToggleComplete, onEdit, onDelete }) => {
// 优先级标签样式映射
const priorityStyles = {
low: { color: '#52c41a', borderColor: '#52c41a' },
medium: { color: '#faad14', borderColor: '#faad14' },
high: { color: '#ff4d4f', borderColor: '#ff4d4f' }
};
return (
<Card
title={task.title}
extra={
<div>
<Button
icon={<EditOutlined />}
size="small"
onClick={() => onEdit(task)}
style={{ marginRight: 8 }}
/>
<Button
icon={<DeleteOutlined />}
size="small"
danger
onClick={() => onDelete(task._id)}
/>
</div>
}
style={{ marginBottom: 16 }}
>
<p>{task.description}</p>
<div style={{ display: 'flex', justifyContent: 'space-between', marginTop: 16 }}>
<Tag bordered style={priorityStyles[task.priority]}>
{task.priority === 'low' && '低优先级'}
{task.priority === 'medium' && '中优先级'}
{task.priority === 'high' && '高优先级'}
</Tag>
<span>截止日期: {new Date(task.dueDate).toLocaleDateString()}</span>
</div>
<div style={{ marginTop: 16, display: 'flex', justifyContent: 'flex-end' }}>
<Checkbox
checked={task.status === 'done'}
onChange={() => onToggleComplete(task._id)}
>
{task.status === 'done' ? '已完成' : '标记完成'}
</Checkbox>
</div>
</Card>
);
};
export default TaskCard;
任务列表与路由配置
路由设计(src/App.tsx):
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import Layout from './components/Layout';
import TaskList from './pages/TaskList';
import TaskDetail from './pages/TaskDetail';
import TaskForm from './pages/TaskForm';
import Login from './pages/Login';
import Register from './pages/Register';
import { PrivateRoute } from './components/PrivateRoute';
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route path="/" element={<PrivateRoute><Layout /></PrivateRoute>}>
<Route index element={<TaskList />} />
<Route path="tasks/new" element={<TaskForm />} />
<Route path="tasks/:id" element={<TaskDetail />} />
<Route path="tasks/:id/edit" element={<TaskForm />} />
</Route>
</Routes>
</BrowserRouter>
);
}
export default App;
Redux状态管理实现
任务切片(src/features/tasks/taskSlice.ts):
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
import axios from 'axios';
// 配置axios基础URL
const api = axios.create({
baseURL: 'http://localhost:5000/api'
});
// 请求拦截器添加token
api.interceptors.request.use(config => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// 异步获取任务列表
export const fetchTasks = createAsyncThunk(
'tasks/fetchTasks',
async (_, { rejectWithValue }) => {
try {
const response = await api.get('/tasks');
return response.data;
} catch (error: any) {
return rejectWithValue(error.response.data);
}
}
);
// 添加新任务
export const addTask = createAsyncThunk(
'tasks/addTask',
async (taskData: any, { rejectWithValue }) => {
try {
const response = await api.post('/tasks', taskData);
return response.data;
} catch (error: any) {
return rejectWithValue(error.response.data);
}
}
);
// 初始状态
interface TaskState {
items: any[];
status: 'idle' | 'loading' | 'succeeded' | 'failed';
error: string | null;
}
const initialState: TaskState = {
items: [],
status: 'idle',
error: null
};
// 创建任务切片
const taskSlice = createSlice({
name: 'tasks',
initialState,
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchTasks.pending, (state) => {
state.status = 'loading';
})
.addCase(fetchTasks.fulfilled, (state, action: PayloadAction<any[]>) => {
state.status = 'succeeded';
state.items = action.payload;
})
.addCase(fetchTasks.rejected, (state, action) => {
state.status = 'failed';
state.error = action.payload as string;
})
.addCase(addTask.fulfilled, (state, action: PayloadAction<any>) => {
state.items.push(action.payload);
});
}
});
export default taskSlice.reducer;
任务表单实现
添加任务表单(src/pages/TaskForm.tsx):
import React, { useState, useEffect } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { Form, Input, DatePicker, Select, Button, Typography, Space, message } from 'antd';
import { useDispatch } from 'react-redux';
import { addTask } from '../features/tasks/taskSlice';
import api from '../services/api';
const { Title } = Typography;
const { Option } = Select;
const TaskForm: React.FC = () => {
const [form] = Form.useForm();
const navigate = useNavigate();
const dispatch = useDispatch();
const { id } = useParams(); // 用于编辑任务
const isEditing = !!id;
useEffect(() => {
// 如果是编辑模式,获取任务详情
if (isEditing) {
const fetchTask = async () => {
try {
const response = await api.get(`/tasks/${id}`);
form.setFieldsValue(response.data);
} catch (error) {
message.error('获取任务详情失败');
}
};
fetchTask();
}
}, [form, id, isEditing]);
const onFinish = async (values: any) => {
try {
// 格式化日期
const taskData = {
...values,
dueDate: values.dueDate.format('YYYY-MM-DD')
};
if (isEditing) {
// 编辑任务逻辑
await api.put(`/tasks/${id}`, taskData);
message.success('任务更新成功');
} else {
// 添加新任务
dispatch(addTask(taskData) as any);
message.success('任务添加成功');
}
navigate('/');
} catch (error) {
message.error(isEditing ? '任务更新失败' : '任务添加失败');
}
};
return (
<div>
<Title level={2}>{isEditing ? '编辑任务' : '添加新任务'}</Title>
<Form
form={form}
layout="vertical"
onFinish={onFinish}
initialValues={{
priority: 'medium',
status: 'todo'
}}
>
<Form.Item
name="title"
label="任务标题"
rules={[{ required: true, message: '请输入任务标题' }]}
>
<Input placeholder="输入任务标题" />
</Form.Item>
<Form.Item
name="description"
label="任务描述"
rules={[{ required: true, message: '请输入任务描述' }]}
>
<Input.TextArea rows={4} placeholder="输入任务描述" />
</Form.Item>
<Form.Item
name="dueDate"
label="截止日期"
rules={[{ required: true, message: '请选择截止日期' }]}
>
<DatePicker style={{ width: '100%' }} />
</Form.Item>
<Form.Item
name="priority"
label="优先级"
rules={[{ required: true, message: '请选择优先级' }]}
>
<Select placeholder="选择优先级">
<Option value="low">低优先级</Option>
<Option value="medium">中优先级</Option>
<Option value="high">高优先级</Option>
</Select>
</Form.Item>
<Form.Item>
<Space>
<Button type="primary" htmlType="submit">
{isEditing ? '更新任务' : '创建任务'}
</Button>
<Button onClick={() => navigate('/')}>取消</Button>
</Space>
</Form.Item>
</Form>
</div>
);
};
export default TaskForm;
后端实现:RESTful API与数据持久化
项目初始化与目录结构
# 创建后端项目
mkdir todo-master-api
cd todo-master-api
npm init -y
# 安装核心依赖
npm install express mongoose dotenv cors jsonwebtoken bcryptjs
npm install nodemon typescript ts-node @types/node @types/express --save-dev
项目目录结构:
todo-master-api/
├── src/
│ ├── config/ # 配置文件
│ ├── controllers/ # 控制器
│ ├── middleware/ # 中间件
│ ├── models/ # 数据模型
│ ├── routes/ # 路由定义
│ ├── types/ # TypeScript类型
│ └── index.ts # 入口文件
├── .env # 环境变量
└── tsconfig.json # TypeScript配置
数据库模型设计
用户模型(src/models/User.ts):
import mongoose, { Document, Schema } from 'mongoose';
import bcrypt from 'bcryptjs';
export interface IUser extends Document {
username: string;
email: string;
password: string;
comparePassword(candidatePassword: string): Promise<boolean>;
}
const userSchema = new Schema<IUser>(
{
username: {
type: String,
required: true,
unique: true,
trim: true
},
email: {
type: String,
required: true,
unique: true,
trim: true,
lowercase: true
},
password: {
type: String,
required: true,
minlength: 6
}
},
{ timestamps: true }
);
// 密码加密中间件
userSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next();
try {
const salt = await bcrypt.genSalt(10);
this.password = await bcrypt.hash(this.password, salt);
next();
} catch (error: any) {
next(error);
}
});
// 密码验证方法
userSchema.methods.comparePassword = async function(candidatePassword: string): Promise<boolean> {
return bcrypt.compare(candidatePassword, this.password);
};
const User = mongoose.model<IUser>('User', userSchema);
export default User;
任务模型(src/models/Task.ts):
import mongoose, { Document, Schema } from 'mongoose';
export interface ITask extends Document {
title: string;
description: string;
status: 'todo' | 'inProgress' | 'done';
priority: 'low' | 'medium' | 'high';
dueDate: Date;
userId: mongoose.Types.ObjectId;
}
const taskSchema = new Schema<ITask>(
{
title: {
type: String,
required: true,
trim: true
},
description: {
type: String,
required: true,
trim: true
},
status: {
type: String,
enum: ['todo', 'inProgress', 'done'],
default: 'todo'
},
priority: {
type: String,
enum: ['low', 'medium', 'high'],
default: 'medium'
},
dueDate: {
type: Date,
required: true
},
userId: {
type: Schema.Types.ObjectId,
ref: 'User',
required: true
}
},
{ timestamps: true }
);
// 添加索引,优化查询性能
taskSchema.index({ userId: 1, status: 1 });
taskSchema.index({ dueDate: 1 });
const Task = mongoose.model<ITask>('Task', taskSchema);
export default Task;
认证中间件实现
JWT认证中间件(src/middleware/auth.ts):
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
import User from '../models/User';
// 扩展Request接口以包含用户信息
declare global {
namespace Express {
interface Request {
user?: any;
}
}
}
export const auth = async (req: Request, res: Response, next: NextFunction) => {
try {
// 获取token
const token = req.header('Authorization')?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({ message: '未提供认证令牌' });
}
// 验证token
const decoded = jwt.verify(token, process.env.JWT_SECRET as string) as any;
// 查找用户
const user = await User.findById(decoded.userId).select('-password');
if (!user) {
return res.status(401).json({ message: '用户不存在' });
}
// 将用户信息添加到请求对象
req.user = user;
next();
} catch (error) {
res.status(401).json({ message: '认证失败' });
}
};
export default auth;
API路由实现
任务路由(src/routes/taskRoutes.ts):
import express from 'express';
import {
getTasks,
getTaskById,
createTask,
updateTask,
deleteTask,
updateTaskStatus
} from '../controllers/taskController';
import { auth } from '../middleware/auth';
const router = express.Router();
// 所有任务路由都需要认证
router.use(auth);
// 获取所有任务
router.get('/', getTasks);
// 获取单个任务
router.get('/:id', getTaskById);
// 创建任务
router.post('/', createTask);
// 更新任务
router.put('/:id', updateTask);
// 更新任务状态
router.patch('/:id/status', updateTaskStatus);
// 删除任务
router.delete('/:id', deleteTask);
export default router;
任务控制器(src/controllers/taskController.ts):
import { Request, Response } from 'express';
import Task from '../models/Task';
// 获取所有任务(支持筛选和排序)
export const getTasks = async (req: Request, res: Response) => {
try {
const { status, priority, sortBy = 'dueDate', order = 'asc' } = req.query;
// 构建查询条件
const query: any = { userId: req.user._id };
if (status) {
query.status = status;
}
if (priority) {
query.priority = priority;
}
// 构建排序条件
const sortOptions: any = {};
sortOptions[sortBy as string] = order === 'asc' ? 1 : -1;
// 执行查询
const tasks = await Task.find(query).sort(sortOptions);
res.json(tasks);
} catch (error) {
res.status(500).json({ message: '获取任务失败', error: (error as Error).message });
}
};
// 创建任务
export const createTask = async (req: Request, res: Response) => {
try {
const { title, description, dueDate, priority, status } = req.body;
// 创建新任务
const task = new Task({
title,
description,
dueDate,
priority: priority || 'medium',
status: status || 'todo',
userId: req.user._id
});
await task.save();
res.status(201).json(task);
} catch (error) {
res.status(400).json({ message: '创建任务失败', error: (error as Error).message });
}
};
// 其他控制器方法省略...
服务器入口文件
src/index.ts:
import express from 'express';
import mongoose from 'mongoose';
import cors from 'cors';
import dotenv from 'dotenv';
import userRoutes from './routes/userRoutes';
import taskRoutes from './routes/taskRoutes';
// 加载环境变量
dotenv.config();
// 创建Express应用
const app = express();
const PORT = process.env.PORT || 5000;
// 中间件
app.use(cors());
app.use(express.json());
// 路由
app.use('/api/users', userRoutes);
app.use('/api/tasks', taskRoutes);
// 错误处理中间件
app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => {
console.error(err.stack);
res.status(500).send({ message: '服务器内部错误' });
});
// 连接数据库并启动服务器
mongoose.connect(process.env.MONGODB_URI as string)
.then(() => {
console.log('MongoDB连接成功');
app.listen(PORT, () => {
console.log(`服务器运行在端口 ${PORT}`);
});
})
.catch(err => {
console.error('MongoDB连接失败:', err.message);
});
前后端整合与功能测试
API服务集成
API服务封装(src/services/api.ts):
import axios from 'axios';
const api = axios.create({
baseURL: process.env.REACT_APP_API_URL || 'http://localhost:5000/api'
});
// 请求拦截器添加token
api.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
// 响应拦截器处理错误
api.interceptors.response.use(
(response) => response,
(error) => {
// 处理401错误(未授权)
if (error.response && error.response.status === 401) {
localStorage.removeItem('token');
window.location.href = '/login';
}
return Promise.reject(error);
}
);
export default api;
前后端联调测试
任务列表页面(src/pages/TaskList.tsx):
import React, { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Typography, Button, Space, Select, message, Spin } from 'antd';
import { PlusOutlined, ReloadOutlined } from '@ant-design/icons';
import { Link } from 'react-router-dom';
import { fetchTasks } from '../features/tasks/taskSlice';
import TaskCard from '../components/TaskCard';
import api from '../services/api';
const { Title } = Typography;
const { Option } = Select;
const TaskList: React.FC = () => {
const dispatch = useDispatch();
const { items: tasks, status, error } = useSelector((state: any) => state.tasks);
const [filter, setFilter] = useState({ status: 'all', priority: 'all', sort: 'dueDate' });
// 加载任务数据
useEffect(() => {
if (status === 'idle') {
dispatch(fetchTasks() as any);
}
}, [status, dispatch]);
// 处理任务状态更改
const handleToggleComplete = async (taskId: string) => {
try {
await api.patch(`/tasks/${taskId}/status`, {
status: tasks.find(t => t._id === taskId).status === 'done' ? 'todo' : 'done'
});
// 重新获取任务列表
dispatch(fetchTasks() as any);
message.success('任务状态已更新');
} catch (error) {
message.error('更新任务状态失败');
}
};
// 处理任务删除
const handleDeleteTask = async (taskId: string) => {
try {
await api.delete(`/tasks/${taskId}`);
// 重新获取任务列表
dispatch(fetchTasks() as any);
message.success('任务已删除');
} catch (error) {
message.error('删除任务失败');
}
};
// 处理筛选变更
const handleFilterChange = (key: string, value: string) => {
setFilter(prev => ({ ...prev, [key]: value }));
};
// 刷新任务列表
const handleRefresh = () => {
dispatch(fetchTasks() as any);
};
// 应用筛选
const filteredTasks = tasks.filter(task => {
if (filter.status !== 'all' && task.status !== filter.status) return false;
if (filter.priority !== 'all' && task.priority !== filter.priority) return false;
return true;
});
// 加载状态显示
if (status === 'loading') {
return <Spin size="large" style={{ display: 'block', margin: '50px auto' }} />;
}
// 错误状态显示
if (status === 'failed') {
return <div style={{ color: 'red', textAlign: 'center', margin: '20px' }}>
加载失败: {error}
</div>;
}
return (
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 20 }}>
<Title level={2}>我的任务列表</Title>
<Link to="/tasks/new">
<Button type="primary" icon={<PlusOutlined />}>添加任务</Button>
</Link>
</div>
{/* 筛选工具栏 */}
<Space style={{ marginBottom: 20 }}>
<Button icon={<ReloadOutlined />} onClick={handleRefresh}>刷新</Button>
<Select
placeholder="任务状态"
style={{ width: 120 }}
onChange={(value) => handleFilterChange('status', value)}
value={filter.status}
>
<Option value="all">所有状态</Option>
<Option value="todo">待办</Option>
<Option value="inProgress">进行中</Option>
<Option value="done">已完成</Option>
</Select>
<Select
placeholder="优先级"
style={{ width: 120 }}
onChange={(value) => handleFilterChange('priority', value)}
value={filter.priority}
>
<Option value="all">所有优先级</Option>
<Option value="low">低</Option>
<Option value="medium">中</Option>
<Option value="high">高</Option>
</Select>
</Space>
{/* 任务列表 */}
{filteredTasks.length === 0 ? (
<div style={{ textAlign: 'center', padding: '50px' }}>
<Typography.Text>暂无任务,点击"添加任务"开始创建</Typography.Text>
</div>
) : (
<div>
{filteredTasks.map(task => (
<TaskCard
key={task._id}
task={task}
onToggleComplete={handleToggleComplete}
onDelete={handleDeleteTask}
/>
))}
</div>
)}
</div>
);
};
export default TaskList;
部署与优化:从开发到生产
前端构建优化
# 构建生产版本
npm run build
# 分析构建包大小
npm install source-map-explorer --save-dev
npx source-map-explorer 'build/static/js/*.js'
优化措施:
- 代码分割与懒加载
- 图片优化与CDN加速
- Tree-shaking减小包体积
- 服务端渲染(SSR)提升首屏加载速度
后端部署配置
Docker部署(Dockerfile):
FROM node:16-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install --production
COPY dist/ ./dist/
EXPOSE 5000
CMD ["node", "dist/index.js"]
docker-compose.yml:
version: '3'
services:
backend:
build: ./backend
ports:
- "5000:5000"
environment:
- MONGODB_URI=mongodb://mongo:27017/todoapp
- JWT_SECRET=your_jwt_secret
depends_on:
- mongo
restart: always
frontend:
build: ./frontend
ports:
- "80:80"
depends_on:
- backend
mongo:
image: mongo:latest
ports:
- "27017:27017"
volumes:
- mongo-data:/data/db
volumes:
mongo-data:
性能监控与错误跟踪
错误监控实现:
// src/utils/errorLogger.js
import * as Sentry from '@sentry/react';
if (process.env.NODE_ENV === 'production') {
Sentry.init({
dsn: "https://your-sentry-dsn.example.com",
integrations: [new Sentry.BrowserTracing()],
tracesSampleRate: 0.5,
});
}
export const logError = (error, context = {}) => {
if (process.env.NODE_ENV === 'production') {
Sentry.captureException(error, { extra: context });
} else {
console.error('Error:', error, 'Context:', context);
}
};
项目总结与未来展望
功能回顾与技术要点
本任务管理应用实现了从用户认证到任务管理的完整功能,涵盖:
- 用户系统:注册、登录、JWT认证
- 任务管理:创建、查询、更新、删除任务
- 筛选与排序:按状态、优先级、截止日期筛选排序
- 响应式设计:适配不同设备屏幕尺寸
核心技术要点:
- React组件化开发与Redux状态管理
- Express RESTful API设计
- MongoDB数据模型设计与索引优化
- JWT认证与权限控制
- 前后端分离架构的最佳实践
项目扩展路线图
结语:项目驱动学习的价值
通过本任务管理应用的全栈实现,我们不仅掌握了React、Express、MongoDB等技术的实战应用,更重要的是学会了如何将分散的知识点整合为完整的项目解决方案。这种项目驱动的学习方式,正是The Odin Project课程体系的核心优势。
下一步行动建议:
- 克隆项目仓库:
git clone https://gitcode.com/GitHub_Trending/cu/curriculum.git - 完成项目中的"扩展挑战"任务
- 参与社区代码审查与讨论
- 将实现的功能扩展到个人作品集
记住,编程学习的关键在于持续实践与迭代改进。希望本案例能为你的全栈开发之旅提供有价值的参考!
如果觉得本文对你有帮助,请点赞、收藏并关注获取更多技术干货! 下一篇预告:《React性能优化实战:从理论到实践》
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



