【综合案例】使用React编写B站评论案例

一、效果展示

默认效果,一开始默认按照最热进行排序 发布了一条评论 按照最新进行排序 按照最新进行排序

二、效果说明

页面上默认有3条评论,且一开始进入页面的时候是按照点赞数量进行倒序排列展示,可以点击【最热 、最新】进行排序的切换。

在文本框中输入要评论的文本,然后点击【发布】按钮,即可将评论添加到下方的评论列表当中进行展示;如果没有输入任何文本的时候直接点击【发布】按钮会弹出提示对话框。

点击删除按钮可以将对应的评论从评论列表中移除。

三、涉及知识点

3.1 useState

3.1.1 基础使用

useState 是一个 React Hook(函数),它允许我们向组件添加一个状态变量, 从而控制影响组件的渲染结果。

🚩 语法:

javascript 复制代码
const [state, setState] = useState(initialState)
  1. useState是一个函数,返回值是一个数组
  2. 数组中的第一个参数是状态变量;第二个参数是set函数,用来修改状态变量
  3. 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状态值
javascript 复制代码
const [value, setValue] = useState('')
  1. 通过value属性绑定状态,通过onChange属性绑定状态同步的函数
html 复制代码
// 通过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 绑定
  1. 在DOM可用时,通过 inputRef.current 拿到 DOM 对象

四、代码实现

4.1 逻辑渲染层

javascript 复制代码
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 样式层

css 复制代码
.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;
}
}
相关推荐
崔庆才丨静觅3 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60614 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了4 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅4 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅5 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅5 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment5 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅6 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊6 小时前
jwt介绍
前端
爱敲代码的小鱼6 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax