react开发模板,最适合新手的react组件,仿学习通首页组件

645 阅读7分钟

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

前言

作为一名前段学习者,react是我们不可缺少的web开发工具。但对一名react初学者来说,要想开发一个比较完成的移动端组件,还是有点难度的,这时候我们就需要去参照一个模板,而我这个项目思路清晰,逻辑简单,可以作为一个参照(我也是参考别的项目写的,小声哔哔...)。只要你电脑安装了node.js,就能让项目跑起来!

项目简介

预期效果

IMG_0842.PNG IMG_0840.PNG

第一张为首页,也就是默认显示页;第二张为我的大学详情页,通过首页顶部下拉菜单进行路由跳转。

实际效果

微信图片_20220629215108.png 微信图片_20220629215113.png

目录介绍

微信图片_20220628210030.png

合理的目录结构有利于项目管理,功能分配,每个文件夹都有明确的分工,这样子不仅开发时思路清晰,后期项目的更改也更方便,下面介绍一个src下每个文件的具体分工。

  • api--获取fastmock接口数据,所有的数据请求都在这个文件完成
  • assets--存放一些静态资源,如iconfont图片库
  • components--顾名思义,放置大大小小的组件,其中的子文件夹common放置一些可复用组件,如轮播组件比较常用的
  • config--js文件,一些自定义配置,如别名配置
  • pages--放置页面级别组件
  • routes--路由配置文件,所有的路由跳转放在这里

依赖包

  • antd-mobile:蚂蚁金服组件库,有很多常用组件,非常好用-文档
  • axios: 数据请求工具
  • react-router,react-router-dom: 路由跳转功能,二级路由可实现页面局部刷新
  • styled-components: css in js的样式编写风格
  • swiper: 轮播图插件

详细组件介绍

底部Tab导航栏

移动端组件开发中底部Tab导航栏是必不可少的,实现非常简单,将Footer组件用position:fixed固定挂载在页面底部,四个Tab盒子用flex布局平均分配空间,每个Tab用Link进行一级路由跳转,从而实现主体内容刷新,达到Tab切换效果。

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

export default function Footer() {
  return (
    <FooterWrapper>
        <Link to="/home">
            <i className='iconfont icon-31shouye'></i>
            <span>首页</span>
        </Link>
        <Link to="/message">
            <i className='iconfont icon-xiaoxi1'></i>
            <span>消息</span>
        </Link>
        <Link to="/note">
            <i className='iconfont icon-biji1'></i>
            <span>笔记</span>
        </Link>
        <Link to="/mine">
            <i className='iconfont icon-31wode'></i>
            <span></span>
        </Link>
    </FooterWrapper>
  )
}

Footer组件放在其父组件,也就是根组件App.jsx中,路由跳转内容要放在父组件相应位置中占位,所占的页面位置就是路由区域。

import { Suspense } from 'react'
import './App.css'
import RoutesConfig from './routes'
import Header from './components/Header'
import Footer from './components/Footer'

function App() {
  return (
    <div className="App">
      <Header></Header>
      <Suspense fallback={<div>loading...</div>}>
        <RoutesConfig></RoutesConfig>
      </Suspense>
      <Footer></Footer>
    </div>
  )
}
export default App

所有路由配置全都封装在RoutesConfig中,除了首页默认显示路由,其余路由都用lazy懒加载引入,配合Suspense组件包裹路由,达到缓加载的效果,可加快首页渲染速度。Suspensefallback属性接受任何在组件加载过程中你想展示的React元素,在加载完成之前显示。

import React from 'react'
import { lazy } from 'react'
import { Routes,Route } from 'react-router-dom'
import Home from '../pages/Home'
import HomeCommon from '../components/HomeCommon'
import HomeRead from '../components/HomeRead/inde'
import HomeFind from '../components/HomeFind'
import HomeConcern from '../components/HomeConcern'
import HomeVideo from '../components/HomeVideo'
const Mine =lazy(()=>import('../pages/Mine'))
const Message =lazy(()=>import('../pages/Message'))
const Note =lazy(()=>import('../pages/Note'))
const MyUniversity =lazy(()=>import('../pages/MyUniversity'))
export default function RoutesConfig() {
  return (
    <div>
      <Routes>
        <Route path='/' element={<Home />}></Route>
        <Route path='/home' element={<Home />}>
            <Route path='/home/common' element={<HomeCommon />}></Route>
            <Route path='/home/find' element={<HomeFind />}></Route>
            <Route path='/home/concern' element={<HomeConcern />}></Route>
            <Route path='/home/read' element={<HomeRead />}></Route>
            <Route path='/home/video' element={<HomeVideo />}></Route>
        </Route>
        <Route path='/message' element={<Message />}></Route>
        <Route path='/mine' element={<Mine />}></Route>
        <Route path='/note' element={<Note />}></Route>
        <Route path='/myuniversity' element={<MyUniversity />}></Route>
      </Routes>
    </div>
  )
}

效果

GIF 2022-6-30 13-57-42.gif

二级路由Nav导航栏

二级路由在移动端也是非常的常见,我这里的二级路由NavBar使用ul列表做的,列表水平排列,通过overflow-x: auto设置水平方向滚动,white-space:nowrap设置不换行,从而达到超出屏幕宽度滚动的效果;路由跳转使用字符串模板${}跳转到相应路径,二级路由主体内容要在其父组件用Outlet进行占位,这样才能在切换NavBar时达到页面局部刷新的效果。

export default function HomeNav() {
    let homenavs=[
        { id:1,desc:'常用',path:'/common'},
        { id:2,desc:'发现',path:'/find'},
        { id:3,desc:'关注',path:'/concern'},
        { id:4,desc:'微读书',path:'/read'},
        { id:5,desc:'微视频',path:'/video'},
    ]  
  return ( 
    <Wrapper>
      <div className="nav-box">
        <ul id='list'>
          {
            homenavs.map((item,index)=>{
              return(
              <li key={item.id}>
                  <NavLink 
                      index={index}
                      to={`/home${item.path}`}
                      className="nav-item"
                    >
                        {item.desc}
                  </NavLink>
                </li>
              )
            })
          }
        </ul>
      </div>
    </Wrapper>
  )
}

父组件Outlet占位

return (
    <Wrapper>
      <HomeSearchBar></HomeSearchBar>
      <HomeNav></HomeNav>
      <Outlet></Outlet>
    </Wrapper>
  )

NavBar选中时添加底部下划线,NavLink路由的css属性有个active属性,选中时才会添加,所以使用伪元素active::after就可以在其底部添加下划线。

&.active::after
                {
                    content: "";
                    background-color: blue;
                    width:1rem;
                    height: 0.1rem;
                    position: absolute;
                    bottom: 0;
                    left: 0;
                    right: 0;
                    margin-left: auto;
                    margin-right: auto;
                }

效果

GIF 2022-6-30 15-01-17.gif

常用课程列表

课程列表分为“常用”和“最近使用”,都放在一个数据请求中,其中有个checked属性,如果为true则添加到常用列表,为false则在最近使用列表中;数据请求在其父组件完成,然后传给子组件,子组件通过map循环输出;css样式用flex布局,一般flex布局可以解决绝大部分布局设计。

export default function CommonCourse({course, addToCommon}) {
    // console.log(course);
    return (
        <Wrapper>
            <div className="list">
                {
                    course.map((item, index) => {
                    return (
                    item.checked &&
                        <div className="list-item" key={item.id}>
                            <div className="list-left">
                                <img src={item.img} alt="" />
                            </div>
                            <div className="list-center">{item.desc}</div>
                            <div className="list-right">
                                <i>
                                    <a href="#">&gt;</a>
                                </i>
                            </div>
                        </div>
                    );
                })
                }
            </div>
            <div className="interval"></div>
            <div className="recent">最近使用</div>
            <div className="list">
                {course.map((item, index) => {
                    return (
                    !item.checked &&
                        <div className="list-item" key={item.id}>
                            <div className="list-left">
                                <img src={item.img} alt="" />
                            </div>
                            <div className="list-center">{item.desc}</div>
                            <div className="list-right">
                                <button onClick={() => addToCommon(item)}>+常用</button>
                            </div>
                        </div>
                    );
                })}
            </div>
        </Wrapper>
    );
}

添加功能及模态框的实现

添加功能及模态框的实现在父组件完成

前面说到数据对象有个checked属性可以判断是否为常用课程,所以可以通过给button点击事件添加一个立即执行函数,返回值为父组件传过来的添加函数,该函数接受一个参数item,该参数为点击的当前列表项,所以我们可以通过let idx = course.findIndex(data => e.id === data.id)拿到该项列表id,然后修改该项checkedboolean值,就能实现将课程添加到常用了。最后还要通过useEffect修改拿到的数据状态,useEffect会在每次页面更新后执行。

modal模态框的实现,我给模态框组件设置了一个loading状态,初始值为false,在触发添加函数时,改变其状态为true,并且触发定时器函数,定时为2s,2s后loading状态变回false

export default function HomeCommon() {
    const [course, setCourse] = useState([]);
    const [loading, setLoading] = useState(false);
    // 提示模态框
    const modal = () => {
        return (
            loading && (
                <Modal>
                    <span>添加成功!</span>
                </Modal>
            )
        );
    };
    // 定时让模态框消失
    const setState = () => {
        setTimeout(() => {
            setLoading(false);
        }, 2000);
    };
    // 改变数据checked,将数据添加到常用
    const addToCommon = e => {
        let idx = course.findIndex(data => e.id === data.id);
        course[idx].checked = !course[idx].checked;
        setCourse([...course]);
        setLoading(true);
        setState();
    };
    useEffect(() => {
        (async () => {
            let { data } = await getCourse();
            setCourse(data);
        })();
    }, []);
    return (
        <>
            {modal()}
            <Wrapper>
                <div className="commonWrapper">
                    <CommonCourse course={course} addToCommon={addToCommon}></CommonCourse>
                </div>
            </Wrapper>
        </>
    );

效果

GIF 2022-6-30 16-35-59.gif

(说明一下,这里添加后样式没有任何问题,可能是生成gif动图的原因导致看上去列表样式不一样)

顶部下拉菜单

下拉菜单我这里引用了antd-mobile组件库中的Dropdown组件,然后在下拉内容中添加了一个路由跳转;顶部的标题内容使用useLocation拿到当前地址,然后根据地址对应的标题使用字符串模板显示顶部区域的标题,当切换不同路由时,顶部标题会相应变化,这里要注意的是,在useEffect中不能带第二个参数,否则不能在页面渲染后一直更改标题,只会在首页渲染后执行一次。

在顶部我还写了一个弹出层组件,也可跳转路由,但由于其比下拉菜单复杂,并且在学习通中也是下拉菜单,所以就不介绍弹出层Popup组件了。

import React, { useEffect,useState } from 'react'
import { HeaderWrapper } from './style'
import { useLocation } from 'react-router-dom'
import { pageTitle } from '../../config'
import HeaderPopup from '../common/HeaderPopup'
import { Popup,Dropdown } from 'antd-mobile'

export default function Header() {
    const currentpathname='/myuniversity'
    const [title,setTitle]=useState('首页')
    const {pathname='/'}=useLocation()
    // console.log(pathname)
    useEffect(()=>{
        let _title=pageTitle[pathname] || ''
        // console.log(_title)
        setTitle(_title)
    })
    const [visible2, setVisible2] = useState(false)
    const onCloseVis=()=>{
      setVisible2(false)
    }
    if(pathname==currentpathname) {
      return
    }
  return (
    <HeaderWrapper>
        <Dropdown className='dropdown'>
          <Dropdown.Item key='sorter' title={title} >
            <HeaderPopup title={title} visible2={visible2} onClose={onCloseVis} ></HeaderPopup>
          </Dropdown.Item>
        </Dropdown>
        <i className='header-right'>&gt;</i>
        // 弹窗层注释代码
        {/* <header onClick={()=>{
        setVisible2(true)
      }}>{title}<i className='iconfont icon-xiangxia'></i></header>  
      <i className='header-right'>&gt;</i>
      <Popup
        visible={visible2}
        onMaskClick={()=>{
          setVisible2(false)
        }}
        position='top'
        bodyStyle={{height:'20vh'}}
        >
          <HeaderPopup title={title} visible2={visible2} onClose={onCloseVis} ></HeaderPopup>
        </Popup> */}
    </HeaderWrapper>
  )
}

效果

GIF 2022-6-30 17-27-45.gif

大学详情组件

顶部返回主页

点击“<”返回到首页,引用了react-router-dom中的useNavigate,在其实例化对象中加入需要跳转的路径即可。顶部的导航引用的是antd-mobie中的NavBar组件,其属性onBack是点击返回区域的回调。

const navigate=useNavigate()
<NavBar   
            right={right()}
            onBack={()=>navigate('/home')}     
        >

搜索框组件

antd-mobile是真的好用,基本上常用的组件都有。

<div className='search'>
          <div className='search-item'>
            <SearchBar placeholder='超星发现' showCancelButton />
          </div>
        </div>

GIF 2022-6-30 18-09-07.gif

轮播图组件

轮播图可以用swiper插件,但个人觉得antd-mobile中的走马灯Swiper组件比较好用,且简单。只需要将几个图片资源放在Swiper.itemmap循环即可。其中Swiper的参数autoplay为自动播放,loop为循环播放。

import React from 'react'
import { Wrapper } from './style'
import { Swiper } from 'antd-mobile'

const imgs = [
        'https://ts1.cn.mm.bing.net/th?id=OIP-C.YdQb5SkG6H6GP_T4MiCCSgHaE8&w=306&h=204&c=8&rs=1&qlt=90&o=6&dpr=1.38&pid=3.1&rm=2', 
        'https://tse2-mm.cn.bing.net/th/id/OIP-C.5DPKoPDHLOPtGeYdPQMKYgHaC9?w=309&h=140&c=7&r=0&o=5&dpr=1.38&pid=1.7',
        'https://tse3-mm.cn.bing.net/th/id/OIP-C.ttDBZdgY1KlHjlLikkbrugHaFT?w=243&h=180&c=7&r=0&o=5&dpr=1.38&pid=1.7'
]
const items = imgs.map((img, index) => (
  <Swiper.Item key={index}>
    <div
      className='img-item'
    >
      <img src={img} alt="" />
    </div>
  </Swiper.Item>
))
export default function UniBanner() {
  return (
    <Wrapper>
      <Swiper autoplay loop>{items}</Swiper>
    </Wrapper>
  )
}

效果

GIF 2022-6-30 18-02-32.gif

其他

剩余的部分都是简单的列表,都采用display:flex布局,图标是iconfont库中引入的

微信图片_20220630185355.png

总结

至此,项目结束,遗憾的是,侧滑删除组件没能实现,查阅了一些相关文档,尝试了去做侧滑删除组件,但都没能成功,今后会加以完善。通过这次项目实践,我认识到,对于新手而言,一定要去尝试把项目写出来,不要觉得难,分成一小块一小块的去写,总能写出来,实践是解决问题的唯一办法。

源码地址

源码地址gitee.con/...