手把手教你做:快手最新爆款“一甜相机”- 新手react开发必备项目

8,095 阅读9分钟

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

前言

React组件化一直是React项目开发的重要学习过程。
一个react项目包含着很多页面逻辑,如果将所有的页面逻辑放在同一个页面上,那么处理起来就会十分 麻烦,而且不利于后续的 管理和扩展,因此,React组件化开发迅速发展了起来。

组件化开发,顾名思义,就是将一个页面拆分成一个个小的功能块,每个功能块封装一个功能,完成属于 自己的独立的功能,然后连接起来,页面的管理和维护就更加简洁方便了。

最近在学习React的组件开发,于是就开始挑战一下一甜相机的模板页面进行React组件化开发,开发过程中还遇到了些问题,页面还有很多功能还未完善,现在只有一个雏形,大家就浅看一下吧,后续功能持续完善中~

组件展示

Screenshot_2022-06-23-00-11-11-933_com.kwai.m2u.jpg

我需要开发的页面如上图所示,整个页面分为五部分,底部为固定的导航栏,页面为模板页面,主要由搜索框,图标,轮播图,图片链接以及双层嵌套的导航Tab组合成的页面展示

组件设计思路

  • 底部栏:Footer 使用 Router 进行设计,用Link 进行跳转

  • 上部分搜索框: 点击跳转到搜索页面

  • 轮播图: 用Swiper 进行设计,自动轮播

  • 图片导航:主要用 css 实现

  • 双层tab嵌套切换:第二层tab导航显示由一个变量控制,大Tab为active状态时,则显示对应的第二层tab,每个小tab里面又会有个数据展示,将主页面传过来的数据进行筛选,然后显示在对应的小tab下

  • loading 状态:在进行异步数据拉取时,页面白屏显得性能不好,设置一个loading 状态,在数据拉取时显示,数据拉取成功后消失

组件介绍完毕,我们就来讲讲具体的封装过程吧。

组件封装

先对项目进行脚手架的建构,取名为react-camera

   npm init @vitejs/app react-camera    
   

在src 目录下创建四个文件夹

image.png

  • api文件夹用来存放与数据相关的链接,组件所有的数据将会在这一个文件夹下的request.js中使用ajax进行数据请求

  • assets 文件夹主要用来存放静态资源,像照片之类的资源

  • pages 文件夹用于放置与路由相关的页面,组件的底部栏是使用路由来配置的,底部栏的页面都将在pages里面展示

  • components 文件夹用来放置除去路由页面以外的组件

底部导航栏的封装

  1. 底部导航栏主要使用路由实现,需要安装一下包

npm i react-router react-router-dom

注意一下,实现路由必须在在入口文件main.jsx中用 Browserrouter将包裹住,这样路由才能正常使用

底部导航栏包括四个页面,用路由实现,那么在pages文件夹下创建四个文件夹代表底部栏的四个页面,在App.jsx中引入
底部导航栏固定在在所有页面上,因此在components 文件夹下建立Footer 文件夹用来实现底部栏

 import {Routes,Route} from 'react-router-dom'
    
    import Tem from './pages/Tem'
    
    import Vedio from './pages/Vedio'
   
    import Pic from './pages/Pic'
    
    import Kd from './pages/Kd'      
    
    import Header from './components/Header'

使用RoutesRoute ,Route设置每个文件夹设置路由路径,Routes 是将所有的Route 组合成一个数组,方便管理

 <div className="App">
      <Header/>
      <Routes>
        <Route path="/temp" element={<Tem/>}></Route>
        <Route path="/pz" element={<Pic/>}></Route>
        <Route path="/vedio" element={<Vedio/>}></Route>
        <Route path="/kd" element={<Kd/>}></Route>
      </Routes>
      <Footer/>
      </div>    

现在去Footer 文件夹下实现导航栏 ,路由有了路径还不够,要能实现跳转还需要使用Link

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

const Footer =() => {
  const {pathname} = useLocation()
  if(['/select'].indexOf(pathname) != -1 ) return 
  return(
    <FooterWrapper>
      <Link to="/temp" className={classnames({active:pathname == '/temp'})}>
            <span>模板</span>
          </Link>
        <Link to="/pz" className={classnames({active:pathname == '/pz'})}>      
            <span>拍照</span>
          </Link> 
          <Link to="/vedio" className={classnames({active:pathname == '/vedio'})}>
            <span>视频</span>
          </Link>
          <Link to="/kd" className={classnames({active:pathname == '/kd'})}>
            <span>跟拍卡点</span>
          </Link>   
            
    </FooterWrapper>  
  )
}

export default  Footer

路由 acitve 状态的改变

 const {pathname} = useLocation()  // 将地址解构出来
  1. 要实现点击时Link 的active状态的改变,可以使用useLocation() 这个api , useLocation() 可以拿到当前页面的web地址,将 pathname 从地址中解构出来,在className中判断pathname 是否为当前点击的地址路径 ,这样我们就实现了导航栏的切换

  2. 当然我们也要注意一下类名中的classnames,这是一个实用的工具库。

    传统的添加类名的方式 className={} 只能添加一个类名,而引入classnames这个工具库可以使className 同时添加多个类名

    对于动态切换导航栏来说,引入classnames 可以更加简单的达到动态添加类名的效果。

className={classnames({active:pathname == '/pz'})}  

实现路由切换的效果为:

未标题-4.gif

模板页面的封装

image.png 四个部分分为四个组件进行封装 Tem模板页面则用来连接和展示组件

import React,{useState,useEffect} from 'react'
import Search from './search'
import Banners from './Banners'
import Tpmb from './Tpmb'
import Spmb from './Spmb'
// import Childrenrouter from './Childrenrouter'
import { getBanners, getTpmb, getSpmb } from '../../api/request'  
import { Navbar ,Links} from './style'
import WeUI from 'react-weui'  
const { Toast }  = WeUI; 

export default function Tem() {
  const [showloading,setShowLoading] = useState(false)
  const [showTpmb,setShowTpmb] = useState(false)
  const [showSpmb,setShowSpmb] = useState(false)
  const [tab ,setTab ] = useState('tpmb')
  const [banners,setBanners] = useState([])
  const [tpmbda,setTpmbda] = useState([])
  const [spmbda,setSpmbda] = useState([])
  useEffect(() =>{
    (async()=>{
      setShowLoading(true)
      let {data:banner} = await getBanners();
      setBanners(banner)
     let { data:result} = await getTpmb()
     setTpmbda(result)
     let {data:spm} = await getSpmb()
     setSpmbda(spm)
     setShowLoading(false)
      // console.log(data)
      if(tab == "tpmb"){
        setShowTpmb(true)
        setShowSpmb(false)
      }else if(tab == "spmb"){
        setShowTpmb(false) 
        setShowSpmb(true)
      }
    })()
  },[tab])

  return (
   <div>
    <Search />
    <Banners banners={banners}/>
    <Navbar>
    <div className="nav-links">
        <ul >
            <li><a className='lnk-xiutu' href="">修图</a></li>
            <li><a className='lnk-pintu' href="">拼图</a></li>
            <li><a className='lnk-erciyuan' href="">二次元脸</a></li>
            <li><a className='lnk-tonghua' href="">童话脸</a></li>
        </ul>
      </div>
    </Navbar>
   
    <Links>
    <ul >
      <li>
        <a className={tab=="tpmb"?'active':''} onClick={() =>setTab('tpmb')}>
          <span>图片模板</span>
        </a>
        </li>
      <li>
        <a className={tab=="spmb"?'active':''} onClick={()=>setTab('spmb') }>
        <span>视频模板</span>
        </a>
        </li>
    </ul>  
      </Links>
      <Toast icon="loading" show={showloading}>加载中</Toast>
     {showTpmb && <Tpmb tpmbda={tpmbda}  />}
    
      {showSpmb && <Spmb spmbda={spmbda}/>}
   </div>
  )
}

数据请求功能

轮播图和双层嵌套Tab导航都需要数据请求,为了方便展示,我使用在线接口Mock工具fastmock 在线模拟ajax请求,实现前端的页面展示(这个工具十分方便,在没有后端程序的情况下可以实现ajax请求,有需要的小伙伴可以去尝试)

  1. 在数据请求api文件夹下的request.js 进行axios 数据请求
import axios from 'axios'

export const getBanners = () => 
    axios.get('https://www.fastmock.site/mock/61dc0b0b924d225e00742e9519eb4e55/beers/banners')

export const getSpmb = () => 
    axios.get('https://www.fastmock.site/mock/61dc0b0b924d225e00742e9519eb4e55/beers/spmb')
export const getTpmb = (  ) =>
   axios.get('https://www.fastmock.site/mock/61dc0b0b924d225e00742e9519eb4e55/beers/tpmb')
  1. 在主页面的UseEffect中使用 async + await 实现 同步使用数据

    数据拉取成功后,将数据作为变量传入对应组件

<Banners banners={banners}/>

loading的实现

数据拉取总是需要一些时间嘛,这时候屏幕一直白屏,用户体验感非常不好,于是我在数据请求的时候设置了一个showloading 变量,来实现加载中的状态

  1. 首先需要安装weui 和 react-weui 这个组件库,将Toast 从中解构出来,制作一个加载中的弹窗
<Toast icon="loading" show={showloading}>加载中</Toast>

weui是微信官方制作的一个基础样式UI库,打造与原生微信同样的视觉和交互体验,里面有很多可以直接使用的样式框架,建议多多使用,组件开发中很多样式都可以在里面找到

  1. 设置一个变量showloading,在请求数据前将showloading 状态变为true ,数据拉取结束后变回false

const [showloading,setShowLoading] = useState(false)

主要实现效果为:

10(1).gif

轮播图的实现

轮播图则使用Swiper 进行实现

swiper 是一款轻量级的轮播图插件,可以支持pc端和移动端的使用,他可以快速的做出一个轮播图,十分方便快捷

实现效果图为:

1.gif

useEffect(() =>{
        new Swiper('.btn-banners',{
            loop:true,
            pagination:{
                el:'.swiper-pagination' //幻灯片滑块
            },
            autoplay:{ //自动轮播
              delay:3000
          }
        })
    },[])

autoplay 属性实现图片的自动轮播

这个轮播图有一个需要注意一下:useEffect需要传一个空数组当第二个参数,如果不传,组件稍有变化,轮播图就会改变,导致轮播图的自动播放有点魔性,传空数组则表示轮播图的更新什么都不依赖。

搜索框的实现

8.gif

搜索框主要由css 样式构成,增加了一个点击事件,点击进入搜索页面,点击取消则返回首页 ,这个是由路由实现的,在App.jsx 中新增一个searchK路由

<Route path="/select" element={<Searchk/>} />

返回首页用Link 实现十分方便

 <Link to="/temp"> <span className="im">取消</span></Link>
import React from 'react'
import { SearchWrapper,Tab } from './style'
import {Link} from 'react-router-dom'

export default function Search() {
    return (
      <SearchWrapper>
        <Tab>
          <Link to="/select">
          <form action="">
            <p>
          <span className="bn">
            <input type="submit" value="搜索"/>
          </span>
          <span className="inp"><input type="text" placeholder='珠光'></input></span>
          <span className="im"></span>
          </p>
          </form>
          </Link>
        </Tab>       
      </SearchWrapper>
    )
  }

图片导航栏的实现

3.gif

本次图片导航单纯用css 实现,只是一个样式,后续功能还在完善中~

功能:

  1. 导航栏的滑动: 使用overflow:auto实现,ul不要忘记display:flex,否则无法滑动哈

  2. 图片的显示则是参考豆瓣页面导航,在li里添加背景图片,再将文字行高拉大,达到隐藏文字的效果

.lnk-xiutu {    
        background:transparent url('http://localhost:3000/src/assets/images/t1.jpg' )
    }

像这样,transparent url() 是为了将图片颜色显示出来
li中的样式 background-size:cover; 则让背景图片缩放成背景区域大小

双层Tab导航切换的实现

由于开发组件的时候还没有学习二级路由,于是页面中的导航只能用tab 键来实现了,后续还会继续完善!
实现效果大概如下:

5.gif

<Links>
    <ul >
      <li>
        <a className={tab=="tpmb"?'active':''} onClick={() =>setTab('tpmb')}>
          <span>图片模板</span>
        </a>
        </li>
      <li>
        <a className={tab=="spmb"?'active':''} onClick={()=>setTab('spmb') }>
        <span>视频模板</span>
        </a>
        </li>
    </ul>  
      </Links>
      <Toast icon="loading" show={showloading}>加载中</Toast>
     {showTpmb && <Tpmb tpmbda={tpmbda}  />}
    
      {showSpmb && <Spmb spmbda={spmbda}/>}

tab键active状态的改变

  1. 点击进行tab 键active的转换,下面的第二层导航栏则封装成一个组件<Tpmb /><Spmb /> 声明两个变量showTpmb showSpmb 在useEffect中监听tab的状态 ,tab改变则显示对应的组件
 const [showTpmb,setShowTpmb] = useState(false)
 const [showSpmb,setShowSpmb] = useState(false)
useEffect(()=>
if(tab == "tpmb"){
        setShowTpmb(true)
        setShowSpmb(false)
      }else if(tab == "spmb"){
        setShowTpmb(false) 
        setShowSpmb(true)
      }
       },[tab])
  1. active状态时下面下划线的改变
    下划线主要使用伪元素制作而成
  • 外层盒子使用display:inline-block ;position:relative;将盒子变为块级元素,并使子元素相对它定位
  • 内层的a中对active状态进行设置,点击颜色和下划线发生改变
&.active::before{
            content: " ";
            color:red;
            position: absolute;
            left:25px ;
            top: 0;
            width: 50%;
            height: 100%;
            justify-content: center;
            border-bottom: 2px solid silver;
        }
        &.active{
            color:#000;
        }

第二层导航栏的实现

数据由主页面中传进来 ,在这里根据第二层导航栏的tab切换进行数据筛选 ,再将图片显示在点击的选项下

这里要注意一点!!!

  1. 数据筛选需要在主页面中拉取传进来后再进行,否则第二层tab键active状态的改变无法传到父组件中,那么数据筛选就是无用的!
  2. 导航栏中的a 标签href中需要加上“#” ,阻止a标签的默认行为发生,否则数据筛选后页面会直接跳转,无法长久显示!!
useEffect(() =>{
  setShowLoading(true)
    switch(tab1){
      case"hot":
      tpmbda =tpmbda.filter(item => item.type == "hot");
      break;
      case"new":
      tpmbda = tpmbda.filter(item => item.type == "new");
      break;
      case"summy":
      tpmbda= tpmbda.filter(item => item.type == "summy");
      break;
      default:
        break;
    }
  setDa([...tpmbda])
},[tab1]

实现效果如下图:

9_2 (1).gif

结束语

至此一甜相机组件开发的模板页面样式已经大致出来,react组件中里面还有很多小细节功能还未完善,待我学成归来,我将继续完善这个页面,大家敬请期待~,感兴趣的话就点个小赞再走吧~

项目源码地址:youzizy1/react-camera.github.io