【大学生课设抢救计划001】Node + React 实现博客系统 -- 全栈项目

📚 介绍

🌟 这是本系列的第一个项目,之后肯定也会陆陆续续出一些其他和课设有关的内容。

📌 此项目是一个博客系统,作为一个刚刚想要前后端兼得的开发工程师。如果没有明确的目标和其他奇思妙想的话,博客系统无疑是最好的开始。

 🚀 当然,此项目不仅仅只适合大学生,如果你是个已就业的前端工程师,你想要尝试去开发个一个以Node为后端的完整项目,此项目无疑也能给你提供一定的帮助。

 🎯 最重要的是,项目的代码都是进行过封装,具有逻辑性和条理,🙅‍♂️💩⛰️代码!!!

一、相关技术知识

💻 前端

  • React  一个目前比较主流的前端开发框架。
  • Axios  一个基于 Promise 的用于浏览器和 Node.js 的 HTTP 客户端,用于发送 HTTP 请求并处理响应。
  • moment  一款处理时间信息的js库。
  • react-quill  一款比较稳定的在react中适用的富文本编辑器。
  • react-router-dom  一款基于 React 的路由库,用于构建单页面应用(SPA)中的路由系统。
  • sass 一种CSS预处理器,它扩展了标准的CSS语法,为开发者提供了更强大、更灵活的样式表编写方式。

🖥 后端

  • cookie-parser 一个Node.js的中间件,用于解析HTTP请求中的cookie,并将其转换为易于使用的JavaScript对象。
  • express 一个流行的、基于Node.js的Web应用程序框架,用于构建Web和API应用程序。
  • md5 一种常见的哈希算法,用于将任意长度的输入数据(消息)转换成128位(16字节)的固定长度输出,在项目中用于数据的加密
  • jsonwebtoken 就是我们常说的JWT,是一种用于在网络应用间传递信息的开放标准(RFC 7519)。JWT由三部分组成:Header、Payload和Signature。
  • mongodb 一个开源的、面向文档的NoSQL数据库管理系统,它以高性能、高可扩展性和灵活的数据模型而闻名。
  • mongoose 一个在Node.js环境下操作MongoDB数据库的对象模型工具,它提供了一种更简单、更强大的方式来管理数据库的操作。
  • multer 一个用于处理 Node.js 中上传文件的中间件,特别是在 Express 框架中使用广泛。它使得处理文件上传变得简单且高效。

💽 数据库


二、页面核心功能演示及部分代码展示

1️⃣ 登陆 / 注册

/*----------------------前端-----------------------*/
// 注册
import React, { useState } from 'react'
import { Link, useNavigate } from "react-router-dom";
import { register as registerApi } from '../utils/api/users/index.js'
import { message } from "antd";

const Register = () => {
    const [inputs, setInputs] = useState({
        username: "",
        password: "",
        email: ""
    });

    const navigate = useNavigate()
    const handleChange = e => {
        setInputs(previous => ({ ...previous, [e.target.name]: e.target.value }))
    }
    const handleSubmit = e => {
        e.preventDefault()
        registerApi(inputs).then(res => {
            if (res.code === 200) {
                navigate("/login")
                message.success("注册成功")
            } else {
                message.error(res?.msg)
            }
        })
    }

    return (
        <div className='auth'>
            <div className="background"></div>
            <h1>注册</h1>
            <form>
                <input required type='username' placeholder='请输入用户名' name='username' onChange={handleChange}/>
                <input required type='email' placeholder='请输入邮箱' name='email' onChange={handleChange}/>
                <input required type='password' placeholder='请输入密码' name='password' onChange={handleChange}/>
                <button onClick={handleSubmit}>注册</button>
                <span>已有账号?<Link to={'/login'}>前往登陆</Link></span>
            </form>
        </div>
    )
}
export default Register

// 登陆
import React, { useContext, useState } from 'react'
import { Link, useNavigate } from "react-router-dom";
import { UserContext } from "../context/userContext.jsx";
import { message } from "antd";

const Login = () => {
    const [inputs, setInputs] = useState({
        username: "",
        password: "",
    });

    const { login } = useContext(UserContext);
    const navigate = useNavigate()
    const handleChange = e => {
        setInputs(previous => ({ ...previous, [e.target.name]: e.target.value }))
    }
    const handleSubmit = e => {
        e.preventDefault()
        login(inputs).then(res => {
            if (res.code === 200) {
                message.success("登录成功")
                navigate('/')
            }
        })
    }

    return (
        <div className='auth'>
            <div className="background"></div>
            <h1>登陆</h1>
            <form>
                <input required type='username' placeholder='请输入用户名' name='username' onChange={handleChange}/>
                <input required type='password' placeholder='请输入密码' name='password' onChange={handleChange}/>
                <button onClick={handleSubmit}>登陆</button>
                <span>尚未拥有账户?<Link to={'/register'}>前往注册</Link></span>
            </form>
        </div>
    )
}
export default Login

/*----------------------后端-----------------------*/
const express = require('express');
const UserModel = require("../mongo/models/userModel");
const router = express.Router();
const md5 = require('md5');
const jwt = require('jsonwebtoken');
const { JWT_SECRET_KEY } = require('../mongo/config/config')
const apiJSON = require("../utils/jsonCreator");
let { successJSON, errorJSON, notAuthenticatedJSON } = apiJSON()

// 登陆
router.post('/login', function (req, res, next) {
    let { username, password } = req.body;
    UserModel.find({ $and: [{ username: username }, { password: md5(password) }] }, (err, data) => {
        // 报错
        if (err) {
            return res.json(err)
        }
        // 判定用户是否存在
        if (data.length === 1) {
            //创建当前用户的 token
            let token = jwt.sign({
                username: data[0].username,
                _id: data[0]._id
            }, JWT_SECRET_KEY, {
                expiresIn: 60 * 60 * 24 * 7
            });
            res.cookie("access_token", token)
            return res.json(successJSON("登陆成功!", data[0]))
        } else {
            return res.json(errorJSON("用户名或密码错误"))
        }
    })
})

// 注册
router.post('/register', function (req, res, next) {
    let { username, email, password } = req.body;
    UserModel.find({ $or: [{ username: username }, { email: email }] }, (err, data) => {
        // 报错
        if (err) {
            return res.json(err)
        }
        // 用户已存在
        if (data.length) {
            return res.json(
                {
                    code: 500,
                    msg: "用户已存在!"
                }
            )
        }
        // 创建用户
        UserModel.create({
            ...req.body,
            password: md5(password)  //md5对密码进行加密
        }, (err, data) => {
            if (err) {
                res.json({
                    code: 500,
                    msg: '创建失败',
                })
                return false;
            }
            res.json({
                // 响应编号
                code: 200,
                // 响应信息
                msg: '创建成功',
                // 响应的数据
                data: data,
            })
        })
        return true;
    })
})

// 退出登陆
router.post('/logout', function (req, res, next) {
    res.clearCookie("access_token").json({
        code: 200,
        msg: '退出成功'
    })
})

module.exports = router;

2️⃣ 写文章 / 查看文章 / 编辑文章 / 删除文章 

/*----------------------前端-----------------------*/
// 展示文章
import React, { useEffect, useState } from 'react'
import { useLocation, useNavigate } from "react-router-dom";
import { getAllPosts } from "../utils/api/posts/index.js";
import { UrlParamsFilter } from "../utils/tools/urlParamsFilter.js";
import { getHTMLText } from "../utils/tools/getHTMLText.js";

const Home = () => {
    const [posts, setPosts] = useState([]);

    let location = useLocation()
    const navigate = useNavigate()

    useEffect(() => {
        let params = UrlParamsFilter(location.search)

        getAllPosts({ category: params.category }).then(res => {
            if (res?.code === 200) {
                setPosts(res?.data)
            }
        })
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [location]);

    return (
        <div className='home'>
            <div className="posts">
                {posts.map(post => (
                    <div className='post' key={post._id} onClick={() => navigate(`/posts/${post._id}`)}>
                        <div className="img">
                            <img src={`http://localhost:7777/uploads/${post.img}`} alt=""/>
                        </div>
                        <div className="content">
                            <h1>{post.title}</h1>
                            <p>{getHTMLText(post.desc)}</p>
                            <button>进入文章</button>
                        </div>
                    </div>
                ))}
            </div>
        </div>
    )
}
export default Home

// 查看文章/删除文章
import React, { useContext, useEffect, useState } from 'react'
import Edit from '../img/edit.png'
import Delete from '../img/delete.png'
import { Link, useLocation, useNavigate } from "react-router-dom";
import Menu from "../components/Menu.jsx";
import { getPost, deletePost } from "../utils/api/posts/index.js";
import { UserContext } from "../context/userContext.jsx";
import moment from 'moment'
import { getHTMLText } from "../utils/tools/getHTMLText.js";
import {message} from 'antd'
const Post = () => {
    const [postDetail, setPostDetail] = useState({});

    const location = useLocation()
    const postId = location.pathname.split("/")[2]
    const { currentUser } = useContext(UserContext);
    const navigate = useNavigate()

    useEffect(() => {
        moment.locale('zh-cn');
        getPost({ id: postId }).then(res => {
            if (res?.code === 200) {
                setPostDetail(res?.data)
            }
        })
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [location]);

    const handleDelete = () => {
        deletePost(postId).then(res => {
            if (res.code === 200) {
                message.success("删除成功")
                navigate('/')
            }else{
                message.error(res.msg)
            }
        })
    }

    return (
        <div className='single'>
            <div className='content'>
                <img src={`http://localhost:7777/uploads/${postDetail?.post?.img}`}/>
                <div className="user">
                    {
                        postDetail?.author?.protrait
                        &&
                        <img src={postDetail?.author?.protrait} alt=""/>
                    }
                    <div className="info">
                        <div className='username'>{postDetail?.author?.username}</div>
                        <span>{moment(postDetail?.post?.date).fromNow()} 发布</span>
                    </div>
                    {
                        currentUser?.username === postDetail?.author?.username
                        &&
                        <div className='edit'>
                            <Link to={`/write?edit=${postId}`} state={postDetail}>
                                <img src={Edit} alt=""/>
                            </Link>
                            <img onClick={handleDelete} src={Delete} alt=""/>
                        </div>
                    }
                </div>
                <h2>
                    {postDetail?.post?.title}
                </h2>
                {getHTMLText(postDetail?.post?.desc)}
            </div>
            <div className="menu">
                <Menu category={postDetail?.post?.category}/>
            </div>
        </div>
    );
}
export default Post

// 写文章/编辑文章
import React, { useState } from 'react'
import ReactQuill from 'react-quill';
import 'react-quill/dist/quill.snow.css';
import { uploadFile } from "../utils/api/files/index.js";
import { useLocation, useNavigate } from "react-router-dom";
import { addPost, editPost } from "../utils/api/posts/index.js";
import { message } from "antd";

const Write = () => {

    const state = useLocation().state
    const navigate = useNavigate()

    const [value, setValue] = useState(state?.post?.desc || '');
    const [title, setTitle] = useState(state?.post?.title || '');
    const [file, setFile] = useState([]);
    const [category, setCategory] = useState(state?.post?.category || 'life');

    const upload = async () => {
        let formData = new FormData();
        formData.append("file", file.length > 0 ? file[0] : '')
        return await uploadFile(formData)
    }

    const handleSubmit = async e => {
        e.preventDefault()
        let fileUrl;
        if (file.length > 0) {
            await upload().then(res => {
                fileUrl = res?.data
            })
        }
        if (state) {
            let params = {
                _id: state?.post?._id,
                title,
                desc: value,
                img: fileUrl || state?.post?.img,
                category: category,
            }
            editPost(state.post._id, params).then(res => {
                if (res?.code === 200) {
                    message.success(res?.msg)
                } else {
                    message.error(res?.msg)
                }
            })
        } else {
            let params = {
                title,
                desc: value,
                img: fileUrl,
                category: category
            }
            addPost(params).then(res => {
                if (res?.code === 200) {
                    message.success(res?.msg)
                } else {
                    message.error(res?.msg)
                }
            })
        }
        navigate('/')
    }

    return (
        <div className='add'>
            <div className="content">
                <input type='text' value={title} placeholder='请输入标题' onChange={e => setTitle(e.target.value)}/>
                <div className="editContainer">
                    <ReactQuill className='editor' theme="snow" value={value} onChange={setValue}/>
                </div>
            </div>
            <div className="menu">
                <div className="item">
                    <h1>发布文章</h1>
                    <span>
                        <b>状态:</b>{state ? '已发布' : '草稿'}
                    </span>
                    <span>
                        <b>可见:</b>所有人可见
                    </span>
                    <input type='file' id='file' name='file' style={{ display: 'none' }} onChange={(e) => setFile(e.target.files)}/>
                    <label className='file' htmlFor='file'>{file[0]?.name || '选择图片'}</label>
                    <div className="buttons">
                        <button onClick={handleSubmit}>发布</button>
                    </div>
                </div>
                <div className="item">
                    <h1>分类</h1>
                    <div className="category">
                        <input type='radio' checked={category === 'life'} name='category' value='life' id='life' onChange={e => setCategory(e.target.value)}/>
                        <label htmlFor='life'>生活</label>
                    </div>
                    <div className="category">
                        <input type='radio' checked={category === 'article'} name='category' value='article' id='article' onChange={e => setCategory(e.target.value)}/>
                        <label htmlFor='article'>博客</label>
                    </div>
                    <div className="category">
                        <input type='radio' checked={category === 'tour'} name='category' value='tour' id='tour' onChange={e => setCategory(e.target.value)}/>
                        <label htmlFor='tour'>旅游</label>
                    </div>
                    <div className="category">
                        <input type='radio' checked={category === 'food'} name='category' value='food' id='food' onChange={e => setCategory(e.target.value)}/>
                        <label htmlFor='food'>美食</label>
                    </div>
                    <div className="category">
                        <input type='radio' checked={category === 'other'} name='category' value='other' id='other' onChange={e => setCategory(e.target.value)}/>
                        <label htmlFor='other'>其他</label>
                    </div>
                </div>
            </div>
        </div>
    )
}
export default Write

/*----------------------后端-----------------------*/
const express = require('express');
const PostsModel = require("../mongo/models/postsModel");
const apiJSON = require("../utils/jsonCreator");
const UserModel = require("../mongo/models/userModel");
const router = express.Router();
const checkTokenMiddleware = require('../mongo/middlewares/checkTokenMiddleware')

let { successJSON, errorJSON, notAuthenticatedJSON } = apiJSON()

// 获取全部博客
router.get('/', function (req, res, next) {
    const category = req?.query?.category
    // 判断是否有分类筛选
    if (category) {
        PostsModel.find({ category }, function (err, data) {
            if (err) {
                res.json(errorJSON('获取失败'))
            } else {
                res.json(successJSON('获取成功', data))
            }
        })
    } else {
        PostsModel.find(function (err, data) {
            if (err) {
                res.json(errorJSON('获取失败'))
            } else {
                res.json(successJSON('获取成功', data))
            }
        })
    }
});

// 获取单个博客
router.get('/:id', function (req, res, next) {
    PostsModel.findById(req.params.id, function (err, post) {
        if (err) {
            res.json(errorJSON('获取失败'))
        } else {
            UserModel.findById(post.uid, function (err, user) {
                if (err) {
                    res.json(errorJSON('获取失败'))
                } else {
                    res.json(successJSON('获取成功', { post: post, author: user }))
                }
            })
        }
    })
});

// 新增单个博客
router.post('/', checkTokenMiddleware, function (req, res, next) {
    PostsModel.create({
        uid: req.user._id,
        ...req.body
    }, function (err, data) {
        if (err) {
            return res.json(notAuthenticatedJSON('登陆已过期'))
        }
        return res.json(successJSON('新增成功', data))
    })
});

// 删除单个博客
router.delete('/:id', checkTokenMiddleware, function (req, res, next) {
    PostsModel.deleteOne({ $and: [{ uid: req.user._id }, { _id: req.params.id }] }, function (err, data) {
        if (err) return res.json(notAuthenticatedJSON('权限不足'))
        if (data.deletedCount === 0) {
            return res.json(errorJSON('删除失败', data))
        }
        return res.json(successJSON('删除成功', data))
    })
});

// 更新单个博客
router.put('/:id', checkTokenMiddleware, function (req, res, next) {
    PostsModel.updateOne({ _id: req.body._id }, {
        uid: req.user._id,
        ...req.body
    }, function (err, data) {
        if (err) {
            return res.json(notAuthenticatedJSON('登陆已过期'))
        }
        return res.json(successJSON('修改成功', data))
    })
});

module.exports = router;

三、其他技术知识 

1️⃣ axios的使用与封装

/**
 * 网络请求配置
 */
import axios from "axios";

// 请求超时的时间
axios.defaults.timeout = 10000;
// 请求接口
axios.defaults.baseURL = "/api";

/**
 * http request 拦截器
 */
axios.interceptors.request.use(
    (config) => {
        if (config.url !== "/files/uploadFile") {
            config.headers = {
                "Content-Type": "application/json;charset=utf-8",
            };
        }
        return config;
    },
    (error) => {
        return Promise.reject(error);
    }
);

/**
 * http response 拦截器
 */
axios.interceptors.response.use(
    (response) => {
        return response;
    },
    (error) => {
        console.log("请求出错:", error);
    }
);

/**
 * 封装get方法
 * @param url  请求url
 * @param params  请求参数
 * @returns {Promise}
 */
export function get(url, params = {}) {
    return new Promise((resolve, reject) => {
        axios.get(url, {
            params: params,
        }).then((response) => {
            resolve(response?.data)
        })
            .catch((error) => {
                reject(error);
            });
    });
}

/**
 * 封装post请求
 * @param url
 * @param data
 * @returns {Promise}
 */

export function post(url, data) {
    return new Promise((resolve, reject) => {
        axios.post(url, data).then(
            (response) => {
                resolve(response?.data);
            },
            (err) => {
                reject(err);
            }
        );
    });
}

/**
 * 封装delete请求
 * @param url
 * @param data
 * @returns {Promise}
 */

export function apiDelete(url, data) {
    return new Promise((resolve, reject) => {
        axios.delete(url, data).then(
            (response) => {
                resolve(response?.data);
            },
            (err) => {
                reject(err);
            }
        );
    });
}

/**
 * 封装patch请求
 * @param url
 * @param data
 * @returns {Promise}
 */
export function patch(url, data = {}) {
    return new Promise((resolve, reject) => {
        axios.patch(url, data).then(
            (response) => {
                resolve(response.data);
            },
            (err) => {
                msag(err);
                reject(err);
            }
        );
    });
}

/**
 * 封装put请求
 * @param url
 * @param data
 * @returns {Promise}
 */

export function put(url, data = {}) {
    return new Promise((resolve, reject) => {
        axios.put(url, data).then(
            (response) => {
                resolve(response.data);
            },
            (err) => {
                msag(err);
                reject(err);
            }
        );
    });
}

//统一接口处理,返回数据
export default function (method, url, param) {
    return new Promise((resolve, reject) => {
        switch (method) {
            case "get":
                get(url, param)
                    .then(function (response) {
                        resolve(response);
                    })
                    .catch(function (error) {
                        console.log("get request GET failed.", error);
                        reject(error);
                    });
                break;
            case "post":
                post(url, param)
                    .then(function (response) {
                        resolve(response);
                    })
                    .catch(function (error) {
                        console.log("get request POST failed.", error);
                        reject(error);
                    });
                break;
            case "delete":
                apiDelete(url, param)
                    .then(function (response) {
                        resolve(response);
                    })
                    .catch(function (error) {
                        console.log("get request DELETE failed.", error);
                        reject(error);
                    });
                break;
            case "put":
                put(url, param)
                    .then(function (response) {
                        resolve(response);
                    })
                    .catch(function (error) {
                        console.log("get request PUT failed.", error);
                        reject(error);
                    });
                break;
            case "patch":
                patch(url, param)
                    .then(function (response) {
                        resolve(response);
                    })
                    .catch(function (error) {
                        console.log("get request PUT failed.", error);
                        reject(error);
                    });
                break;
            default:
                break;
        }
    });
}

//失败提示
function msag(err) {
    if (err?.response) {
        switch (err.response.status) {
            case 400:
                alert(err.response.data.msg);
                break;
            case 401:
                alert("未授权,请登录");
                break;

            case 403:
                alert("拒绝访问");
                break;

            case 404:
                alert("请求地址出错");
                break;

            case 408:
                alert("请求超时");
                break;

            case 500:
                alert("服务器内部错误");
                break;

            case 501:
                alert("服务未实现");
                break;

            case 502:
                alert("网关错误");
                break;

            case 503:
                alert("服务不可用");
                break;

            case 504:
                alert("网关超时");
                break;

            case 505:
                alert("HTTP版本不受支持");
                break;
            default:
        }
    }
}

2️⃣ express中间件的使用

const jwt = require('jsonwebtoken');
const { JWT_SECRET_KEY } = require('../config/config')
const jsonCreator = require('../../utils/jsonCreator')

const { notAuthenticatedJSON, notLoginJSON } = jsonCreator()

function checkTokenMiddleware(req, res, next) {
    const token = req.cookies?.access_token
    if (!token) return res.json(notLoginJSON('暂未登陆'))

    jwt.verify(token, JWT_SECRET_KEY, (err, userInfo) => {
        if (err) return res.json(notAuthenticatedJSON('登陆已过期'))
        req.user = userInfo;
        next()
    })
}

module.exports = checkTokenMiddleware

四、总结

本项目面向处于燃眉之急🔥的当代大学生和想从前端转向全栈的开发工程师🧑🏻‍💻,若项目中技术使用不当之处或者功能存在漏洞之所,都望各路高人斧正,轻喷。

📖能学到什么

1️⃣ 能学到react基本前端页面的搭建

2️⃣ 能学到实用react-router-dom来管理前端的路由

3️⃣ 能学到封装axios,并使用axios来接通接口

4️⃣ 能学到前后端文件上传的具体细节

5️⃣ 能学到如何用express来搭建后端服务

6️⃣ 能学到如何使用mongodb和mongoose来实现数据库与后端服务的联动

7️⃣ 能学到如何使用jwt来校验用户身份

8️⃣ 能学到如何把信息放入cookie后使用

9️⃣ 能学到如何使用express中间件

🔟 如果👇这个环节🌟一下,就会变成帅哥/美女(不是

🙏🏻乞讨环节

⬇️ 下面这个是github的链接🔗,如果觉得有帮助的,请动动大手,star🌟一波,您的star是我更新此系列最大的动力😭

github地址

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值