前言

继Vue3全家桶仿网易云Demo(概况上一篇文章),我又携带React全家桶仿Eyepetizer | 开眼视频的WebApp来啦~~

“我报名参与金石计划1期挑战——瓜分10万奖池,这是我的第1篇文章,点击查看活动概况”

项目简介

1.运用到的技能栈:

  • React18+React Hooks函数组件代码编写
  • React-Router V6进行路由装备编写
  • Recoil+Recoil-Persist耐久化数据存储)
  • 据守前端MVVM的设计理念,遵循组件化、模块化的编程思想

2.后端:

作用图

项目结构

├─ src
    ├─api                   // 网路请求代码
    ├─assets                // 字体装备及大局款式
    ├─components            // 可复用的 UI 组件
    ├─pages                 // 页面文件
    ├─recoil                // recoil 相关文件
    ├─route                 // 路由装备文件
    ├─utils                 // 工具类函数和相关装备
      App.jsx               // 根组件
      main.jsx              // 进口文件

页面结构

打造一款归于自己的短视频webApp(Vite建立React Hooks+Recoil+Antd)

项目内容

Router V6内容

  • 路由表装备
/* route/index.jsx文件下 */
import React  from 'react';
//路由重定位
import { Navigate } from 'react-router-dom'
//Home组件和其子组件
import Home from  '../pages/Home'
import Recommend from '../pages/Home/Recommend'
......
export default [
    {
        path: '/home',
        element: <Home />,
        children: [
            {
                path: 'recommend',
                element: <Recommend />
            },
            {
                path: 'attention',
                element: <Attention />
            },
            {
                path: 'texts',
                element: <Texts />
            },
        ]
    },
......
    {
        path: '/',
        //重定位
        element: <Navigate to="/home/recommend"></Navigate>
    }
]
/* App.jsx文件下 */
import React from 'react'
import './App.less'
//运用route
import { useRoutes } from 'react-router-dom';
import route from '../src/route'
export default function App() {
  const routes = useRoutes(route)
  return (
    <div className='App'>
      {routes}
    </div>
  )
}

详细代码点这儿

  • 路由的跳转完成
/* <NavLink>是<Link>的一个特定版别,具有多个组件属性,如能够设置高亮作用 */
import { NavLink } from 'react-router-dom';
......
export default function Footer() {
  function getActive({ isActive }) {
    return isActive ? 'Active' : ''
  }
  return (
    <div className="Footer">
      <NavLink className={getActive} to="/home/recommend">首页</NavLink>
      <NavLink className={getActive} to={{ pathname: '/square' }}>广场</NavLink>
  ......
    </div>
  )
}

详细代码点这儿

  • 编程式路由跳转与路由传参
/*
V5 的useHistory现已被V6 的useNavigate替代完成路由跳转
路由传参有以下三种方法:
1.params(需求在路由表声明占位符)
2.search(不需求在路由表声明占位符)
3.state(不需求在路由表声明占位符)
*/
import React, { useEffect, useState, Fragment, useRef } from 'react'
import { NavLink, useNavigate } from 'react-router-dom'
......
export default function Recomment() {
  const navigate = useNavigate()
     //函数式路由跳转,只能用于state传参
    navigate('/details',
      {
        replace: true,
        state: {
          index: index
        }
      }
    )
    }
......    
 /* 接纳参数 */
 import { useLocation } from 'react-router-dom'
 export default function Details() {
......
  //引荐页拿过来的index
  const { state: { index } } = useLocation()
  }

详细代码点这儿

  • 组件间通讯
    1.父组件向子组件通讯
  ......
  return (
    <div className='Recommend'>
      <ListRe recommentEye={recommentEye} handlePlay={handlePlay} />
      ......
    </div>
  )
}
/* 子组件接纳 */
......
export default function listRe(props) {
  const { recommentEye, name, handlePlay } = props
  ......

2.子组件向父组件通讯(在这儿是孙组件状况进步将index传给子组件,子组件再与父组件通讯)

......
//孙组件
export default function videoRe(props) {
  const { handleClassDetail } = props
  return (
    <div className='videoRe'>
     ......
     <i onClick={() => handleClassDetail(index)} className='iconfont icon-bofang'></i>
     ......
    </div >
  )
}
/* 父组件回调函数接纳 */
export default function Details() {
  const { state: { index } } = useLocation()
  let handleClassDetail = async (index) => {
    ......
    setClickVideoState(getClassDetails[index])
  }
  ......
  return (
    <div className='Details' >
      ......
      {/* 子组件 */}
      <Introduction handleClassDetail={handleClassDetail}/>
      ......
    </div>
  )
}

3.兄弟间通讯
兄弟间组件通讯能够将父组件作为桥梁,兄弟组件进行本身的状况进步,从而达到互相通讯的作用。

4.跨级组件通讯(父向孙或许孙以下组件通讯) 运用Provider-Consumer的生产者消费者方法,即可在组件树间进行数据传递。

5.非嵌套联系的组件通讯
页面级或许较为杂乱的数据通讯则需求用到数据状况同享了,这儿我运用的是Recoil

装置Recoil:npm install recoil
将`RecoilRoot`放置在根组件:
......
import {
  RecoilRoot,
  atom,
  selector,
  useRecoilState,
  useRecoilValue,
  useSetRecoilState
} from 'recoil';
......
ReactDOM.createRoot(document.getElementById('root')).render(
    <RecoilRoot>
          <App />
    </RecoilRoot>
)

创建appState.js文件:

import { atom, selector } from "recoil";
......
import { getVideoClass } from '../api'
//视频引荐拿到的引荐列表
export const videoState = atom(
  {
    key: "videoState",
    default: '',
  }
);
..
//依靠的 atom 产生变更时,selector 代表的值会自动更新(相当于getter)
export const powerState = selector({
  key: 'powerState',
  get: async ({ get }) => {
    const res = await getVideoClass({
      userID: get(currentUserIDState),
    });
    return response.name;
  },
})

Recoil的三个Hooks API
1.useRecoilState:相似 useState 的一个 Hook,能够取到 atom 的值以及 setter 函数

 ......
 import { useRecoilState} from "recoil"
export default function Details() {
  ......
  let [getClassDetails, setClassDetails] = useRecoilState(classState)
  let [getClickVideoState, setClickVideoState] = useRecoilState(clickVideoState)
  ......
  let handleClassDetail = async (index) => {
   ......
    setClassDetails(videoData)
    //获取点击的视频概况
    setClickVideoState(getClassDetails[index])
  }
  ......
}

2.useSetRecoilState:只获取 setter 函数,假如只运用了这个函数,状况改变不会导致组件从头烘托

......
import { useSetRecoilState } from "recoil"
export default function Recomment() {
  let setData = useSetRecoilState(videoState)
  let setClassDetails = useSetRecoilState(classState)
   //点击icon播映
  let handlePlay = async (index) => {
    ......
    setClassDetails(videoData)
    setData([...recommentEye, ...recommentCard])
}

3.useRecoilValue:只获取状况

......
import { useRecoilValue} from "recoil"
export default function videoRe(props) {
  ......
  let recommentCard = useRecoilValue(classState)
  return(
      <div className='videoRe'>
      {
        recommentCard.map((item, index) => {
          return (
           ......
          )
        })
      }
    )
    </div >
}

详细代码点这儿

首要页面编写

开眼短视频的UI风格在视觉上十分地简洁明了,并且在许多页面中许多地方的页面结构都是相似的,因此我都把相似的结构抽离出来作为一个组件并进行重复引用。

引荐页和日报页
这两个页面的首要差异:引荐页面第一个烘托的内容是视频方法,而日报页面烘托的第一个内容是图片方法,我把引荐页和日报页作为父组件向一起的子组件传递某个常量值,子组件经过判别常量值来进行区分,并作出相应的烘托。

打造一款归于自己的短视频webApp(Vite建立React Hooks+Recoil+Antd)
打造一款归于自己的短视频webApp(Vite建立React Hooks+Recoil+Antd)

export default function listRe(props) {
......
  return (
                  ......
                {/* 判别是否为第一个视频并判别是否为日报父组件传过来的数据 */}
              {index === 0 && name !== 'Texts' ?
                <Fragment>
                  {
                    <video ref={player} controls autoPlay muted width="100%" onPlay={play} onPause={pause} >
                      <source src={recommentEye[0].data.content.data.playUrl}
                        type="video/webm" />
                    </video>
                  }
                  {
                    Icon === true ? '' :
                      <Fragment>
                        <i onClick={() => handleRePlay(index)} className='iconfont icon-bofang inconVideo'></i>
                        <div className='iconMain'><p>开眼</p><p>精选</p></div>
                      </Fragment>
                  }
                </Fragment> :
                <Fragment>
                  <img src={item.data.content.data.cover.detail} />
                  <i onClick={() => handlePlay(index)} className='iconfont icon-bofang inconVideo'></i>
                  <div className='iconMain'><p>开眼</p><p>精选</p></div>
                </Fragment>
              }
              ......
  )
}

详细代码点这儿

关注页和广场页
这两个页面的一起组件的数据来历仅有的差异为图片大小不一,只需设置为width:100%即可,在图片介绍的文字里,我设置了规则的文字长度,超越必定的文字长度才会去看到“展示”,“收起”的字样;并经过过判别当时点击的index和图片列表的index是否共同,完成“展示”,“收起”的作用。

打造一款归于自己的短视频webApp(Vite建立React Hooks+Recoil+Antd)
打造一款归于自己的短视频webApp(Vite建立React Hooks+Recoil+Antd)
打造一款归于自己的短视频webApp(Vite建立React Hooks+Recoil+Antd)
export default function listVideo(props) {
  ......
  let [isAcitive, setAcitive] = useState(false)
  let [isIndex, setIndex] = useState('')
  function handleActive(index) {
    setAcitive(!isAcitive)
    setIndex(index)
  }
    return (
    <div className='listVideo'>
      <hr />
      {
        listData.map((item, index) => {
          return (
               ......
                <div className="listVideo-video">
                <div style={{ position: 'relative' }}>
                  <i className='iconfont icon-bofang'></i>
                  <img src={item.data.content.data.cover.detail} alt="" />
                </div>
                <p className={isAcitive && index === isIndex ? '' : 'isActive'}>{item.data.content.data.description}</p>
                {
                  item.data.content.data.description.length > 55 ? <h5 onClick={() => handleActive(index)}>{isAcitive && index === isIndex ? '收起' : '打开'}</h5> : ''
                }
              </div>
              ......
          )
         )
}

详细代码点这儿

视频概况页
视频概况页首要有“简介”和“评论”两个子组件,父组件上制作了一个简略的播映器,经过操作原生videoDOM操作来对icon进行点击播映、暂停和加快;点击“简介”组件的视频列表的某个播映按钮更新父组件视频播映的内容,并同步更新相似视频列表。

import React, { useEffect, useState, useRef, useLayoutEffect } from 'react'
......
export default function Details() {
  //获取元素的dom操作
  const player = useRef()
   //播映暂停点击
  let [isPlay, setPlay] = useState(false)
  //处理播映和暂停
  let handlePlay = (value) => {
    !value ? player.current.play() : player.current.pause()
    if (!value && isIcon) {
      clearTimeout(time)
      time = setTimeout(() => {
        setIcon(false)
      }, 3000);
    }
    setPlay(value)
  }
}

优化

  • Recoil引荐运用SuspenseSuspense将会捕获所有异步状况,别的能够配合ErrorBoundary来进行错误捕获。
......
import { Spin } from 'antd';
ReactDOM.createRoot(document.getElementById('root')).render(
      ......
      <React.Suspense fallback={<div className="example"><Spin /></div>}>
        <BrowserRouter>
          <App />
        </BrowserRouter>
      </React.Suspense>
)
  • Recoil引进recoil-persist进行耐久化存储
    Recoil进行数据同享时,每一次刷新都会把数据给重置,因此需求装置recoil-persist插件来进行耐久化本地数据存储。
装置:npm install recoil-persist
(appState.js文件添加)运用:
import { atom, selector } from "recoil";
+ import { recoilPersist } from 'recoil-persist'
+ const { persistAtom } = recoilPersist()
export const videoState = atom(
  {
    key: "videoState",
    default: '',
    + effects_UNSTABLE: [persistAtom],
  }
);
  • 页面烘托优化 Memo
    假如你的组件在相同 props 的情况下烘托相同的成果,那么你能够经过将其包装在React.memo中调用,以此经过回忆组件烘托成果的方法来进步组件的性能体现。这意味着在这种情况下,React将越过烘托组件的操作并直接复用最近一次烘托的成果。
 React.memo(function App(props) {
  /* 运用 props 烘托 */
});

详细代码点这儿

项目遇到的坑

  • useState异步回调的问题
    当一些页面需求当即获取最新的数据的时分,发现运用useState并不能第一次时间更新,后边经过深化了解才知道useState更新状况是异步更新。
  //解决方案:经过事情回调监听数据改变
  let [Icon, setIcon] = useState(false)
  //监听video播映
  let play = (event) => {
    setIcon(true)
  }
  //解决方案:经过useEffect监听,监听到改变(数据变新)后再运用与烘托该数据
  let [listData, setData] = useState([])
  async function getData() {
    ......
    setData(res.data.itemList)
  }
  useEffect(() => {
    getData()
    return () => {
    };
  }, []);
  • 点击播映视频,不及时更新问题
    当点击相似视频列表中的某一个播映视频按钮,其他内容是会及时更新的,比如说视频简介,视频的链接也会及时更新,可是视频的内容并没有及时更新。
//问题原代码:
<video controls width="250">
    <source src="/media/cc0-videos/flower.webm" type="video/webm">
</video>
//解决方法(把source去掉):
<video controls width="250" src="/media/cc0-videos/flower.webm"></video>
  • 运用pushpopsplice等直接更改数组目标的问题
    setState的更新函数会直接替换旧的state,因此运用pushpopsplice等直接更改数组目标是不被答应的。
//解决方案
增:数组解构生成一个新数组,在数组后边加上咱们新增的随机数达成数组新增项
    setData([...recommentEye, ...recommentCard])
删:运用filter数组进行过滤
    let videoData = res.data.itemList.filter((item, index) => {
      return item.type !== 'textCard' && index < 6
    })

总结

本次项目是为了娴熟运用React全家桶,全体的项目内容没有全部完善,例如说一些页面还仅仅静态页面,由于代码是自己手把手去撸出来的,会比较缺少代码优化和规范;可是项目页面结构比较完善,交互功能也比较齐全,需求学习的小伙伴能够把我的项目拉下来将整个项目持续开发下去,肯定会达到实践学习操作的作用哦! (ps:后端接口也不是很完善,许多数据无法全部展示)

源码

项目源码地址:GitHub,欢迎star