小刻也看得懂的react入门实战:bilibili移动游戏页

411 阅读6分钟

chrome-capture-2022-5-30.gif shouye7.jpg

持续创作,加速成长!这是我参与「掘金日新计划 · 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!

项目架构

image.png

  • api目录下放的是与数据请求相关的
  • assets目录下放的是静态资源(图片、字体库、css rset)
  • components目录下放的是组件
  • config目录下放的是与配置有关的
  • modules目录下放的是自适应处理相关
  • pages目录下放的是单个页面的展示

目标预览

2.gif

设计思路

    首先是可以分成HeaderFooter、和页面中间的内容pages,Footer组件tab的切换
同时产生路由的切换,tab的切换可以用classnames来动态设置类名。
    首页“新游”与“关注”也可以像Footer组件的tab切换来写,中间的游戏详情与视频
详情可以通过axios向fastmock请求数据,并渲染在页面上。
    不同的page还可以细化出不同的子组件。

Footer组件的实现

首先我们看看预览

3.gif

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':'我的'
}

精选页面的实现

导航栏

5.gif

导航栏的切换其实与底部栏的切换差不多,通过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组件的实现

6.gif

通过封装了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组件的实现

8.gif

细心的小伙伴们肯定发现了,上方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>
    )
}

发现页面的实现

轮播图的实现

10.gif

通过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'当组件深度比较大的时候,相对路径变的比较难和长,我们就要工程化配置路径别名。后面会去继续学习、慢慢完善这个组件。希望本篇文章对你有所帮助,当然也不要吝惜你的点赞哦!小刻爱你哦!

GitHub源码地址

GitHub Pages