🌟 这是本系列的第一个项目,之后肯定也会陆陆续续出一些其他和课设有关的内容。
📌 此项目是一个博客系统,作为一个刚刚想要前后端兼得的开发工程师。如果没有明确的目标和其他奇思妙想的话,博客系统无疑是最好的开始。
🚀 当然,此项目不仅仅只适合大学生,如果你是个已就业的前端工程师,你想要尝试去开发个一个以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 框架中使用广泛。它使得处理文件上传变得简单且高效。
💽 数据库
- 用的是mongodb,mongodb的安装和下载这里就不再赘述。mac安装mongodb——其他博主的(此处引用,若侵必删)
- 数据库可视化工具用的是mongodb搭配的MongoDB Compass,网上若有其他更好的可视化工具也行,但作者个人觉得这款的界面很清新。Compess的安装——其他博主的(此处引用,若侵必删)
二、页面核心功能演示及部分代码展示
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是我更新此系列最大的动力😭