前言
抖音是现在最热门的软件之一,在人们之间非常的普及。我就想着能不能模仿下抖音,正好我现在正在学react. 于是我就用react模仿一个抖音app。
展示
前期准备
- 第一步 npm init @vitejs/app 初始化脚手架 输入项目名 选择react开发框架
- 第二部 安装一些我们制作项目需要的依赖
- axios 它是一个基于 promise 的网络请求库,用于获取后端数据,是前端常用的数据请求工具;
- react-router react-router-dom 它为我们的项目提供路由跳转功能。
- antid-mobile 可以为我们提供样式组件与图标 我们可以直接使用里面封装的小组件
- "styled-components" 为我们提供一种新的书写css样式的格式——css in js 我们可以再js文件中书写css 这样书写使得代码相互嵌套,使得更加容易理解。
- swiper 制作制作幻灯片效果 。
正文
组建设计思路
我们要做的是抖音的首页 也就是大家看得最多的页面 大家对这个页面应该非常的熟悉吧! 在这个页面我们可以划分为六个组件模块
- 第一个组件Home组件,这个组件是所有组件的父组件,用来搭载其他子组件。
- 第二个组件是Video组件,用来播放视频。
- 第三个组件是Video_Footer组件,是video的子组件用来些视频的一些说明文字。
- 第四个组件是Video_Sidebar组件,这也是Video的子组件,用来写旁边的点赞评论等数据。
- 第五个组件是Bottom组件,这是底部主导航组件。
- 第六个组件是Header组件,这是头部导航组件。
下图是我制作项目搭建的目录 Bottom 与 Header 放入了components目录下,Home 组件下有video组件 video组件下又放入了Video_Footer组件与Video_Sidebar组件。接下来详细介绍一下这些组件吧!
video组件
这个组件包括一个video_player视频播放器,Video_Footer视频描述组件和Video_Sidebar视频点赞收藏等组件,这个组件需要用到useState 和 useRef 从react中解构出来
import React, { useState, useRef } from 'react'
import { Wrapper } from './style.js'
import VideoFooter from './Video_footer'
import Video_sidebar from './Video_sidebar'
export default function Video({ videos, user, description, song, hearts, comments, collects, share, users, cd }) {
const videoRef = useRef(null)
const [play, setPlay] = useState(false)
const onVideo = () => {
if (play) {
videoRef.current.pause();
setPlay(false)
}
else {
videoRef.current.play();
setPlay(true)
}
}
return (
<Wrapper>
<div className='video'>
{/* <Header/> */}
<video
className='video_player'
loop
onClick={onVideo}
ref={videoRef}
src={videos}
>
</video>
<VideoFooter //底部介绍组件
user={user}
description={description}
song={song}
cd={cd}
/>
<Video_sidebar //右侧点赞组件
hearts={hearts}
comments={comments}
collects={collects}
share={share}
users={users} />
</div>
</Wrapper>
)
}
- 我们将我们获取的视频放入到video标签中,设置一个类名,接着设置一个点击事件,onVideo函数来控制视频的暂停与继续。
- useRefuseRef是一个方法,且useRef返回一个可变的ref对象,返回的ref在整个周期中都是不变的,它只有一个current这一个属性。并且ref对象的值发生改变之后,不会触发组件重新渲染。这有一个窍门,把它的改变动作放到useState()之前,因为用useState时当应用程序重新渲染时,渲染值会发生变化,这再次导致应用程序重新渲染。所有我们需要useRef来获取不变的ref,进而通过current属性来控制暂停与播放。这是Video组件的核心功能。
Video_Sidebar组件
如图,该组件是用来显示旁边的用户头像的,点赞,收藏,转发等操作的组件。
这里我们需要注意的是在几个点
- 点赞与收藏是我用useState+点击事件由于次点击事件很简单就不单独写一个函数了。直接再行内用箭头函数来控制状态的改变来改变点赞与收藏的样式的改变。实现原理是我们通过状态控制来实现样式的转变。这里注意的是因为两个图标一样的,所有直接改变样式无法实现,我们只能将其中一个图标用一个盒子装一下再进行图标背景颜色的更改。
- 用antid-mobile里的Popup组件来制作评论与转发的页面显示。
- 并且注意一下这些图标样式的布局就行了
import { Popup, Space ,Image} from 'antd-mobile'
function Video_sidebar({hearts,comments,collects,share,users}) {
const [liked,setLiked]=useState(false)
const [shoucang,setShoucang]=useState(false)
const [visible1, setVisible1] = useState(false)
const [visible2, setVisible2] = useState(false)
return (
<div className='video_sidebar'>
<div className="video_sidebar_button">
<Image src={users}
width={60}
height={60`}`
fill="cover"
style={{ borderRadius: 60 }} >
</Image>
</div>
<div className="video_sidebar_button">
{liked?(<div className="heart">
<i className='iconfont icon-aixin1' onClick={(e=>setLiked(false))}></i>
</div> ):(
<i className='iconfont icon-aixin1' onClick={(e=>setLiked(true))}></i>
)}
<p>{hearts}</p>
</div>
<div className="video_sidebar_button">
<Space direction='vertica2'>
<i
onClick={() => {
setVisible2(true)
}}
className='iconfont icon-xiaoxi'
>
</i>
<Popup
visible={visible2}
onMaskClick={() => {
setVisible2(false)
}}
bodyStyle={{ minHeight: '70vh' }}
className="commentbody"
>
<div className="comments">
<div className="comments_header">
<h3>大家都在搜:火锅英雄</h3>
<p>{comments} 条评论</p>
</div>
<div className="comments_bottom">
<input className="search_input" placeholder="善语结善缘,恶语伤人心~">
</input>
</div>
</div>
</Popup>
</Space>
<p>{comments}</p>
</div>
<div className="video_sidebar_button">
{shoucang?(<div className="shoucang">
<i className='iconfont icon-shoucang' onClick={(e=>setShoucang(false))}></i>
</div> ):(
<i className='iconfont icon-shoucang' onClick={(e=>setShoucang(true))}></i>
)}
<p>{collects}</p>
</div>
<div className="video_sidebar_button">
<Space direction='vertical'>
<i
onClick={() => {
setVisible1(true)
}}
className='iconfont icon-zhuanfa00'
>
</i>
<Popup
visible={visible1}
onMaskClick={() => {
setVisible1(false)
}}
bodyStyle={{ minHeight: '50vh' }}
>
分享给朋友
</Popup>
</Space>
<p>{share}</p>
</div>
</div>
)
}
点赞与收藏分别设置两个点击事件来改变样式。Popup的具体的用法具体操作可以去antd-mobile的官网上查看具体用法。
Video_Footer组件
Video_Footer组件组件也是Video的子组件,用来存放用户名,视频描述和音乐信息等文字。
这个组件比较简单注意下文字的布局就行
mport React from 'react'
import './style.css'
import { Image} from 'antd-mobile'
function VideoFooter({user,description,song,cd}) {
return (
<div className='videofooter'>
<div className="videofotter_text">
<h3>@{user}</h3>
<p className='videofotter_text_intruduce'>{description}</p>
<div className="video_footer_music">
<i className='fa fa-music'></i>
<p className='videofotter_text_music'>@{song}</p>
</div>
</div>
<div className="musicCd">
<div className="musicPicture">
<Image src={cd}
width={40}
height={40}
fill="cover"
style={{ borderRadius: 40 }} >
</Image>
</div>
</div>
</div>
)
}
export default VideoFooter
右下边的CD的旋转效果在css中用动画来做。这个CD样式需要用两个盒子,将盒子设置为圆。给两个盒子设置属性,animation定义动画效果,对应的"infinite",动画无限次播放,对应的"linear",动画从头到尾的速度是相同的。transform对元素进行旋转、缩放、移动或倾斜.
.musicCd{
position: absolute;
bottom: 0;
right:0.5rem;
width:3rem;
height: 3rem;
/*定义动画效果,对应的"infinite",动画无限次播放,对应的"linear",动画从头到尾的速度是相同的。*/
animation: musicPicture 10s infinite linear;
border-radius: 100%;
display: flex;
justify-content: center;
align-items: center;
background-color: #363636;
/*重复径向渐变 从中心开始沿着四周产生渐变效果,模拟出唱片那种感觉,不喜欢可以注释或删掉*/
background: repeating-radial-gradient(#111 0%, #000 5%);
}
.musicPicture{
width:2rem;
height: 2rem;
display: flex;
justify-content: center;
align-items: center;
margin: 0 auto;
border-radius: 100%;
background-size: cover;
}
@keyframes musicPicture{
0%{
/*transform对元素进行旋转、缩放、移动或倾斜。以下就是旋转0度。*/
transform: rotate(0deg);
}
100%{
/*以下就是旋转360度*/
transform: rotate(360deg);
}
}
Bottom组件
Bottom组件是公共组件,需要用导路由来显示功能。
-
路由配置
我会把所有的路由配置单独写在一个routes文件夹里,我通过Routes与Route来配置路由。
- 这里有一个小优化就是将不是优先显示的页面通过lazy采用按需加载,而只加载首页一个组件,少加载一些加载肯定会变快了。
import { lazy } from 'react'
import { Routes, Route } from 'react-router-dom'
import Friend from '../pages/Friend'
import Home from '../pages/Home'
import Message from '../pages/Message'
import Video from '../pages/Home/Video'
import Search from '../pages/Search'
const Mine = lazy(() => import('../pages/Mine'))
const DouyinRank = lazy(() => import('../pages/Search/DouyinRank'))
const KangyiRank = lazy(() => import('../pages/Search/KangyiRank'))
const PingpaiRank = lazy(() => import('../pages/Search/ZhiboRank'))
const YingyueRank = lazy(() => import('../pages/Search/YingyueRank'))
const ZhiboRank = lazy(() => import('../pages/Search/PingpaiRank'))
const RoutesConfig=()=>(
<Routes>
<Route path='/' element={<Home/>}></Route>
<Route path='/home' element={<Home/>}></Route>
<Route path='/mine' element={<Mine/>}></Route>
<Route path='/friend' element={<Friend/>}></Route>
<Route path='/message' element={<Message/>}></Route>
<Route path='/search' element={<Search/>}>
<Route path="/search" element={<DouyinRank/>}/>
<Route path="/search/douyinrank" element={<DouyinRank/>}/>
<Route path="/search/kangyirank" element={<KangyiRank/>}/>
<Route path="/search/pingpairank" element={<PingpaiRank/>}/>
<Route path="/search/yingyuerank" element={<YingyueRank/>}/>
<Route path="/search/zhiborank" element={<ZhiboRank/>}/>
</Route>
</Routes>
)
export default RoutesConfig
-
Tab切换与Link跳转
我们要实现的功能是点击底部导航时能够跳转到相应的页面并且文字加粗变颜色。这个我们要使用useLocation与active来结合使用。首先我们通过useLocation来获取当前页面的路径,并且写一个判断,当获取的路径与当前页面的路径一直是我们触发active伪元素,即将文字加粗变色。而路由跳转则更加简单,使用Link标签即可。
import React from 'react'
import {NavLink,useLocation} from "react-router-dom"
import {Wrapper } from "./style.js"
import classnames from "classnames"
export default function Bottom() {
const pathname=useLocation();
return (
<Wrapper>
<div className='video_bottom'>
<NavLink to="/home" className={classnames({active:pathname=="/home"||pathname=="/"})}>
<span>首页</span>
</NavLink>
<NavLink to="/friend" className={classnames({active:pathname=="/friend"})}>
<span>朋友</span>
</NavLink>
<NavLink to="/video" className={classnames({active:pathname=="/video"||pathname=="/"})}>
<i className='fa fa-plus'></i>
</NavLink>
<NavLink to="/message" className={classnames({active:pathname=="/message"})}>
<span>消息</span>
</NavLink>
<NavLink to="/mine" className={classnames({active:pathname=="/mine"})}>
<span>我</span>
</NavLink>
</div>
</Wrapper>
)
}
- 还有一个小技巧是但我们需要使用很多个类名时,我们可已使用classnames来创建类名,它很好的解决了按需使用不同样式类的问题。
Header组件与Bottom组件类似我就不再写了。
数据的请求与获取
数据请求我统一写再api文件夹下,通过axios来请求因为我的每一个用户都会有一串的数据,所有我直接把一个用户的所有数据写在了一起,所有我只有一条数据请求。
import axios from 'axios'
export const getVideos = () =>
axios.get('https://www.fastmock.site/mock/7c42bc8294ddcaeb64b540334e5f24fb/dkfjklsdflksf/videos')
- 数据的获取我是通过useEffect与useState来实现,并且通过map来进行输出。
function Home() {
const [vid, setVideos] = useState([])
useEffect(() => {
(async () => {
let { data:videosData } = await getVideos()
setVideos(videosData)
})()
})
Search页面
该页面是抖音的搜索页面,只写了大概的样式,主要实现了轮播图与二级路由功能。
轮播图是通过swiper实现的,幻灯片全局引入css,使用固定的html结构, .swiper-container>.swiper-wrapper>.swiper-slide{n}
- 这里需要注意的是,若一个页面有多个自盒子,那么就需要对这写盒子进行切割分两次或多次进行引入,若一次性引入,那么划过去之后就不会显示任何画面。
- 组件挂载后, useEffect ,通过new Swiper('.btn_banners')实例化幻灯片功能。
import React, { useEffect } from 'react'
import { NavLink } from 'react-router-dom'
import Swiper from 'swiper'
import { Wrapper } from './style'
export default function SearchRankNav() {
let swiper = null;
useEffect(() => {
if (swiper) return
swiper = new Swiper('.navbar')
}, [])
// 页面二级路由的导航准备
let rankNavs = [
{ id: 1, desc: '抖音热榜', path: '/douyinrank'},
{ id: 2, desc: '抗疫榜', path: '/kangyirank'},
{ id: 3, desc: '直播榜', path: '/pingpairank'},
{ id: 4, desc: '音乐榜', path: '/yingyuerank'},
{ id: 5, desc: '品牌榜', path: '/zhiborank'},
]
return (
<Wrapper>
<div className="navbar swiper-container">
<div className="nav-box swiper-wrapper">
{
rankNavs.map((item, index) => {
return (
<NavLink
index={index}
to={`/search${item.path}`}
key={item.id}
className="nav-item swiper-slide"
>
{item.desc}
</NavLink>
)
})
}
</div>
</div>
</Wrapper>
)
}
- 由于需要的数据不多就直接建了五个数据,并且五个数据一个页面正好能够容的下,所以轮滑功能就没有实现出来。
- 并且此处还使用了二级路由,通过NavLink来实现路由功能map函数来输出所需要的数据来改变路由路径去到不同的子路由,实现路由跳转。
配置文件
- 配置自适应文件 因为我们制作的是移动端app,所以我们需要考虑到不同机型的适配问题,所以我们在写样式大小时一般不用px 而是用相对rem能够自动适应 引入下列代码可以将20px=1rem进行转换。
var init = function () {
var clientWidth = document.documentElement.clientWidth || document.body.clientWidth;
if (clientWidth >= 640) {
clientWidth = 640;
}
var fontSize = 20 / 375 * clientWidth;
document.documentElement.style.fontSize = fontSize + "px";
}
init();
window.addEventListener("resize", init);
- 配置通用样式文件
@charset "utf-8";
/* CSS Document */
body,h1,h3,h2,h4,h5,h6,p,dl,dd,ul,li,ol,td,form,input,fieldset,legend{
margin:0; padding:0;
}
li{list-style:none;}
a{text-decoration:none; font-size:12px; color:#333;}
body{
font-size:14px;
font-family:"微软雅黑";
color:#666;
background:$bg-color;
}
* {
box-sizing: border-box;
}
img{vertical-align:top; display:block; border:none;}
a,input{outline:none;}
em,i{font-style:normal;}
b,strong,h1,h2,h3,h4,h5,h6{font-weight:normal;}
.clear:after{content:""; display:block; clear:both; height:0; visibility:hidden;}
table{border-collapse:collapse;}
.border0{border:none;}
.fl{float:left;}
.fr{float:right;}
总结
这个app主要就是为了模仿抖音播放功能,写了之后才发现有多难,以我目前所学很多功能都实现不了。我目前所学的知识只能写成这样了,希望在今后可以慢慢完善一些功能。这个项目,一个缺点是将所有的数据都通过一个接口进行请求。并且一些数据都是通过自己写没有通过数据请求,这在以后都会进行改进的。抖音的数据需求实在太大了,我在数据请求方面还有很大的不足。
结束语
希望大家能够给一些意见或建议。你们的每一个攒都是对我这个前端新手最大的支持!谢谢大家观看!
项目源码地址:github.com/ltrookie/re…