持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第1天,点击查看活动详情
前言
学习react也有一段时间了,react的声明式设计、组件化、高效灵活,
让作为初学者的我摸不着头脑,所以想找一个项目练练手,那么久决定是你了!
bilibili移动游戏页!
前期准备
写一个react项目之前,你需要会一些前端的知识,比如:JavaScript、HTML、CSS。
再去看一些react相关的文档,就可以动手试试水啦。
当然创建我们react项目,还要通过脚手架的帮忙,我这里用的是vite,一个很方便的
前端构建工具。只需要在命令行上输入npm init @vitejs/app 就可以初始化我们的第一个
react项目啦。
技术栈
- react全家桶
- swiper 轮播图就靠它了!
- font-awesome 漂漂亮亮的图标字体库!
- antd-mobile 很多好用的组件我以后一定好好研究(qwq
- axios 数据请求 和 fastmock 一起用
- fastmock可以让你在没有后端程序的情况下能真实地在线模拟ajax请求,你可以用fatmock实现项目初期纯前端的效果演示,也可以用fastmock实现开发中的数据模拟从而实现前后端分离
- classnames 动态设置类名
- styled-components css in js!
项目架构
- api目录下放的是与数据请求相关的
- assets目录下放的是静态资源(图片、字体库、css rset)
- components目录下放的是组件
- config目录下放的是与配置有关的
- modules目录下放的是自适应处理相关
- pages目录下放的是单个页面的展示
目标预览
设计思路
首先是可以分成Header、Footer、和页面中间的内容pages,Footer组件tab的切换
同时产生路由的切换,tab的切换可以用classnames来动态设置类名。
首页“新游”与“关注”也可以像Footer组件的tab切换来写,中间的游戏详情与视频
详情可以通过axios向fastmock请求数据,并渲染在页面上。
不同的page还可以细化出不同的子组件。
Footer组件的实现
首先我们看看预览
Footer组件主要是底部栏的tab切换,首先我们建立路由:
// App.jsx
import { useState } from 'react'
import './App.css'
import {Route,Routes,Link} from 'react-router-dom'
import Footer from './components/Footer'
import Header from './components/Header'
import Choose from './pages/Choose'
import Find from './pages/Find'
import Mine from './pages/Mine'
function App() {
return (
<div className="App">
<Header/>
<Routes>
<Route path='/' element={<Choose/>}></Route>
<Route path='/choose' element={<Choose/>}></Route>
<Route path='/find' element={<Find/>}></Route>
<Route path='/mine' element={<Mine/>}></Route>
</Routes>
<Footer/>
</div>
)
}
export default App
再在Footer组件通过classnames动态设置tab的选中状态
//Footer.jsx
import React from 'react'
import { Link,useLocation } from 'react-router-dom'
import {FooterWrapper} from './style'
import classnames from 'classnames'
export default function Footer() {
const { pathname } = useLocation();
return (
<FooterWrapper>
<Link to='/' className={classnames({active:pathname == '/'})}>
<i className="iconfont icon-shouye"></i>
<span>精选</span>
</Link>
<Link to='/find' className={classnames({active:pathname == '/find'})}>
<i className="iconfont icon-faxian"></i>
<span>发现</span>
</Link>
<Link to='/mine' className={classnames({active:pathname == '/mine'})}>
<i className="iconfont icon-wode"></i>
<span>我的</span>
</Link>
</FooterWrapper>
)
}
Header组件的实现
在上面的预览我们可以看到,切换路由的同时顶部中间的标题也会改变,这里通过react-router里的useLocation实现,同时在config文件夹下写上每个路由的标题配置。
//Header.jsx
import React,{ useState, } from 'react'
import {HeaderWrapper} from './style'
import { pageTitle } from '../../config'
import { useLocation } from 'react-router'
import { useEffect } from 'react';
export default function Header() {
const [title,setTitle] = useState();
const {pathname='/'} = useLocation();
useEffect(() => {
let _title = pageTitle[pathname] || '';
setTitle(_title);
});
return (
<HeaderWrapper>
<div className="top">
<div className="t-left">
<div className="t-back">
<span className="fa fa-arrow-left back-icon"></span>
</div>
</div>
<div className="t-center">
<span className='t-txt'>{title}</span>
</div>
<div className="t-notice">
<span className="fa fa-bell-o bell-icon"></span>
</div>
<div className="t-right">
<div className="t-search">
<span className="fa fa-search search-icon"></span>
</div>
</div>
</div>
</HeaderWrapper>
)
}
export const pageTitle = {
'/':'首页',
'/choose':'首页',
'/find':'发现',
'/mine':'我的'
}
精选页面的实现
导航栏
导航栏的切换其实与底部栏的切换差不多,通过classnames来动态设置被点击后的状态,同时设置子组件 < New/> < Focus/> 的显示与隐藏。由于数据请求一般通过父组件进行,父组件再将请求到的数据传给子组件,所以我在这里进行对New组件的游戏数据的请求。
import React,{useState,useEffect} from "react";
import { ChooseWrapper } from './style'
import { getGameList } from '../../api/request'
import Banners from './Banners'
import New from "./New";
import Focus from './Focus'
import classnames from 'classnames'
const Choose = () => {
const [gameData,setGameData] = useState([]);
const [tab,setTab] = useState('新游');
const changeTab = (target) => {
setTab(target);
}
useEffect(() => {
(async () => {
let { data } = await getGameList();
setGameData(data);
})();
},[]);
return(
<ChooseWrapper>
{/* <Banners/> */}
<ul>
<li></li>
<li className={classnames({active:tab==="新游"})} onClick={changeTab.bind(null,'新游')}>新游</li>
<li className={classnames({active:tab==="关注"})} onClick={changeTab.bind(null,'关注')}>关注</li>
<li></li>
</ul>
{tab === "新游" && <New source={gameData}/>}
{tab === "关注" && <Focus/>}
</ChooseWrapper>
)
}
export default Choose;
New组件的实现
通过封装了rendernew函数来进行数据的遍历。同时父组件通过fastmock请求到的数据传给了New组件。
import React from 'react'
import './style.css'
import newimg from '../../../assets/images/newimg.webp'
import newicon from '../../../assets/images/newicon.webp'
export default function New({source=[]}) {
const rendernew = () => {
return(
source.map((item) => (
<div className="bui-mod-wrap home-content" key={item.id}>
<div className="bui-mod-content">
<div className="bui-list-topic-index">
<div className="today-recommend content-item">
<p><img src={newimg} className="feed-bg item-pic" /></p>
<div className="tag">今日推荐</div>
<div className="card-info">
<div className="game-info">
<img src={newicon} alt="" className="game-icon" />
<span className="name-tag">
<div className="game-name">{item.title}</div>
<div className="type-name"></div>
<div className="tags">
<span className="tag-name">{item.tag1}</span>
<span className="tag-name">{item.tag2}</span>
<span className="tag-name">{item.tag3}</span>
</div>
</span>
<div className="cart-grade">
<span className=" icon-start-active fa fa-star"></span>
<div className="num">{item.num}</div>
</div>
<div className="game-desc">{item.desc}</div>
<div className="game-bz">{item.bz}</div>
</div>
</div>
</div>
</div>
</div>
</div>
))
)
}
return (
<section className='bui-container'>
<div className="bui-tab home-tab-content">
<div className="tab-content">
<div className="tab-content-item">
{rendernew()}
</div>
</div>
</div>
</section>
)
}
Focus组件的实现
细心的小伙伴们肯定发现了,上方tab的切换,还是用的classnames设置动态类名(qwq) ,然后继续细分组件,设置了子组件< Info/> < Video/> ,通过fastmock请求的数据传给 Video子组件,只要传给Info组件特定的tab值就可以完成切换了。
import React,{ useState,useEffect } from 'react'
import {FocusWrapper} from './style'
import classnames from 'classnames'
import Info from './Info'
import Video from './Video'
import { getVideoList } from '../../../api/request'
import icon from '../../../assets/images/icon.webp'
import icon1 from '../../../assets/images/icon1.webp'
import icon2 from '../../../assets/images/icon2.webp'
import icon3 from '../../../assets/images/icon3.webp'
export default function Focus() {
const [tab,setTab] = useState('1');
const [videoData,setVideoData] = useState([]);
const changeTab = (target) => {
setTab(target);
}
useEffect(() => {
(async () => {
let { data } = await getVideoList();
setVideoData(data);
})();
},[]);
return (
<FocusWrapper>
<div className="edit-play-game swiper-container">
<div className="swiper-wrapper">
<div className={"swiper-slide" + classnames({active:tab === '1'})} onClick={changeTab.bind(null,'1')}>
<img src={icon} alt="" className='item-pic' />
</div>
<div className={"swiper-slide" + classnames({active:tab === '2'})} onClick={changeTab.bind(null,'2')}>
<img src={icon1} alt="" className='item-pic' />
</div>
<div className={"swiper-slide" + classnames({active:tab === '3'})} onClick={changeTab.bind(null,'3')}>
<img src={icon2} alt="" className='item-pic' />
</div>
<div className={"swiper-slide" + classnames({active:tab === '4'})} onClick={changeTab.bind(null,'4')}>
<img src={icon3} alt="" className='item-pic' />
</div>
</div>
<div className="bg-shade"></div>
<a href="#" className="btn-more">
<span className="fa fa-plus bui-icon"></span>
</a>
</div>
<Info tab={tab}/>
<Video video={videoData}/>
</FocusWrapper>
)
}
Info组件的实现
从父组件传过来的tab值需要结构出来,这里的设计我用到了antd-mobile的卡片组件, antd-mobile的组件都蛮好用的,大家可以学习起来。
import React from 'react'
import {Card,Button} from 'antd-mobile'
import { InfoWrapper } from './style'
export default function Info({tab}) {
return (
<InfoWrapper>
{ tab === "1" && <Card
title={
<div style={{fontWeight:'bolder',fontSize:'0.5rem'}}>
时空猎人3
</div>
}
extra={
<Button
color='primary'
borderRadius="2rem"
onClick={() => {}}>
下载
</Button>}
style={{ borderRadius: '0.5rem' }}
>
<div className="content">暂无更新</div>
<div className="footer" onClick={e => e.stopPropagation()}>
了解游戏更多详情内容
</div>
</Card>}
{ tab === "2" && <Card
title={
<div style={{fontWeight:'bolder',fontSize:'0.5rem'}}>
神觉者
</div>
}
extra={
<Button
color='primary'
borderRadius="2rem"
onClick={() => {}}>
已下载
</Button>}
style={{ borderRadius: '0.5rem' }}
>
<div className="content">暂无更新</div>
<div className="footer" onClick={e => e.stopPropagation()}>
了解游戏更多详情内容
</div>
</Card>}
{ tab === "3" && <Card
title={
<div style={{fontWeight:'bolder',fontSize:'0.5rem'}}>
明日方舟:终末地
</div>
}
extra={
<Button
color='primary'
borderRadius="2rem"
onClick={() => {}}>
预约
</Button>}
style={{ borderRadius: '0.5rem' }}
>
<div className="content">暂无更新</div>
<div className="footer" onClick={e => e.stopPropagation()}>
了解游戏更多详情内容
</div>
</Card>}
{ tab === "4" && <Card
title={
<div style={{fontWeight:'bolder',fontSize:'0.5rem'}}>
明日方舟
</div>
}
extra={
<Button
color='primary'
borderRadius="2rem"
onClick={() => {}}>
下载
</Button>}
style={{ borderRadius: '0.5rem' }}
>
<div className="content">暂无更新</div>
<div className="footer" onClick={e => e.stopPropagation()}>
了解游戏更多详情内容
</div>
</Card>}
</InfoWrapper>
)
}
Video组件的实现
通过封装了rendervideo函数来进行数据的遍历。同时父组件通过fastmock请求到的数据传给了Video组件。
import React from 'react'
import { VideoWrapper } from './style'
import pic from '../../../../assets/images/pic.webp'
import userimg from '../../../../assets/images/usericon.webp'
export default function Video({video=[]}) {
const rendervideo = () => {
return (
video.map((item) =>(
<div className="blg-ai-video-item" key={item.id}>
<div className="img-cover">
<img src={pic} alt="" className="item-pic" />
<div className="bg-shade">
<div className="numbers">
<div className="video-time">{item.videotime}</div>
<div className="view-num">{item.viewnum}</div>
<div className="barrage-num">{item.barragenum}</div>
</div>
</div>
</div>
<div className="game-desc">
<div className="title">{item.title}</div>
<div className="user-desc">
<span className="user-face">
<img src={userimg} alt="" className="item-pic" />
<span className="verify-type">
<span className="bui-icon"></span>
</span>
</span>
<span className="desc">
<div className="name">{item.name}</div>
<div className="time">{item.time}</div>
</span>
<div className="game-tag">
<span className="bui-icon fa fa-chain"></span>
{item.tag}
</div>
</div>
</div>
</div>
))
)
}
return (
<VideoWrapper>
{rendervideo()}
</VideoWrapper>
)
}
发现页面的实现
轮播图的实现
通过swiper来实现轮播图效果,实例化幻灯片功能 new Swiper('.btn-banners'),再设置loop为true可循环播放,autoplay:{delay:1000}自动播放延迟1s。
import React,{useEffect} from 'react'
import {BannersWrapper} from './style'
import Swiper from 'swiper'
import { Link } from 'react-router-dom'
import propTypes from 'prop-types'
import banner1 from '../../../assets/images/aobidao.jpg'
import banner2 from '../../../assets/images/diwurenge.jpg'
import banner3 from '../../../assets/images/mnxshx.jpg'
export default function Banners() {
useEffect(() => {
new Swiper('.btn-banners',{
loop: true,
autoplay: {
delay: 1000
},
pagination: {
el: '.swiper-pagination'
}
});
},[]);
return (
<BannersWrapper>
<div className="btn-banners swiper-container">
<div className="swiper-wrapper">
<div className="swiper-slide">
<img src={banner1} alt="" />
</div>
<div className="swiper-slide">
<img src={banner2} alt="" />
</div>
<div className="swiper-slide">
<img src={banner3} alt="" />
</div>
</div>
<div className="swiper-pagination"></div>
</div>
</BannersWrapper>
)
}
数据请求设置
在api目录下专门设置request.js文件封装数据请求
import axios from 'axios'
// 游戏列表的请求
export const getGameList = () =>
axios.get(`https://www.fastmock.site/mock/477f993fb8b86e1e7fa9aa8ca719a766/bilibili-game/gamelist`)
// 视频列表的请求
export const getVideoList = () =>
axios.get('https://www.fastmock.site/mock/477f993fb8b86e1e7fa9aa8ca719a766/bilibili-game/videoinfo')
自适应设置
document.documentElement.style.fontSize =
document.documentElement.clientWidth / 10.8 + 'px'
// 横竖屏切换
window.onresize = function () {
document.documentElement.style.fontSize =
document.documentElement.clientWidth / 10.8 + 'px'
}
写在最后
其实本次实现的功能并没有很多,还有很多的不足,只实现了3个页面的期中一个,(在发布文章的时候bilibili游戏又改回了4页多加了一个排行榜,可恶啊qwq),代码也有很多优化的地方,比如路由的懒加载,和工程化配置alias,import userimg from '../../../../assets/images/usericon.webp'当组件深度比较大的时候,相对路径变的比较难和长,我们就要工程化配置路径别名。后面会去继续学习、慢慢完善这个组件。希望本篇文章对你有所帮助,当然也不要吝惜你的点赞哦!小刻爱你哦!