React组件开发-仿哔哩哔哩移动端首页

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第1天,点击查看活动详情

前言

什么是React?

React是一个简单的javascript UI库,用于构建高效、快速的用户界面。它是一个轻量级库,因此很受欢迎。它遵循组件设计模式声明式编程范式函数式编程 概念,使前端应用程序更高效。它使用虚拟DOM来有效地操作DOM。它遵循从高阶组件到低阶组件的单向数据流

React 中一切都是组件。 我们通常将应用程序的整个逻辑分解为小的单个部分。 我们将每个单独的部分称为组件。 通常,组件是一个javascript函数,它接受输入,处理它并返回在UI中呈现的React元素。

项目准备

预期效果:

A7B141639D34828D7D58CB9D4FEF35A4.jpg

想要搭建一个好的项目,称手的工具必不可少。

在我们已经安装好 vscode 和 node 的前提下,我们需要在终端环境下初始化一个项目,项目我们取名为 bilibili-page:

这里我们使用 vite 进行搭建,它相比于 webpack 更快

npm init @vitejs/app bilibili-page

执行该命令后我们选择 react:

Snipaste_2022-06-26_18-47-42.png

因为这个项目使用的是 js 语法,所以我们选择 react,如果对 ts 语法比较熟悉的小伙伴也可以选择 react-ts。

Snipaste_2022-06-26_18-48-04.png

选择好以后,我们依次执行如下命令: 进入项目目录,并使用 npm i 或 (npm install) 安装 react。

cd bilibili-page
npm install

安装依赖

除此之外,我们还需要安装以下依赖,使用命令(npm i xxx)默认安装最新版本:

  1. antd-mobile:Ant Design Mobile 简称 antd-mobile,是 Ant Design 的移动规范的 React 实现,是一个基于Preact/React/React Native的UI组件库。我们用它来实现轮播图效果。

  2. axios:axios 是一个基于 promise 的网络请求库,用于获取后端数据,是前端常用的数据请求工具。

  3. react-routerreact-router-dom:react-router 用来实现了路由的核心功能,而 react-router-dom 基于 react-router,加入了在浏览器运行环境下的一些功能。

  4. weuireact-weui:weui 是一套同微信原生视觉体验一致的基础样式库,react-weui 是微信提供的前端 UI 库,在 react 中我们可以直接拿来使用。这里我们用来实现 loading 状态,优化用户体验。

  5. styled-components:安装这一依赖,我们能够实现 css in js,是 react 中设计组件很好用的一种方法。

  6. classnames:它是用来合并类名的,存在多个类名变量时,想添加到对应的元素中,采用 classnames 非常方便。

搭建项目

主要功能

  1. 路由跳转及tab切换,包括一级路由和二级路由,当用户进入到首页时,默认跳转到首页的推荐页面。

  2. 轮播图效果的实现,可自动循环播放以及手动滑动。

  3. loading状态,由于视频数据是从远程请求过来的,需要一些等待时间,加上了 loading 状态后,填补了页面的空白状态,优化用户体验。

在该项目中,我主要用来实现 bilibili 首页页面的快速搭建,具体的实现功能后面我会一一去实现,例如下拉刷新、上滑加载、点击视频详情页功能,以及搜索、点赞、投币等功能。

设计思路

根据需求,我们需要在 src 目录下新建以下文件夹:

Snipaste_2022-06-26_19-42-49.png

  1. 在 api 文件夹下,我们将数据请求封装在 api 目录下方便管理,在 api 下的 request.js 文件中,我们定义了一个接口 getVideos 用来获取视频信息数据。
import axios from 'axios'

export const getVideos = () => 
 axios.get('https://www.fastmock.site/mock/3f112f6cb2f621fc9c2dd6a14be19f38/beers/videolist')
  1. 在 assets 文件夹用来存放静态资源文件,如:本地图片、下载的图标字体和全局的css样式,用来做 reset,样式重置。

  2. components 文件夹用来存放组件,主要有底部路由(一级路由)、头部搜索框以及导航路由(二级路由)。

  3. config 目录下的 index,js,用来对页面进行标题配置:

export const pageTitle = {
  '/dynamic': '动态',
  '/vip': '会员购'
}
  1. modules 目录下的 rem.js 用来对进行移动端的适配,字体大小随着机型的不同进行变化。
document.documentElement.style.fontSize = 
  document.documentElement.clientWidth / 3.75 + 'px'

window.onresize = function() {
  document.documentElement.style.fontSize = 
    document.documentElement.clientWidth / 3.75 + "px"
}
  1. pages 目录,用来存放单页面级别组件,例如:主页(主页下的多个页面)、动态、会员购和我的。

  2. routes 目录,这里我们将路由抽离出来放到这个文件夹下,便于管理。我们希望有些比较大的组件,只有在此组件被加载时,内部资源才被引用,因此我们引入 lazy 实现懒加载。

import { lazy } from 'react'
import { Routes, Route } from 'react-router-dom'
import Home from '../pages/Home'  // 首页
const Dynamic = lazy(() => import('../pages/dynamic'))  // 动态
const Vip = lazy(() => import('../pages/vip'))  // 会员购
const Mine = lazy(() => import('../pages/Mine'))  // 我的

const Live = lazy(() => import('../pages/Home/Live'))  // 直播
const Recommend = lazy(() => import('../pages/Home/Recommend'))  // 推荐
const Hot = lazy(() => import('../pages/Home/Hot'))  // 热门
const Animation = lazy(() => import('../pages/Home/Animation'))  // 动画
const Movies = lazy(() => import('../pages/Home/Movies'))  // 影视
const Campus = lazy(() => import('../pages/Home/Campus'))  // 校园

const RoutesConfig = () => {

  return (
    <Routes>
      <Route path="/" element={<Home />}></Route>
      <Route path="/home" element={<Home />} >
        {/* 二级路由 */}
        <Route path="/home/live" element={<Live />} />
        <Route path="/home/recommend" element={<Recommend />} />
        <Route path="/home/hot" element={<Hot />} />
        <Route path="/home/animation" element={<Animation />} />
        <Route path="/home/movies" element={<Movies />} />
        <Route path="/home/campus" element={<Campus />} />
      </Route>
      <Route path="/dynamic" element={<Dynamic />}></Route>
      <Route path="/vip" element={<Vip />}></Route>
      <Route path="/mine" element={<Mine />}></Route>
    </Routes>
  )
}

export default RoutesConfig

功能的具体实现

路由跳转及tab切换

  1. 一级路由

我们从 react-router-dom 中解构出 LinkuseLocation,Link 实现页面的跳转,从 useLocation 中我们解构出 pathname,用来判断当前路径是否匹配,如果匹配就将样式添加上去。

import React from 'react'
import { Link, useLocation } from 'react-router-dom'
import { FooterWrapper } from './style'
import classnames from 'classnames'

export default function Footer(props) {
  const { pathname } = useLocation()

  return (
    <FooterWrapper>
      <Link to="/home" className={classnames({active:pathname == '/home' || pathname == '/'  || pathname == '/home/recommend'})}>
        <i className='iconfont icon-shouye'></i>
        <span>首页</span>
      </Link>
      ...
    </FooterWrapper>
  )
}

效果如下图:

1.gif

  1. 二级路由

我们首先将二级路由放到一个数组中,包括id、desc和path,然后我们使用 map 方法代替forEach循环,来遍历并对每个值应用转换函数,通过点击我们既想要跳转又想要添加样式,我们使用了比 Link 更高级的 NavLink

import React from 'react'
import { Wrapper } from './style'
import { NavLink } from 'react-router-dom'

export default function HomeNav() {
  let homeNavs = [
    { id: 1, desc: '直播', path: '/live'},
    { id: 2, desc: '推荐', path: '/recommend'},
    { id: 3, desc: '热门', path: '/hot'},
    { id: 4, desc: '动画', path: '/animation'},
    { id: 5, desc: '影视', path: '/movies'},
    { id: 6, desc: '校园', path: '/campus'}
  ]

  return (
    <Wrapper>
      <div className="navbar swiper-container">
        <div className="nav-box swiper-wrapper">
          {
            homeNavs.map((item, index) => {
              return (
                <NavLink
                  index={index}
                  to={`/home${item.path}`}
                  key={item.id}
                  className="nav-item swiper-slide"
                >
                {item.desc}
                </NavLink>
              )
            })
          }
        </div>
      </div>
    </Wrapper>
  )
}

轮播图的实现

轮播图我使用了 antd-mobile 中的 SwiperToast,大家可以参考:mobile.ant.design/zh/componen… 这里我大致展示了一下轮播图的效果,没有把图片加上去(后面会加),在 Swiper 组件中加入 autoplayloop,使得轮播图既能自动播放又能循环。Toast.show 在你触发了点击事件之后,能够弹出相关信息。

import React from 'react'
import './style.css'
import { Swiper, Toast } from 'antd-mobile'
import classnames from 'classnames'

export default function SetMovie() {
  const colors = ['#ace0ff', '#bcffbd', '#e4fabd', '#ffcfac']

  const items = colors.map((color, index) => (
    <Swiper.Item key={index}>
      <div
        className={classnames('content')}
        style={{ background: color }}
        onClick={() => {
          Toast.show(`你点击了电影 ${index + 1}`)
        }}
      >
        {index + 1}
      </div>
    </Swiper.Item>
  ))

  return (
    <div className='wrapper'>
      <Swiper autoplay loop>{items}</Swiper> 
    </div>
  )
}

效果如下图:

swiper.gif

设置 loading 状态

首先我们从引入的 WeUI 中解构出 Toast,然后再给 loading 设置默认状态为 true,当从远程请求到数据之后,setLoading(false),状态消失,数据显示,优化用户体验。

import React, { useEffect, useState } from 'react'
import SetMovie from '../SetMovie'
import { Wrapper } from './style'
import { getVideos } from '@/api/request'
import { Link } from 'react-router-dom'
import WeUI from 'react-weui'

const {
  Toast
} = WeUI;

export default function Recommend() {
  const [loading, setLoading] = useState(true)
  const [videos, setVideos] = useState([])

  useEffect(() => {
    (async() => {
      let { data } = await getVideos()
      setVideos([...data])
      setLoading(false)
    })()
  }, [])

  return (
    <>
      <SetMovie />
      <Wrapper> 
        <Toast show={loading} icon="loading">加载中...</Toast>
        ...
      </Wrapper>
    </>
  )
}

效果如下图:

loading.gif

完成数据渲染

从远程请求过来的数据通过唯一 key 值 video.id,通过 map 分配给 video,完成数据的渲染。

{
          videos && videos.map(
            video => (
              <div className='videos-flex'>
                <Link
                  className="videos-list"
                  to={`/animation/video${video.id}`}
                  key={video.id}
                >
                  <div className='videos-box'>
                    <div className="videos-img">
                      <img src={video.img} alt="" />
                      <div className="info">
                        <i className='iconfont icon-bofangqi-bofangxiaodianshi'></i>
                        <span>{video.bofang}</span>
                        <i className='iconfont icon-pinglun'></i>
                        <span>{video.pinglun}</span>
                        <span>{video.time}</span>
                      </div>
                    </div>
                    <div className='title'>
                      <span>{video.title}</span>
                    </div>
                    <div className='up'>
                      <span>{video.up}</span>
                    </div>
                  </div>
                </Link>
              </div>
            )
          )
        }

单个的 video demo 效果如下图:

Snipaste_2022-06-27_00-29-48.png

总结

当我们在自己动手写第一个React项目(或其他项目)时,建议大家把思路理清楚,不要急于写样式,把大致的模板建好,然后逐个组件进行完善,如果大家想要快速搭建项目,可以直接使用我在 github 上传的一个 React 的单页应用模板,如果你对 git 较为熟悉,建议直接在终端 git clone,然后再 npm i,不会使用 git 的小伙伴也可以直接下载,模板地址:React单页应用模板
项目地址:哔哩哔哩移动端首页

猜你喜欢

转载自juejin.im/post/7113588048922673183