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

2 篇文章 0 订阅
1 篇文章 0 订阅

前言

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

项目简介

1.使用到的技术栈:

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

2.后端:

  • 🙏感觉大佬提供的在线开源api接口➡️GitHub

效果图

效果图.gif

项目结构

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

页面结构

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-H7dOKXFd-1664428704728)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/901487a722724296a4216cd3b82dff99~tplv-k3u1fbpfcp-watermark.image?)]

项目内容

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风格在视觉上非常地简洁明了,并且在许多页面中很多地方的页面结构都是类似的,因此我都把类似的结构抽离出来作为一个组件并进行重复引用。

推荐页和日报页
这两个页面的主要区别:推荐页面第一个渲染的内容是视频形式,而日报页面渲染的第一个内容是图片形式,我把推荐页和日报页作为父组件向共同的子组件传递某个常量值,子组件通过判断常量值来进行区分,并作出相应的渲染。
推荐页.pngimage.png

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是否一致,实现“展示”,“收起”的效果。

关注页.png 广场页.png 关注页面jif.gif
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进行点击播放、暂停和加速;点击“简介”组件的视频列表的某个播放按钮更新父组件视频播放的内容,并同步更新类似视频列表。

视频详情页.gif
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😘

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值