【综合案例】使用React编写B站评论案例
一、效果展示
二、效果说明
页面上默认有3条评论,且一开始进入页面的时候是按照点赞数量进行倒序排列展示,可以点击【最热 、最新】进行排序的切换。
在文本框中输入要评论的文本,然后点击【发布】按钮,即可将评论添加到下方的评论列表当中进行展示;如果没有输入任何文本的时候直接点击【发布】按钮会弹出提示对话框。
点击删除按钮可以将对应的评论从评论列表中移除。
三、涉及知识点
3.1 useState
3.1.1 基础使用
useState 是一个 React Hook(函数),它允许我们向组件添加一个状态变量, 从而控制影响组件的渲染结果。
🚩 语法:
const [state, setState] = useState(initialState)
- useState是一个函数,返回值是一个数组
- 数组中的第一个参数是状态变量;第二个参数是set函数,用来修改状态变量
- useState的参数将作为state的初始值
🚩 本质:
和普通JS变量不同的是,状态变量一旦发生变化组件的视图UI也会跟着变化(数据驱动视图)。
注意事项:
useState
是一个 Hook,因此你只能在 组件的顶层 或自己的 Hook 中调用它。你不能在循环或条件语句中调用它。如果你需要这样做,请提取一个新组件并将状态移入其中。- 在严格模式中,React 将 两次调用初始化函数,以 帮你找到意外的不纯性。这只是开发时的行为,不影响生产。如果你的初始化函数是纯函数(本该是这样),就不应影响该行为。其中一个调用的结果将被忽略。
3.1.2 修改状态的规则
🚩 状态不可变
在React中,状态被认为是只读的,我们应该始终
替换它而不是修改它,
直接修改状态不能引发视图更新。
🚩 修改对象状态
规则:对于对象类型的状态变量,应该始终传给set方法一个
全新的对象
来进行修改。
3.2 classnames优化类名控制
classnames是一个简单的JS库,可以非常方便的
通过条件动态控制class类名的显示。
现在的问题:字符串的拼接方式不够直观,也容易出错。
3.3 受控表单绑定
概念:使用React组件的状态(useState)控制表单的状态。
1. 准备一个React状态值
const [value, setValue] = useState('')
2. 通过value属性绑定状态,通过onChange属性绑定状态同步的函数
// 通过value属性绑定react状态
// 绑定onChange事件,通过事件参数e拿到输入框最新的值,反向修改到react状态
<input
type="text"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
3.4 获取DOM
在React组件中获取 / 操作DOM,需要使用useRef React Hook钩子函数,分为两步:
1. 使用useRef创建 ref 对象,并与 JSX 绑定
2. 在DOM可用时,通过 inputRef.current 拿到 DOM 对象
四、代码实现
4.1 逻辑渲染层
import { useRef, useState } from "react";
import './App.scss'
import avatar from './image/bozai.png'
import dayjs from 'dayjs'
import { v4 as uuidV4 } from 'uuid'
import _ from 'lodash'
import classNames from 'classnames'
function App() {
// 当前登录用户信息
const user = {
// 用户id
uid: '30009257',
// 用户头像
avatar,
// 用户昵称
uname: '嘟嘟嘟',
}
// 评论列表数据
const defaultList = [
{
// 评论id
rpid: 3,
// 用户信息
user: {
uid: '13258165',
avatar: require('./image/panda.jpg'),
uname: '周杰伦',
},
// 评论内容
content: '哎哟,不错哦',
// 评论时间
ctime: '10-18 08:15',
like: 88,
},
{
rpid: 2,
user: {
uid: '36080105',
avatar: require('./image/panda.jpg'),
uname: '许嵩',
},
content: '我寻你千百度 日出到迟暮',
ctime: '11-13 11:29',
like: 88,
},
{
rpid: 1,
user: {
uid: '30009257',
avatar,
uname: '黑马前端',
},
content: '学前端就来黑马',
ctime: '10-19 09:00',
like: 66,
},
]
// 导航 Tab 数组
const tabs = [
{ type: 'hot', text: '最热' },
{ type: 'time', text: '最新' },
]
// 最初的时候先按照最热进行倒叙排列
const [remarkList, setRemark] = useState(_.orderBy(defaultList, 'hot', 'desc'))
const [content, setContent] = useState('')
const inputRef = useRef(null)
// 发布评论
function handleSubmit() {
// 阻止提交空数据
if (!content) return alert('请输入评论内容')
setRemark([
{
rpid: uuidV4(),//随机id
user,
content,
ctime: dayjs(new Date()).format('MM-DD HH:mm'),
like: 0,
},
...remarkList
])
setContent('')//清空输入框中的内容
inputRef.current?.focus()//重新聚焦
}
// 删除评论
function handleDel(item) {
setRemark(remarkList.filter(v => v.rpid !== item.rpid))
}
const [type, setType] = useState('hot')
/**
* tab切换
* 1.点击谁就把谁的type记录下来
* 2.通过记录的type和每一项遍历时的type做匹配,控制激活类名的显示
*/
function handleTabChange(type) {
setType(type)
if (type === 'time') {
// 按照时间倒序
setRemark(_.orderBy(remarkList, 'ctime', 'desc'))
} else {
// 按照点赞数倒序
setRemark(_.orderBy(remarkList, 'like', 'desc'))
}
}
return (
<div className="App">
<div className="top">
<div className="left">
<span className="l-title">评论</span>
<span>{remarkList.length}</span>
</div>
<div className="right">
{/* 高亮类名 */}
{/*
classnames优化类名控制
classnames是一个简单的JS库,可以非常方便的通过条件动态控制class类型的显示
*/}
{tabs.map((item) => {
return (
<span
className={classNames('nav-item', { active: type === item.type })}
key={item.type}
onClick={() => handleTabChange(item.type)}>{item.text}</span>
)
})}
</div>
</div>
<div className="push">
<img className="avatar" src={user.avatar} />
<textarea className="textarea" placeholder="发一条友善的评论" ref={inputRef} value={content} onChange={(e) => setContent(e.target.value)}></textarea>
<button className="pushBtn" onClick={handleSubmit}>发布</button>
</div>
<div className="main">
{/* 评论项 */}
{remarkList.map((item) => {
return (
<div className="m-item" key={item.rpid}>
<img className="avatar" src={item.user.avatar} />
<div className="mi-right">
<div>{item.user.uname}</div>
<div className="text">{item.content}</div>
<div >
<span>{item.ctime}</span>
<span className="like">点赞数:{item.like}</span>
<span onClick={() => handleDel(item)}>删除</span>
</div>
</div>
</div>
)
})}
</div>
</div>
);
}
export default App;
4.2 样式层
.App{
margin: 10px;
}
.top{
margin-bottom: 20px;
display: flex;
align-items: baseline;
font-size: 15px;
color: #999;
.left{
margin-right: 50px;
.l-title{
font-size: 20px;
font-weight: 700;
color: #000;
margin-right: 5px;
}
}
.right{
display: flex;
flex-direction: row;
align-items: center;
.nav-item {
cursor: pointer;
&:hover {
color: #00aeec;
}
&:last-child::after {
display: none;
}
&::after {
content: ' ';
display: inline-block;
height: 10px;
width: 1px;
margin: -1px 12px;
background-color: #9499a0;
}
}
.nav-item.active{
color: #000;
}
}
}
.push{
margin-bottom: 20px;
display: flex;
.textarea{
margin: 0 10px;
padding: 10px;
flex: 1;
min-height: 30px;
max-height: 100px;
border-radius: 10px;
border: none;
outline: none;
background-color: #ebebeb;
}
.pushBtn{
width: 100px;
height: 50px;
font-size: 16px;
color: #fff;
border: none;
border-radius: 5px;
background-color: rgba(0,174,236,0.5);
&:hover{
background-color: rgba(0,174,236);
}
}
}
.avatar{
width: 40px;
height: 40px;
border-radius: 50%;
}
.m-item{
display: flex;
margin-bottom: 10px;
.mi-right{
padding-bottom: 10px;
margin-left: 10px;
flex: 1;
font-size: 14px;
color: #999;
border-bottom: 1px solid #e4e3e3;
}
.text{
margin: 10px 0;
color: #000;
}
.like{
margin: 0 20px;
}
}