前言
继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 // 进口文件
页面结构
项目内容
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风格在视觉上十分地简洁明了,并且在许多页面中许多地方的页面结构都是相似的,因此我都把相似的结构抽离出来作为一个组件并进行重复引用。
引荐页和日报页:
这两个页面的首要差异:引荐页面第一个烘托的内容是视频方法,而日报页面烘托的第一个内容是图片方法,我把引荐页和日报页作为父组件向一起的子组件传递某个常量值,子组件经过判别常量值来进行区分,并作出相应的烘托。
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
是否共同,完成“展示”,“收起”的作用。
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>
......
)
)
}
详细代码点这儿
视频概况页
视频概况页首要有“简介”和“评论”两个子组件,父组件上制作了一个简略的播映器,经过操作原生video
的DOM
操作来对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
引荐运用Suspense
,Suspense
将会捕获所有异步状况,别的能够配合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>
- 运用
push
,pop
,splice
等直接更改数组目标的问题setState
的更新函数会直接替换旧的state
,因此运用push
,pop
,splice
等直接更改数组目标是不被答应的。
//解决方案
增:数组解构生成一个新数组,在数组后边加上咱们新增的随机数达成数组新增项
setData([...recommentEye, ...recommentCard])
删:运用filter数组进行过滤
let videoData = res.data.itemList.filter((item, index) => {
return item.type !== 'textCard' && index < 6
})
总结
本次项目是为了娴熟运用React
全家桶,全体的项目内容没有全部完善,例如说一些页面还仅仅静态页面,由于代码是自己手把手去撸出来的,会比较缺少代码优化和规范;可是项目页面结构比较完善,交互功能也比较齐全,需求学习的小伙伴能够把我的项目拉下来将整个项目持续开发下去,肯定会达到实践学习操作的作用哦! (ps:后端接口也不是很完善,许多数据无法全部展示)
源码
项目源码地址:GitHub,欢迎star