仿B站评论
- 效果展示
- Node.js后端实现模拟网络请求
- 静态结构
- 效果展示
- 代码部分
- tab切换
- 排序功能的实现
- 按照热度排序
- 按照时间排序
- 删除评论功能
- 发布评论功能
- 组件的封装
- 完整代码
- App.js
- 后端server.js
效果展示
Node.js后端实现模拟网络请求
- 采用了
Express框架
来构建服务器,用到了cors
中间件来解决跨域问题 - 这里数据在后端仍然是写死的,只是为了来模拟网络请求
//引入express和cors
const express = require('express')
const cors = require('cors')
//创建服务器
const app = express()
//挂载cors中间件
app.use(cors())// 评论列表数据
const defaultList = [{// 评论idrpid: 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">{/* 评论框 */}<textareaclassName="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"><imgclassName="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);//绑定到输入区
<textareaclassName="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(), //随机iduser: {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 = {// 用户iduid: "30009257",// 用户头像avatar,// 用户昵称uname: "miraculous",
};// 导航 Tab 数组
const tabs = [{ type: "hot", text: "最热" },{ type: "time", text: "最新" },
];// 封装请求数据的Hookfunction 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(), //随机iduser: {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) => (<spankey={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">{/* 评论框 */}<textareaclassName="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 = [{// 评论idrpid: 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');
})