搭建动态页面
初始化
安装依赖 npm i axios
创建 axios.js
// axios.js
import axios from "axios";
export const makeRequest = axios.create({
baseURL: 'http://localhost:8800/api/',
withCredentials: true
})
auth
注册
-
创建
state保存所有输入数据- 创建
input的事件处理函数,更新state
// register.jsx export default function Register() { const [inputs, setInputs] = useState({ username: "", email: "", password: "", name: "" }) function handleChange(e) { setInputs(prev => ({...prev,[e.target.name]: e.target.value})); } return ( //............... <input type="text" placeholder='Username' name='username' onChange={handleChange} /> <input type="email" placeholder='Email' name='email' onChange={handleChange} /> <input type="password" placeholder="Password" name='password' onChange={handleChange} /> <input type="text" placeholder='Name' name='name' onChange={handleChange} /> //.................... ) } - 创建
-
提交输入处理逻辑
handleClick逻辑,调用注册接口,提交数据- 创建
err state,保存错误信息 - 如果调用接口失败,则以错误信息更新
err
const [err,setErr] = useState(null); async function handleClick(e) { e.preventDefault(); try { await axios.post('http://localhost:8800/api/auth/register', inputs); } catch(err) { setErr(err.response.data); } } return ( <div className="right"> <h1>Register</h1> <form> {err && err} <button onClick={handleClick}>Register</button> </form> </div> )
登录
之前静态页面中,将 login 的逻辑交给了 authContext。authContext 只是简单地设置了一个本地数据
- 完善
authContext的login函数
const login = async (inputs) => {
const res = await axios.post("http://localhost:8800/api/auth/login", inputs, {
withCredentials: true
});
setCurrentUser(res.data);
}
- 整理
login.jsxinputs state err state像register.jsx那样设置state和事件处理函数
const [inputs, setInputs] = useState({
username: "",
password: ""
});
const [err, setErr] = useState(null);
function handleChange(e) {
setInputs(prev => ({...prev,[e.target.name]: e.target.value}));
}
async function handleLogin(e) {
e.preventDefault();
try {
await login(inputs);
} catch (error) {
setErr(error.response.data);
}
}
// ............................................
<div className="right">
<h1>Login</h1>
<form>
<input type="text" placeholder='Username' name='username' onChange={handleChange} />
<input type="password" placeholder="Password" name='password' onChange={handleChange} />
{err && err}
<button onClick={handleLogin}>Login</button>
</form>
</div>
posts
获取
- 开始使用
react-query,安装最新版npm i @tanstack/react-query- 初始化内容就按照文档的来,引入,创建变量,包裹所有标签
**import { QueryClient, QueryClientProvider, useQuery, } from '@tanstack/react-query';
const queryClient = new QueryClient()**
const Layout = () => {
return (
**<QueryClientProvider client={queryClient}>**
<div className={`theme-${darkMode ? 'dark' : 'light'}`}>
<NavBar />
<div style={{ display: 'flex' }}>
<LeftBar />
<div style={{ flex: 6 }}>
<Outlet />
</div>
<RightBar />
</div>
</div>
**</QueryClientProvider>**
)
}
- 请求接口
渲染列表的数据改为 data
import {useQuery} from '@tanstack/react-query';
import {makeRequest} from 'axios.js';
export default function Posts() {
**const {isLoading, error, data} = useQuery(["posts"], () => {
return makeRequest('/posts').then((res) => {
return res.data;
})
})**
return (
<div className="posts">
{**data**.map(post => (
<Post post={post} key={post.id}/>
))}
</div>
)
}
发帖
-
desc state -
file state -
提交事件处理函数
- 使用
react-query的mutation函数,目的是在 post 成功后刷新一次页面 - 上传文件逻辑,判断是否有上传文件,有则调用
upload()
- 使用
-
代码
**const [file,setFile] = useState(null); const [desc,setDesc] = useState(''); const queryClient = useQueryClient(); const mutation = useMutation((newPost) => { return makeRequest.post('/posts', newPost); }, { onSuccess: () => { queryClient.invalidateQueries(["posts"]); } }) async function upload() { try { const formData = new FormData(); formData.append('file', file); const res = await makeRequest.post('/upload', formData); return res.data; } catch (error) { console.log(error); } } async function handleClick(e) { e.preventDefault(); let imgUrl = ''; if(file) imgUrl = await upload() mutation.mutate({desc, img: imgUrl}); }** //.............. return ( <div className="share"> <input type="text" placeholder={`What's on your mind ${currentUser.name}?`} **onChange={(e) => setDesc(e.target.value)}** /> </div> <hr /> <div className="left"> <input type="file" id="file" style={{display:"none"}} **onChange={e => setFile(e.target.files[0])}** /> </div> <div className="right"> <button onClick={handleClick}>Share</button> </div> </div> ); -
更改之前静态页面的代码,让贴子动态显示发布时间
// post/post.js
<div className="details">
<Link to={`/profile/${post.useId}`}>
<span className='name'>{post.name}</span>
</Link>
**<span className='date'>{moment(post.createdAt).fromNow()}</span>**
</div>
点赞
-
获取该帖子的所有点赞用户ID
- 可以确定点赞数
- 可以判断当前用户是否点赞
-
判断是点赞还是取消点赞,然后发送请求
删帖
state openMenu- 不仅以 openMenu 为依据,还要判断该 post 是属于当前用户,如果不是则不打开
- 发送请求
- 使用突变
comments
获取
- comment 组件接收 prop → postId
- 像 posts 组件那样创建 query comments
- 列表渲染 data
- 动态展示创建时间
export default function Comments({postId}) {
**const {isLoading, error, data} = useQuery(["comments"], () => {
return makeRequest.get("/comments?postId=" + postId).then((res) => {
return res.data;
})
})**
return (
<div className="comments">
<div className="write">
<img src={currentUser.profilePic} alt="" />
<input type="text" placeholder='write a comment' />
<button>Send</button>
</div>
**{isLoading ? "isLoading"
:data.map(comment => (
<div className="comment">
<img src={comment.profilePic} alt="" />
<div className="comment-info">
<span>{comment.name}</span>
<p>{comment.desc}</p>
</div>
<span className='date'>{moment(comment.createdAt).fromNow()}</span>
</div>
))}**
</div>
)
}
添加评论
desc state- 设置突变
- 点击事件处理函数
- 调用接口完后更新
desc为空
- 调用接口完后更新
const [desc, setDesc] = useState("");
const queryClient = useQueryClient();
const mutation = useMutation((newComent) => {
return makeRequest.post('/comments', newComent);
}, {
onSuccess: () => {
queryClient.invalidateQueries(["comments"]);
}
})
async function handleClick(e) {
e.preventDefault();
mutation.mutate({desc, postId});
setDesc('');
}
return (
<div className="comments">
<div className="write">
<img src={currentUser.profilePic} alt="" />
<input type="text" placeholder='write a comment'
**onChange={e => setDesc(e.target.value)}/>**
**<button onClick={handleClick}>Send</button>**
</div>
</div>
)
profile page
- 获取用户信息
- 通过 react-router 的方法获取路由上的 userId
- 更新 jsx
- 将之前静态展示的信息改为动态
- 判断个人页面展示的是否为当前用户信息,是则改 follow 为 update
关注
-
一加载 profile page 就获取 profile host 的跟随者有哪些
- 如果 currentUser 是 profile host 的跟随者,应该将中间 button 内容改为 following
-
关注或者取关
- 判断应该是关注还是取关操作
- 使用突变,在操作后,再次请求跟随者列表以刷新关注状态
-
追加,profile page 的 posts 应显示自己的 post
- 给 api/posts 接口增加 query,即 userId
更新
- 接收 setOpenUpdate,用于关闭 model
state texts cover profile- 图片与文本信息分开
- 图片是文件类型,用 upload 方法
- 调用 api
- 使用突变,在更新后再次请求 user 信息
- 提交,如果有打算上传图片,先请求 upload,再请求更新
后端部分
数据库设计
users
posts
建立外键
comments
建立外键
stories
relationships
保存关注记录的表
likes
记录用户点赞帖子记录的表
api
初始化
进入 api 文件夹
执行命令
npm init -y
npm i express mysql nodemon
创建文件 index.js
在 package.json 文件中增加命令 "start": "nodemon index.js” ,增加属性 “type”: “module”
// index.js
import express from 'express';
import cors from 'cors';
import cookieParser from 'cookie-parser';
const app = express();
app.use(cors({
origin: "http://localhost:3000"
}));
app.use(cookieParser());
app.listen(8800, () => {
console.log('app is listening');
})
路由
创建 api/routes 文件夹
创建文件 users.js posts.js comments.js likes.js auth.js
import express from 'express';
const router = express.Router();
router.get('/test', (req, res) => {
res.send('it works');
})
export default router;
使用路由
// index.js
import express from 'express';
import usersRouter from './routes/users.js';
import postsRouter from './routes/posts.js';
import commentsRouter from './routes/comments.js';
import likesRouter from './routes/likes.js';
import authRouter from './routes/auth.js';
const app = express();
app.use('/api/users', usersRouter);
app.use('/api/posts', postsRouter);
app.use('/api/comments', commentsRouter);
app.use('/api/likes', likesRouter);
app.use('/api/auth', authRouter);
app.listen(8800, () => {
console.log('app is listening');
})
controllers
创建文件夹 api/controllers
创建文件 user.js post.js comment.js like.js auth.js
将对应路由所需要的请求处理逻辑都写在 controller 文件中,导出,路由文件导入使用
数据库
创建文件 api/connect.js
// connect.js
import mysql from 'mysql';
export const db = mysql.createConnection({
host: 'localhost',
user: 'root',
password: 'onlylove245',
database: 'social'
})
为使服务器能接收JSON数据
// index.js
app.use(express.json());
设置响应头
// index.js
app.use((req, res, next) => {
res.header("Access-Control-Allow-Credentials", true);
next();
})
auth路由处理请求逻辑
注册
- 首先检测是否已经存在该用户
- 如果已存在则响应已存在的错误信息
- 如果未存在则继续
- 接着加密密码
- 安装依赖
npm i bcryptjs - 生成加密密码
- 安装依赖
- 插入数据
export const register = (req, res) => {
// 首先检查用户是否已经存在
const query = 'select * from users where username = ?';
db.query(query, [req.body.username], (err, data) => {
if (err) return res.status(500).json(err);
if (data.length) return res.status(409).json('User already exists!');
// 创建新用户对象
// 加密密码
const salt = bcrypt.genSaltSync(10);
const hashedPassword = bcrypt.hashSync(req.body.password, salt);
const query = 'insert into users (`username`, `email`, `password`, `name`) value (?)';
const values = [req.body.username, req.body.email, hashedPassword, req.body.name];
db.query(query, [values], (err, data) => {
if (err) return res.status(500).json(err);
return res.status(200).json('User has been created');
})
})
}
登录
-
检查用户是否存在
-
验证密码
-
设置cookie
- 安装依赖
npm i jsonwebtoken cookie-parser cors
// index.js app.use(cors()); app.use(cookieParser());- 使用 jsonwebtoken
- 安装依赖
export const login = (req, res) => {
const query = 'select * from users where username = ?';
db.query(query, [req.body.username], (err, data) => {
if (err) return res.status(500).json(err);
if (data.length === 0) return res.status(404).json('User not exists!');
// 验证密码
const checkPassword = bcrypt.compareSync(req.body.password, data[0].password);
if (!checkPassword) return res.status(400).json('Wrong password or username');
const token = jwt.sign({ id: data[0].id }, 'secretkey');
const { password, ...others } = data[0];
res.cookie('accessToken', token, {
httpOnly: true
}).status(200).json(others);
})
}
登出
- 清除cookie
export const logout = (req, res) => {
res.clearCookie('accessToken', {
secure: true,
sameSite: 'none'
}).status(200).json('User has been logged out.');
}
posts路由逻辑
路由路径
// routes/posts.js
router.get('/', getPosts);
router.post('/', addPost);
获取用户相关的帖子信息
- 验证 token
- 查询数据库
- 连接了三个表 users posts relationships,目的是找出自己发的贴子和关注用户发的贴子
- 追加,如果有 query userId,说明只要该用户的 posts
export const getPosts = (req, res) => {
const userId = req.query.userId;
const token = req.cookies.accessToken;
if (!token) return res.status(401).json('Not logged in');
jwt.verify(token, 'secretKey', (err, userInfo) => {
if (err) return res.status(403).json('Token is not valid!');
const query = userId !== 'undefined' ? `select p.*, u.id as userId, name, profilePic from posts as p join users as u on (u.id = p.userId)
where p.userId = ?`
: `select p.*, u.id as userId, name, profilePic from posts as p join users as u on (u.id = p.userId)
left join relationships as r on (p.userId = r.followedUserId) where r.followerUserId = ? or p.userId = ?
order by p.createdAt desc`;
const values = userId !== 'undefined' ? [userId] : [userInfo.id, userInfo.id];
db.query(query, values, (err, data) => {
if (err) return res.status(500).json(err);
return res.status(200).json(data);
})
})
}
发帖
-
验证 token
-
上传文件
- 安装 multer 依赖
npm i multer - 默认将上传的文件放入 client/public/upload 文件夹中
- api 返回结果为图片在本地的相对路径
// index.js const storage = multer.diskStorage({ destination: (req, file, cb) => { cb(null, '../client/public/upload') }, filename: (req, file, cb) => { cb(null, Date.now + file.originalname); } }) const upload = multer({ storage: storage }); app.use('/api/upload', upload.single('file'), (req, res) => { const file = req.file; res.status(200).json('./upload/' + file.filename); }) - 安装 multer 依赖
-
插入数据库
- 安装 moment 依赖
npm i moment
- 安装 moment 依赖
// controlles/post.js
export const addPost = (req, res) => {
const token = req.cookies.accessToken;
if (!token) return res.status(401).json('Not logged in');
jwt.verify(token, 'secretKey', (err, userInfo) => {
if (err) return res.status(403).json('Token is not valid!');
const query = `insert into posts ("desc","img","createdAt","userId") values ?`;
const values = [
req.body.desc,
req.body.img,
moment(Date.now()).format("YYYY-MM-DD HH:mm:ss"),
userInfo.id
]
db.query(query, [values], (err, data) => {
if (err) return res.status(500).json(err);
return res.status(200).json('post has been created');
})
})
}
删除
export const deletePost = (req, res) => {
const token = req.cookies.accessToken;
if (!token) return res.status(401).json('Not logged in');
jwt.verify(token, 'secretKey', (err, userInfo) => {
if (err) return res.status(403).json('Token is not valid!');
const query = 'delete from posts where `id` = ? and `userId` = ?';
db.query(query, [req.params.id, userInfo.id], (err, data) => {
if (err) return res.status(500).json(err);
if(data.affectedRows > 0) return res.status(200).json('Post has been deleted.');
return res.status(403).json('You can delete only your post');
})
})
}
comments 路由逻辑
路由路径
// routes/comments.js
router.get('/', getComments);
router.post('/', addComment);
获取该帖子的评论
// controllers/comment.js
export const getComments = (req, res) => {
const query = `SELECT c.*, u.id AS userId, name, profilePic FROM comments AS c JOIN users AS u ON (u.id = c.userId)
WHERE c.postId = ? ORDER BY c.createdAt DESC
`;
db.query(query, [req.query.postId], (err, data) => {
if (err) return res.status(500).json(err);
return res.status(200).json(data);
})
}
添加评论
- 验证身份
- 插入
export const addComment = (req, res) => {
const token = req.cookies.accessToken;
if (!token) return res.status(401).json('Not logged in');
jwt.verify(token, 'secretKey', (err, userInfo) => {
if (err) return res.status(403).json('Token is not valid!');
const query = "INSERT INTO comments(`desc`, `createdAt`, `userId`, `postId`) VALUES (?)";
const values = [
req.body.desc,
moment(Date.now()).format("YYYY-MM-DD HH:mm:ss"),
userInfo.id,
req.body.postId
]
db.query(query, [values], (err, data) => {
if (err) return res.status(500).json(err);
return res.status(200).json('Comment has been created');
})
})
}
like路由逻辑
获取该帖子的所有点赞用户ID
export const getLikes = (req, res) => {
const query = 'select userId from likes where postId = ?';
db.query(query, [req.query.postId], (err, data) => {
if (err) return res.status(500).json(err);
return res.status(200).json(data.map(like => like.userId));
})
}
点赞
export const addLike = (req, res) => {
const token = req.cookies.accessToken;
if (!token) return res.status(401).json('Not logged in');
jwt.verify(token, 'secretKey', (err, userInfo) => {
if (err) return res.status(403).json('Token is not valid!');
const query = 'insert into likes (`userId`,`postId`) values (?)';
const values = [
userInfo.id,
req.body.postId
]
db.query(query, [values], (err, data) => {
if (err) return res.status(500).json(err);
return res.status(200).json('Post has been liked');
})
})
}
取消点赞
export const cancelLike = (req, res) => {
const token = req.cookies.accessToken;
if (!token) return res.status(401).json('Not logged in');
jwt.verify(token, 'secretKey', (err, userInfo) => {
if (err) return res.status(403).json('Token is not valid!');
const query = 'delete from likes where `userId` = ? and `postId` = ?';
const values = [
userInfo.id,
req.body.postId
]
db.query(query, [values], (err, data) => {
if (err) return res.status(500).json(err);
return res.status(200).json('Like has been canceled');
})
})
}
relationships路由逻辑
获取
获取当前用户的跟随者ID
export const getRelationships = (req, res) => {
const query = 'select followerUserId from relationships where followedUserId = ?';
db.query(query, [req.query.followedUserId], (err, data) => {
if (err) return res.status(500).json(err);
return res.status(200).json(data.map(relationship => relationship.followerUserId));
})
}
user路由逻辑
路由路径
router.get('/find/:userId', getUser);
router.put('/', updateUser);
获取用户信息
export const getUser = (req, res) => {
const userId = req.params.userId;
const query = 'select * from users where id = ?';
db.query(query, [userId], (err, data) => {
if (err) return res.status(500).json(err);
const { password, ...info } = data[0];
return res.json(info);
})
}
更新用户信息
export const updateUser = (req, res) => {
const token = req.cookies.accessToken;
if (!token) return res.status(401).json('Not logged in');
jwt.verify(token, 'secretKey', (err, userInfo) => {
if (err) return res.status(403).json('Token is not valid!');
const query = 'update users set `name`=?,`city`=?,`website`=?,`profilePic`=?,`coverPic`=? where id=?';
db.query(query, [
req.body.name,
req.body.city,
req.body.website,
req.body.profilePic,
req.body.coverPic,
userInfo.id
], (err, data) => {
if (err) return res.status(500).json(err);
if (data.affectedRows > 0) return res.status(200).json('Updated!');
return res.status(403).json('You can only update your profile');
})
})
}