1.主要功能模块:
- 文章管理:CRUD操作
- 用户系统:注册、登录、权限控制
- 评论系统:文章评论功能
-
2.技术栈:
- 前端:React + Ant Design + React Router
- 后端:Express + MongoDB
- 通信:RESTful API + Axios
这个项目采用了典型的MVC架构,前后端完全分离,通过API进行通信。前端负责用户界面和交互,后端负责数据处理和业务逻辑。
3.文件结构
1. blog-project (前端)
---
├── src/
│ ├── App.jsx # 根组件,路由配置
│ ├── App.css # 全局样式
│ ├── main.jsx # 入口文件
│ ├── pages/ # 页面组件
│ │ ├── Home/ # 首页目录
│ │ │ └── index.jsx # 首页组件
│ │ ├── Articles/ # 文章相关页面目录
│ │ │ ├── index.jsx # 文章列表页
│ │ │ └── detail.jsx # 文章详情页
│ │ └── Login/ # 登录页面目录
│ │ └── index.jsx # 登录组件
│ ├── components/ # 可复用组件
│ │ └── Layout/ # 布局组件
│ │ └── index.jsx # 布局组件实现
│ ├── api/ # API接口
│ │ ├── article.js # 文章相关API
│ │ ├── user.js # 用户相关API
│ │ └── request.js # axios请求封装
│ ├── utils/ # 工具函数
│ └── assets/ # 静态资源
---
└── package.json # 项目依赖配置
2. blog-server (后端)
---
├── src/
│ ├── index.js # 服务器入口文件
│ ├── routes/ # 路由定义
│ │ ├── postRoutes.js # 文章路由
│ │ └── userRoutes.js # 用户路由
│ ├── controllers/ # 控制器
│ │ ├── postController.js # 文章控制器
│ │ └── userController.js # 用户控制器
│ ├── models/ # 数据模型
│ │ ├── post.js # 文章模型
│ │ └── user.js # 用户模型
│ ├── middleware/ # 中间件
│ │ └── auth.js # 认证中间件
│ ├── utils/ # 工具函数
│ └── config/ # 配置文件
---
└── package.json # 项目依赖配置
4. 流程原理
1. 整体架构思想
前后端分离架构
- 前端(React + Ant Design)专注于用户界面和交互
- 后端(Express + MongoDB)专注于数据处理和业务逻辑
- 通过 HTTP API 进行通信,实现解耦
MVC 架构模式
- Model(数据模型):MongoDB 的 Schema 定义
- View(视图):React 组件
- Controller(控制器):Express 的路由和控制器
整体工作流程
客户端(React) → 服务端(Express) → 数据库(MongoDB)
-
客户端发起请求
- 用户点击 React Router 的 Link 组件或触发事件
- React 组件通过 axios 发送 HTTP 请求
- 请求经过 axios 拦截器处理(添加 token 等)
-
服务端接收请求
- Express 服务器接收请求
- 通过路由(routes)匹配对应的处理函数
- 控制器(controllers)处理业务逻辑
- 与 MongoDB 数据库交互
-
数据返回流程
- MongoDB 返回查询结果
- Express 处理数据并返回响应
- axios 拦截器处理响应
- React 组件更新状态并重新渲染
2 请求流程示例
用户点击文章列表
↓
React Router 的 Link 组件触发路由变化
↓
React 组件加载,调用 axios 请求
↓
axios 拦截器添加 token
↓
请求发送到 Express 服务器
↓
Express 路由匹配到 postRoutes
↓
控制器调用 MongoDB 查询
↓
数据返回给前端
↓
React 组件更新显示
3 关键组件的工作方式
1. axios 拦截器
- 请求拦截:添加 token、处理请求参数
- 响应拦截:处理响应数据、处理错误
2. Express 路由
- 匹配 URL 和 HTTP 方法
- 调用对应的控制器函数
- 处理请求参数
3. MongoDB 操作
- 通过 Mongoose 模型定义数据结构
- 执行 CRUD 操作
- 返回查询结果
4.错误处理
前端错误:
React 组件 → axios 拦截器 → 显示错误信息
后端错误:
MongoDB 错误 → Express 控制器 → 返回错误响应 → axios 拦截器 → React 组件显示错误
5.具体代码:
前端部分
App.jsx
import { BrowserRouter as Router, Routes, Route, Link, useNavigate } from 'react-router-dom';
import { Layout, Menu, Button } from 'antd';
import './App.css';
import Login from './pages/Login';
import Home from './pages/Home';
import Articles from './pages/Articles';
import { useState } from 'react';// 后续会创建这些组件
const About = () => <div>关于我</div>;const { Header, Content, Footer } = Layout;function App() {const [isLoggedIn, setIsLoggedIn] = useState(!!localStorage.getItem('token'));return (<Router><Layout className="layout"><Header className="header"><div className="logo">我的个人博客</div><Menutheme="dark"mode="horizontal"defaultSelectedKeys={['/']}items={[{key: '/',label: <Link to="/">首页</Link>,},{key: '/articles',label: <Link to="/articles">文章列表</Link>,},{key: '/about',label: <Link to="/about">关于我</Link>,},]}/>{isLoggedIn ? (<Button onClick={() => {localStorage.removeItem('token');setIsLoggedIn(false);}}>退出</Button>) : (<Link to="/login"><Button type="primary">登录</Button></Link>)}</Header><Content><Routes><Route path="/login" element={<Login />} /><Route path="/" element={<Home />} /><Route path="/articles/*" element={<Articles />} /><Route path="/about" element={<About />} /></Routes></Content><Footer style={{ textAlign: 'center' }}>个人博客 ©2024 Created by shandian</Footer></Layout></Router>);
}export default App;
main.jsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import 'antd/dist/reset.css'
import './index.css'ReactDOM.createRoot(document.getElementById('root')).render(<React.StrictMode><App /></React.StrictMode>,
)
article.js
import request from './request';// 获取文章列表
export const getArticles = () => {return request({url: '/posts',method: 'get'});
};// 获取文章详情
export const getArticleById = (id) => {console.log('Calling API for article:', id); // 添加日志return request({url: `/posts/${id}`,method: 'get'});
};// 创建文章
export const createArticle = (data) => {return request({url: '/posts',method: 'post',data});
};// 更新文章
export const updateArticle = (id, data) => {return request({url: `/posts/${id}`,method: 'put',data});
};// 删除文章
export const deleteArticle = (id) => {return request({url: `/posts/${id}`,method: 'delete'});
};// 获取标签列表
export const getTags = () => {return request({url: '/posts/tags',method: 'get'});
};
request.js
import axios from 'axios';
import { message } from 'antd';// 创建axios实例
const request = axios.create({baseURL: 'http://localhost:3000/api',timeout: 5000,headers: {'Content-Type': 'application/json',}
});// 请求拦截器
request.interceptors.request.use(config => {const token = localStorage.getItem('token');if (token) {config.headers.Authorization = `Bearer ${token}`;}// 添加请求日志console.log('Request:', {url: config.url,method: config.method,data: config.data,params: config.params});return config;},error => {console.error('Request Error:', error);return Promise.reject(error);}
);// 响应拦截器
request.interceptors.response.use(response => {// 添加响应日志console.log('Response:', {url: response.config.url,status: response.status,data: response.data});return response.data;},error => {console.error('Response Error:', {url: error.config?.url,status: error.response?.status,data: error.response?.data,message: error.message});if (error.response) {switch (error.response.status) {case 401:message.error('请先登录');localStorage.removeItem('token');window.location.href = '/login';break;case 403:message.error('没有权限');break;case 404:message.error('请求的资源不存在');break;case 500:message.error('服务器错误:' + (error.response.data?.message || '未知错误'));break;default:message.error(`请求错误 (${error.response.status}): ${error.response.data?.message || '未知错误'}`);}} else if (error.request) {message.error('无法连接到服务器,请检查网络连接');} else {message.error('请求配置错误:' + error.message);}return Promise.reject(error);}
);export default request;
user.js
import request from './request';// 用户登录
export const login = (data) => {return request({url: '/users/login',method: 'post',data});
};// 用户注册
export const register = (data) => {return request({url: '/users/register',method: 'post',data});
};// 获取用户信息
export const getUserInfo = () => {return request({url: '/users/info',method: 'get'});
};// 更新用户信息
export const updateUserInfo = (data) => {return request({url: '/user/update',method: 'put',data});
};
index.jsx
import { Outlet } from 'react-router-dom';
import Navigation from '../Navigation';function Layout() {return (<div><Navigation /><main><Outlet /></main></div>);
}export default Layout;
CreateArticle.jsx
import React, { useState } from 'react';
import { Card, Form, Input, Button, Select, message } from 'antd';
import { useNavigate } from 'react-router-dom';
import { createArticle } from '../../api/article';const { TextArea } = Input;const CreateArticle = () => {const [form] = Form.useForm();const [loading, setLoading] = useState(false);const navigate = useNavigate();const onFinish = async (values) => {try {setLoading(true);// 处理标签,将字符串转换为数组const tags = values.tags.split(',').map(tag => tag.trim());await createArticle({...values,tags});message.success('文章创建成功!');navigate('/articles'); // 创建成功后返回文章列表} catch (error) {message.error('创建文章失败:' + (error.message || '未知错误'));} finally {setLoading(false);}};return (<div className="create-article-container"><Card title="创建新文章" className="create-article-card"><Formform={form}layout="vertical"onFinish={onFinish}><Form.Itemname="title"label="文章标题"rules={[{ required: true, message: '请输入文章标题' }]}><Input placeholder="请输入文章标题" /></Form.Item><Form.Itemname="content"label="文章内容"rules={[{ required: true, message: '请输入文章内容' }]}><TextArea rows={15} placeholder="请输入文章内容"style={{ resize: 'none' }}/></Form.Item><Form.Itemname="tags"label="文章标签"help="多个标签请用逗号分隔,如:React,JavaScript,前端"><Input placeholder="请输入标签,用逗号分隔" /></Form.Item><Form.Item><Button type="primary" htmlType="submit" loading={loading} block>发布文章</Button></Form.Item></Form></Card></div>);
};export default CreateArticle;
home.jsx
import React, { useState, useEffect } from 'react';
import { Card, List, Space, Tag, Spin } from 'antd';
import { ClockCircleOutlined, UserOutlined, EyeOutlined } from '@ant-design/icons';
import { Link } from 'react-router-dom';
import { getArticles, getTags } from '../../api/article';
import { getUserInfo } from '../../api/user';
import './index.css';const Home = () => {const [articles, setArticles] = useState([]);const [loading, setLoading] = useState(false);const [userInfo, setUserInfo] = useState(null);const [popularTags, setPopularTags] = useState([]);// 获取最新文章const fetchLatestArticles = async () => {try {setLoading(true);const res = await getArticles({page: 1,pageSize: 5,sort: 'createTime',order: 'desc'});setArticles(res.list);} catch (error) {console.error('获取最新文章失败:', error);} finally {setLoading(false);}};// 获取热门标签const fetchPopularTags = async () => {try {const res = await getTags();setPopularTags(res.slice(0, 10)); // 只显示前10个标签} catch (error) {console.error('获取热门标签失败:', error);}};// 获取用户信息const fetchUserInfo = async () => {try {const res = await getUserInfo();setUserInfo(res);} catch (error) {console.error('获取用户信息失败:', error);}};useEffect(() => {fetchLatestArticles();fetchPopularTags();fetchUserInfo();}, []);return (<div className="home-container"><div className="article-list"><Spin spinning={loading}><ListitemLayout="vertical"size="large"dataSource={articles}renderItem={(item) => (<List.Itemkey={item.id}actions={[<Space><ClockCircleOutlined /> {item.createTime}</Space>,<Space><UserOutlined /> {item.author}</Space>,<Space><EyeOutlined /> {item.views} 次浏览</Space>]}><Card hoverable className="article-card"><Link to={`/article/${item.id}`}><List.Item.Metatitle={<h2>{item.title}</h2>}description={<div><p className="article-summary">{item.summary}</p><Space className="article-tags">{item.tags.map(tag => (<Tag key={tag} color="blue">{tag}</Tag>))}</Space></div>}/></Link></Card></List.Item>)}/></Spin></div><div className="sidebar"><Card title="关于博主" className="about-card">{userInfo ? (<><p>{userInfo.bio || '热爱编程的前端开发者'}</p><p>文章数:{userInfo.articleCount || 0}</p><p>访问量:{userInfo.totalViews || 0}</p></>) : (<p>加载中...</p>)}</Card><Card title="热门标签" className="tags-card"><Space wrap>{popularTags.map(tag => (<Tag key={tag} color="blue"onClick={() => window.location.href = `/articles?tag=${tag}`}style={{ cursor: 'pointer' }}>{tag}</Tag>))}</Space></Card></div></div>);
};export default Home;
login.jsx
import React, { useState } from 'react';
import { Form, Input, Button, Card, message } from 'antd';
import { UserOutlined, LockOutlined, MailOutlined } from '@ant-design/icons';
import { useNavigate } from 'react-router-dom';
import { login, register } from '../../api/user';
import './index.css';const Login = () => {const [isLogin, setIsLogin] = useState(true);const [loading, setLoading] = useState(false);const navigate = useNavigate();const [form] = Form.useForm();const onFinish = async (values) => {try {setLoading(true);if (isLogin) {// 登录请求const res = await login({email: values.email,password: values.password});// 保存tokenlocalStorage.setItem('token', res.token);message.success('登录成功!');navigate('/'); // 登录成功后跳转到首页} else {// 注册请求if (values.password !== values.confirmPassword) {message.error('两次输入的密码不一致!');return;}await register({username: values.username,email: values.email,password: values.password});message.success('注册成功!');setIsLogin(true); // 注册成功后切换到登录界面form.resetFields();}} catch (error) {// 错误已经在request.js中统一处理console.error('操作失败:', error);} finally {setLoading(false);}};const switchMode = () => {setIsLogin(!isLogin);form.resetFields();};return (<div className="login-container"><Card title={isLogin ? "登录" : "注册"} className="login-card"><Formform={form}name="normal_login"className="login-form"onFinish={onFinish}>{!isLogin && (<Form.Itemname="username"rules={[{ required: true, message: '请输入用户名!' },{ min: 3, message: '用户名至少3个字符!' }]}><Input prefix={<UserOutlined />} placeholder="用户名" /></Form.Item>)}<Form.Itemname="email"rules={[{ required: true, message: '请输入邮箱!' },{ type: 'email', message: '请输入有效的邮箱地址!' }]}><Input prefix={<MailOutlined />} placeholder="邮箱" /></Form.Item><Form.Itemname="password"rules={[{ required: true, message: '请输入密码!' },{ min: 6, message: '密码至少6个字符!' }]}><Inputprefix={<LockOutlined />}type="password"placeholder="密码"/></Form.Item>{!isLogin && (<Form.Itemname="confirmPassword"dependencies={['password']}rules={[{ required: true, message: '请确认密码!' },({ getFieldValue }) => ({validator(_, value) {if (!value || getFieldValue('password') === value) {return Promise.resolve();}return Promise.reject(new Error('两次输入的密码不一致!'));},}),]}><Inputprefix={<LockOutlined />}type="password"placeholder="确认密码"/></Form.Item>)}<Form.Item><Button type="primary" htmlType="submit" className="login-form-button"loading={loading}>{isLogin ? '登录' : '注册'}</Button><Button type="link" onClick={switchMode}>{isLogin ? '还没有账号?去注册' : '已有账号?去登录'}</Button></Form.Item></Form></Card></div>);
};export default Login;
后端部分
postController.js
const Post = require('../models/post');// 创建新文章
exports.createPost = async (req, res) => {try {const { title, content, tags } = req.body;// 添加调试日志console.log('userId:', req.userId);console.log('request body:', req.body);const post = new Post({title,content,tags,author: req.userId // 确保这里使用的是 req.userId});const savedPost = await post.save();res.status(201).json({ message: '文章创建成功', post: savedPost });} catch (err) {console.error('Error creating post:', err); // 添加错误日志res.status(400).json({ message: err.message });}
};// 获取所有文章
exports.getPosts = async (req, res) => {try {console.log('Getting posts...'); // 添加调试日志const posts = await Post.find().populate('author', 'email') // 关联作者信息.sort({ createdAt: -1 }); // 按创建时间倒序console.log('Found posts:', posts); // 添加调试日志res.json(posts);} catch (err) {console.error('Error in getPosts:', err); // 添加错误日志res.status(500).json({ message: err.message });}
};// 获取单个文章
exports.getPost = async (req, res) => {try {const post = await Post.findById(req.params.id).populate('author', 'username email');if (!post) {return res.status(404).json({ message: '文章不存在' });}// 增加阅读量post.views = (post.views || 0) + 1;await post.save();res.json(post);} catch (err) {console.error('获取文章详情失败:', err);res.status(500).json({ message: err.message });}
};// 更新文章
exports.updatePost = async (req, res) => {try {const { title, content, tags, status } = req.body;const post = await Post.findById(req.params.id);if (!post) {return res.status(404).json({ message: '文章不存在' });}// 检查是否是文章作者if (post.author.toString() !== req.userId) {return res.status(403).json({ message: '没有权限修改此文章' });}post.title = title || post.title;post.content = content || post.content;post.tags = tags || post.tags;post.status = status || post.status;await post.save();res.json({ message: '文章更新成功', post });} catch (err) {res.status(400).json({ message: err.message });}
};// 删除文章
exports.deletePost = async (req, res) => {try {const post = await Post.findById(req.params.id);if (!post) {return res.status(404).json({ message: '文章不存在' });}if (post.author.toString() !== req.userId) {return res.status(403).json({ message: '没有权限删除此文章' });}await Post.deleteOne({ _id: req.params.id });res.json({ message: '文章删除成功' });} catch (err) {res.status(500).json({ message: err.message });}
};// 获取所有标签
exports.getTags = async (req, res) => {try {const posts = await Post.find();// 获取所有文章的标签,去重const tags = [...new Set(posts.flatMap(post => post.tags))];res.json(tags);} catch (err) {console.error('Error getting tags:', err); // 添加错误日志res.status(500).json({ message: err.message });}
};
userController.js
const jwt = require('jsonwebtoken');
const User = require('../models/User');// 注册新用户
exports.register = async (req, res) => {try {const { username, email, password } = req.body;const user = new User({ username, email, password });await user.save();res.status(201).json({ message: '用户创建成功' });} catch (err) {res.status(400).json({ message: err.message });}
};// 登录
exports.login = async (req, res) => {try {const { email, password } = req.body;// 查找用户const user = await User.findOne({ email });if (!user) {return res.status(401).json({ message: '用户不存在' });}// 验证密码const isMatch = await user.comparePassword(password);if (!isMatch) {return res.status(401).json({ message: '密码错误' });}// 生成 JWT tokenconst token = jwt.sign({ userId: user._id },process.env.JWT_SECRET,{ expiresIn: '24h' });// 返回用户信息和 tokenres.json({token,user: {id: user._id,username: user.username,email: user.email,role: user.role}});} catch (err) {res.status(500).json({ message: err.message });}
};// 获取所有用户
exports.getUsers = async (req, res) => {try {const users = await User.find({}, '-password');res.json(users);} catch (err) {res.status(500).json({ message: err.message });}
};// 获取单个用户
exports.getUser = async (req, res) => {try {const user = await User.findById(req.params.id, '-password');if (!user) {return res.status(404).json({ message: '用户不存在' });}res.json(user);} catch (err) {res.status(500).json({ message: err.message });}
};// 获取用户信息
exports.getUserInfo = async (req, res) => {try {console.log('Getting user info for userId:', req.userId); // 添加调试日志const user = await User.findById(req.userId).select('-password');if (!user) {return res.status(404).json({ message: '用户不存在' });}console.log('Found user:', user); // 添加调试日志res.json(user);} catch (err) {console.error('Error in getUserInfo:', err); // 添加错误日志res.status(500).json({ message: err.message });}
};module.exports = exports;
auth.js
const jwt = require('jsonwebtoken');module.exports = (req, res, next) => {try {// 添加调试日志console.log('Auth Header:', req.headers.authorization);const authHeader = req.headers.authorization;if (!authHeader) {return res.status(401).json({ message: '未提供认证token' });}const token = authHeader.split(' ')[1];console.log('Using JWT_SECRET:', process.env.JWT_SECRET); // 添加调试日志console.log('Extracted token:', token); // 调试日志if (!token) {return res.status(401).json({ message: 'token格式错误' });}// 确保使用正确的 JWT_SECRETconst decoded = jwt.verify(token, process.env.JWT_SECRET);console.log('Decoded token:', decoded); // 调试日志req.userId = decoded.userId;next();} catch (err) {console.error('Auth error:', err); // 错误日志res.status(401).json({ message: '认证失败' });}
};
post.js
const mongoose = require('mongoose');const postSchema = new mongoose.Schema({title: {type: String,required: true,trim: true},content: {type: String,required: true},author: {type: mongoose.Schema.Types.ObjectId,ref: 'User',required: true},tags: [{type: String,trim: true}],status: {type: String,enum: ['draft', 'published'],default: 'published'},views: {type: Number,default: 0}
}, {timestamps: true // 自动添加 createdAt 和 updatedAt 字段
});const Post = mongoose.model('Post', postSchema);module.exports = Post;
user.js
const bcrypt = require('bcryptjs');const mongoose = require('mongoose');const userSchema = new mongoose.Schema({username: {type: String,required: true,unique: true,trim: true,minlength: 3},email: {type: String,required: true,unique: true,trim: true,lowercase: true},password: {type: String,required: true,minlength: 6},avatar: {type: String,default: ''},role: {type: String,enum: ['user', 'admin'],default: 'user'}
}, {timestamps: true // 自动添加 createdAt 和 updatedAt 字段
});userSchema.pre('save', async function(next) {if (this.isModified('password')) {this.password = await bcrypt.hash(this.password, 10);}next();
});userSchema.methods.comparePassword = async function(candidatePassword) {return bcrypt.compare(candidatePassword, this.password);
};const User = mongoose.model('User', userSchema);module.exports = User;
postRoutes.js
const express = require('express');
const router = express.Router();
const postController = require('../controllers/postController');
const auth = require('../middleware/auth');// 公开路由
router.get('/tags', postController.getTags);
router.get('/', postController.getPosts);
router.get('/:id', postController.getPost);// 需要认证的路由
router.post('/', auth, postController.createPost);
router.put('/:id', auth, postController.updatePost);
router.delete('/:id', auth, postController.deletePost);module.exports = router;
userRoutes.js
const express = require('express');
const router = express.Router();
const userController = require('../controllers/userController');
const auth = require('../middleware/auth');// 公开路由
router.post('/register', userController.register);
router.post('/login', userController.login);// 需要认证的路由
router.get('/info', auth, userController.getUserInfo);
router.get('/', auth, userController.getUsers);
router.get('/:id', auth, userController.getUser);module.exports = router;
处理
const express = require('express');
const cors = require('cors');
const mongoose = require('mongoose');
require('dotenv').config();// 修改这一行,使用正确的相对路径
const userRoutes = require('./routes/userRoutes.js'); // 确保加上 .js 扩展名
const postRoutes = require('./routes/postRoutes.js'); // 添加这行// 连接MongoDB数据库
mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/blog').then(() => console.log('MongoDB 连接成功!')).catch(err => console.error('MongoDB 连接失败:', err));const app = express();// 更详细的 CORS 配置
app.use(cors({origin: 'http://localhost:5173', // 您的前端地址methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],allowedHeaders: ['Content-Type', 'Authorization'],credentials: true
}));app.use(express.json());// 测试路由
app.get('/api/test', (req, res) => {res.json({ message: '后端服务器运行正常!' });
});// 在其他路由之前添加
app.get('/api/health', (req, res) => {res.json({ status: 'ok', timestamp: new Date().toISOString() });
});// 使用路由
app.use('/api/users', userRoutes);
app.use('/api/posts', postRoutes); // 添加这行// 启动服务器
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {console.log(`服务器运行在 http://localhost:${PORT}`);
});