React + Node.js实践 仿B站评论

发布于:2025-03-20 ⋅ 阅读:(23) ⋅ 点赞:(0)

效果展示

在这里插入图片描述

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');
})