仿B站评论
效果展示
Node.js后端实现模拟网络请求
- 采用了
Express框架
来构建服务器,用到了cors
中间件来解决跨域问题 - 这里数据在后端仍然是写死的,只是为了来模拟网络请求
//引入express和cors
const express = require('express')
const cors = require('cors')
//创建服务器
const app = express()
//挂载cors中间件
app.use(cors())
// 评论列表数据
const defaultList = [
{
// 评论id
rpid: 3,
// 用户信息
user: {
uid: '13258165',
avatar: 'https://img1.baidu.com/it/u=1464960456,2346146635&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500',
uname: '白芷',
},
// 评论内容
content: '关于他你又会触动',
// 评论时间
ctime: '10-18 08:15',
like: 88,
},
{
rpid: 2,
user: {
uid: '36080105',
avatar: 'https://img2.baidu.com/it/u=3659722672,2310840292&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500',
uname: '海威',
},
content: '为何你停留不走',
ctime: '11-13 11:29',
like: 99,
},
{
rpid: 1,
user: {
uid: '30009257',
avatar:'https://img.rongyuejiaoyu.com/uploads/20240728/02511242750.jpeg',
uname: 'miraculous',
},
content: '让我们红尘作伴',
ctime: '10-19 09:00',
like: 66,
},
]
//使用res.send()方法,向客户端响应一个列表数据
app.get('/comment',(req,res)=>{
res.send({defaultList})
})
//启动web服务器
app.listen(8080,()=>{
console.log('express server running at http://127.0.0.1:8080');
})
静态结构
- 这里是静态页面结构
下面的代码部分并没有数据,也没有渲染列表,只是页面结构
效果展示
代码部分
const App = () => {
return (
<div className="app">
{/* 导航 Tab */}
<div className="reply-navigation">
<ul className="nav-bar">
<li className="nav-title">
<span className="nav-title-text">评论</span>
{/* 评论数量 */}
<span className="total-reply">{10}</span>
</li>
<li className="nav-sort">
{/* 高亮类名: active */}
<span className='nav-item'>最新</span>
<span className='nav-item'>最热</span>
</li>
</ul>
</div>
<div className="reply-wrap">
{/* 发表评论 */}
<div className="box-normal">
{/* 当前用户头像 */}
<div className="reply-box-avatar">
<div className="bili-avatar">
<img className="bili-avatar-img" src={avatar} alt="用户头像" />
</div>
</div>
<div className="reply-box-wrap">
{/* 评论框 */}
<textarea
className="reply-box-textarea"
placeholder="发一条友善的评论"
/>
{/* 发布按钮 */}
<div className="reply-box-send">
<div className="send-text">发布</div>
</div>
</div>
</div>
{/* 评论列表 */}
<div className="reply-list">
{/* 评论项 */}
<div className="reply-item">
{/* 头像 */}
<div className="root-reply-avatar">
<div className="bili-avatar">
<img
className="bili-avatar-img"
alt=""
/>
</div>
</div>
<div className="content-wrap">
{/* 用户名 */}
<div className="user-info">
<div className="user-name">jack</div>
</div>
{/* 评论内容 */}
<div className="root-reply">
<span className="reply-content">这是一条评论回复</span>
<div className="reply-info">
{/* 评论时间 */}
<span className="reply-time">{'2023-11-11'}</span>
{/* 评论数量 */}
<span className="reply-time">点赞数:{100}</span>
<span className="delete-btn">
删除
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
)
}
export default App
tab切换
- 我们在B站评论区通常可以看到两种排序方式,最新或者最热如下图:
- 这里我们需要实现对tab的点击切换功能
- 数据列表我这里用到了React中的
useState
,commentList
是存储数据的数组,setCommentList
是修改commentList
状态变量的一个函数
const [commentList, setCommentList] = useState([]);
排序功能的实现
- 这里用到了一个第三方库-----
lodash
,这个库里面的_.orderBy
方法可以实现排序
按照热度排序
- 根据评论的点赞数进行排序,点赞数多的在上面,点赞数少的在下面
//根据点赞数进行排序
setCommentList(_.orderBy(commentList, "like", "desc"));
//其中的like字段是点赞数的键
按照时间排序
- 根据评论的发布时间进行排序,新发布的评论在上面
//根据时间排序
setCommentList(_.orderBy(commentList, "ctime", "desc"));
//ctime字段是发布时间的键
删除评论功能
- 这里采用一种骗自己的方法(前端筛选删除),利用数组的
filter
方法根据传入的评论id筛出点击删除的评论
const onDelete = (id) => {
console.log(id);
setCommentList(commentList.filter((item) => item.rpid !== id));
};
发布评论功能
- 使用
useState
定义用户输入的内容以及修改内容的函数
const [content, setContent] = useState("");
- 使用
useRef
获取评论输入框的DOM元素
const inputRef = useRef(null);
//绑定到输入区
<textarea
className="reply-box-textarea"
placeholder="发一条友善的评论"
ref={inputRef}
value={content}
onChange={(e)=>setContent(e.target.value)}
/>
- 点击发布后,将文本插入到
commentList
中,清空输入区并且将光标聚焦到文本输入区
这里用到了day.js三方库,用来获取当前时间
还用到uuid三方库,用来随机生成评论的rpid
const onPublish = () => {
//向commentList中插入数据
setCommentList([
...commentList,
{
rpid: uuidV4(), //随机id
user: {
uid: "30009257",
avatar,
uname: "miraculous",
},
content:content,
ctime:dayjs(new Date()).format("MM-DD HH:mm"), //对时间进行格式化
like:52
},
]);
console.log(inputRef);
//清空输入框的内容
setContent('')
//重新聚焦
inputRef.current.focus()
};
组件的封装
- 在项目开发中,我们经常会在不同的地方用到同一个东西,这时候就体现了封装的好处,这里对评论列表中的列表项进行了封装
//item为commentList中的每一项
//onDel是从父组件传过来的删除评论方法
function Item({ item, onDel }) {
return (
<div className="reply-item">
{/* 头像 */}
<div className="root-reply-avatar">
<div className="bili-avatar">
<img className="bili-avatar-img" alt="" src={item.user.avatar} />
</div>
</div>
<div className="content-wrap">
{/* 用户名 */}
<div className="user-info">
<div className="user-name">{item.user.uname}</div>
</div>
{/* 评论内容 */}
<div className="root-reply">
<span className="reply-content">{item.content}</span>
<div className="reply-info">
{/* 评论时间 */}
<span className="reply-time">{item.ctime}</span>
{/* 评论数量 */}
<span className="reply-time">点赞数:{item.like}</span>
{/* 条件:user.id === item.user.id */}
{user.uid === item.user.uid && (
<span className="delete-btn" onClick={() => onDel(item.rpid)}>
删除
</span>
)}
</div>
</div>
</div>
</div>
);
}
完整代码
App.js
import { useEffect, useRef, useState } from "react";
import "./App.scss";
import avatar from "./images/avator1.jpeg";
import axios from "axios";
import _ from "lodash";
import classNames from "classnames";
import { v4 as uuidV4 } from "uuid";
import dayjs from "dayjs";
// 当前登录用户信息
const user = {
// 用户id
uid: "30009257",
// 用户头像
avatar,
// 用户昵称
uname: "miraculous",
};
// 导航 Tab 数组
const tabs = [
{ type: "hot", text: "最热" },
{ type: "time", text: "最新" },
];
// 封装请求数据的Hook
function useGetList() {
// 获取接口数据渲染
const [commentList, setCommentList] = useState([]);
useEffect(() => {
// 请求数据
async function getList() {
// axios请求数据
const res = await axios.get(" http://localhost:8080/comment");
setCommentList(res.data.defaultList);
}
getList();
}, []);
return {
commentList,
setCommentList,
};
}
// 封装Item组件
function Item({ item, onDel }) {
return (
<div className="reply-item">
{/* 头像 */}
<div className="root-reply-avatar">
<div className="bili-avatar">
<img className="bili-avatar-img" alt="" src={item.user.avatar} />
</div>
</div>
<div className="content-wrap">
{/* 用户名 */}
<div className="user-info">
<div className="user-name">{item.user.uname}</div>
</div>
{/* 评论内容 */}
<div className="root-reply">
<span className="reply-content">{item.content}</span>
<div className="reply-info">
{/* 评论时间 */}
<span className="reply-time">{item.ctime}</span>
{/* 评论数量 */}
<span className="reply-time">点赞数:{item.like}</span>
{/* 条件:user.id === item.user.id */}
{user.uid === item.user.uid && (
<span className="delete-btn" onClick={() => onDel(item.rpid)}>
删除
</span>
)}
</div>
</div>
</div>
</div>
);
}
const App = () => {
//渲染评论列表
const { commentList, setCommentList } = useGetList();
//删除功能
const onDelete = (id) => {
console.log(id);
setCommentList(commentList.filter((item) => item.rpid !== id));
};
//tab切换功能
const [type, setType] = useState("hot");
const tabChange = (type) => {
console.log(type);
setType(type);
//基于列表排序
if (type === "hot") {
//根据点赞数进行排序
setCommentList(_.orderBy(commentList, "like", "desc"));
} else {
//根据时间排序
setCommentList(_.orderBy(commentList, "ctime", "desc"));
}
};
//发布评论功能
const [content, setContent] = useState("");
const inputRef = useRef(null);
const onPublish = () => {
setCommentList([
...commentList,
{
rpid: uuidV4(), //随机id
user: {
uid: "30009257",
avatar,
uname: "miraculous",
},
content:content,
ctime:dayjs(new Date()).format("MM-DD HH:mm"), //对时间进行格式化
like:52
},
]);
console.log(inputRef);
//清空输入框的内容
setContent('')
//重新聚焦
inputRef.current.focus()
};
return (
<div className="app">
{/* 导航 Tab */}
<div className="reply-navigation">
<ul className="nav-bar">
<li className="nav-title">
<span className="nav-title-text">评论</span>
{/* 评论数量 */}
<span className="total-reply">{10}</span>
</li>
<li className="nav-sort">
{/* 高亮类名: active */}
{tabs.map((item) => (
<span
key={item.type}
onClick={() => tabChange(item.type)}
className={classNames("nav-item", {
active: type === item.type,
})}
>
{item.text}
</span>
))}
</li>
</ul>
</div>
<div className="reply-wrap">
{/* 发表评论 */}
<div className="box-normal">
{/* 当前用户头像 */}
<div className="reply-box-avatar">
<div className="bili-avatar">
<img className="bili-avatar-img" src={avatar} alt="用户头像" />
</div>
</div>
<div className="reply-box-wrap">
{/* 评论框 */}
<textarea
className="reply-box-textarea"
placeholder="发一条友善的评论"
ref={inputRef}
value={content}
onChange={(e)=>setContent(e.target.value)}
/>
{/* 发布按钮 */}
<div className="reply-box-send">
<div className="send-text" onClick={onPublish}>
发布
</div>
</div>
</div>
</div>
{/* 评论列表 */}
<div className="reply-list">
{/* 评论项 */}
{commentList.map(item => <Item key={item.rpid} item={item} onDel={onDelete} />)}
</div>
</div>
</div>
);
};
export default App;
后端server.js
const express = require('express')
const cors = require('cors')
//创建服务器
const app = express()
app.use(cors())
// 评论列表数据
const defaultList = [
{
// 评论id
rpid: 3,
// 用户信息
user: {
uid: '13258165',
avatar: 'https://img1.baidu.com/it/u=1464960456,2346146635&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500',
uname: '白芷',
},
// 评论内容
content: '关于他你又会触动',
// 评论时间
ctime: '10-18 08:15',
like: 88,
},
{
rpid: 2,
user: {
uid: '36080105',
avatar: 'https://img2.baidu.com/it/u=3659722672,2310840292&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500',
uname: '海威',
},
content: '为何你停留不走',
ctime: '11-13 11:29',
like: 99,
},
{
rpid: 1,
user: {
uid: '30009257',
avatar:'https://img.rongyuejiaoyu.com/uploads/20240728/02511242750.jpeg',
uname: 'miraculous',
},
content: '让我们红尘作伴',
ctime: '10-19 09:00',
like: 66,
},
]
//使用res.send()方法
app.get('/comment',(req,res)=>{
res.send({defaultList})
})
//启动web服务器
app.listen(8080,()=>{
console.log('express server running at http://127.0.0.1:8080');
})