React 组件实战,仿起点移动端创造一个属于自己的网文世界

1,605 阅读9分钟

前言

组件化是React的核心思想。

一个完整的react项目包含许多的业务逻辑,这时候就需要组件化登场了。它通过提供了一种项目开发方式,把任何的应用都抽象成一颗组件树,让我们可以开发出一个个独立的小组件来构造我们的应用,从而达到组件复用、代码便于管理、项目协同开发的效果。

笔者最近在学习React的组件开发,再加上酷爱看小说于是就萌生了挑战起点移动端的页面进行React组件化开发的想法,开发过程中还遇到了些问题,页面还有很多功能还未完善还有不足之处,希望大佬们能在评论区指点一番。

组件展示

废话不多说,直接展示:

  • 首页展示 1.gif
  • 路由跳转

2.gif

技能清单

要想实现需要安装各种框架插件和依赖,下面是我在该项目所用到的插件和依赖:

"dependencies": {
    "axios": "^0.27.2", : 
    "font-awesome": "^4.7.0",
    "gh-pages": "^4.0.0",
    "prop-types": "^15.8.1",
    "react": "^18.0.0",
    "react-dom": "^18.0.0",
    "react-router": "^6.3.0",
    "react-router-dom": "^6.3.0",
    "styled-components": "^5.3.5",
    "swiper": "^4.5.0"
  },

axios:用于获取后端api数据且本项目后端数据是通过fastmock自己编写的

font-awesome:是一种主题图标库,在这可以通过不下载到项目就可以使用不过就是不支持中文搜索比较麻烦。与之相对应的可以用iconfont,一款由阿里开发的字体图标库,优点是支持中文搜索且资源丰富,缺点是需下载才能使用。

gh-pages: 当项目完成后,使用npm run build打包好将其上传到gh-pages分支,github会给一个二级域名能让你的项目运行并展示。

props-types:用于组件间传值时对值的校验。

react-router,react-router-dom:提供了许多相关的路由组件。

styed-componets:搭配React 框架使用,能让其支持 CSS in JS 的写法,提供样式组件。

swiper:使用轮播图需要的依赖。

正文

划分组件

react中一切皆组件,废话不多说直接将项目的组件组成划分出来,如下:

SelectNav: 头部组件
Footer:底部组件
Slect: 选择组件
Male: 性别组件,Select组件的子组件 实现男女生模式切换 Banners:轮播图组件, Male组件的子组件 用于轮播图的实现
Bestsellers:畅销组件
Navbars:导航组件
Recommend:推荐组件

文件架构

划分的一个个组件及父子组件,如下图所示:

QQ截图20220703184903.png

组件设计与实现

路由配置

此时需要安装一些相关的包 相关命令:npm i react-router react-router-dom Footer组件More组件Select组件都使用了Router进行设计用NavLink进行跳转,同时引入lazySuspense对进入首页不需要立即加载的内容使用了路由懒加载并解决路由跳转需重新刷新的问题,来提升首页加载速度。再使用redirect来使路由重定向使页面一打开就是你所设置的页面。

import { lazy, Suspense } from 'react'
import { Routes, Route, Navigate } from 'react-router-dom'
import Select from '@/pages/Select'
const Bookshelf = lazy(() => import('@/pages/Bookshelf'));
const Find = lazy(() => import('@/pages/Find'));
const Mine = lazy(() => import('@/pages/Mine'));
import Male from '../pages/Select/Male';
const Female = lazy(() => import('@/pages/Select/Female'))
const More = lazy(() => import('@/pages/More'))

const RouterConfig = () => {
    return (
        <Suspense fallback={null}>
            <Routes>
                {/* redirect 重定向 */}
                <Route path="/" element={<Navigate to="/select/male" replace={true} />} />
                <Route path="/select" element={<Select />}>
                    <Route path="/select/male" element={<Male />} />
                    <Route path="/select/female" element={<Female />} />
                </Route>
                <Route path="/bookshelf" element={<Bookshelf />} />
                <Route path="/find" element={<Find />} />
                <Route path="/mine" element={<Mine />} />
                <Route path="/more" element={<More />} />
            </Routes>
        </Suspense>
    )
}
export default RouterConfig

Footer底部组件的实现

  1. Footer底部组件的实现,使用了固定定位对其布局,使得项目进行页面跳转时Footer组件仍在底部,但不是所有的页面都需要底部,在那时我们又该怎么做呢?不要慌张,我们可以在utils文件下定义一个isPathPartlyExisted()函数来通过当前页面的路由路径来判断是否需要底部,代码如下:
export const isPathPartlyExisted = (path) => {
    const arr = ['/more'];
    let pathRes = path.split('/')
    if (pathRes[1] && arr.indexOf(`/${pathRes[1]}`) != -1) return true;
    return false
  1. 请各位看官仔细看看下面的代码,细心的看官可能已经发现了(不细心也没关系😂)路由跳转使用了NavLink标签,那么它相对于Link标签有什么好处呢?其实当我们使用NavLink标签选中时它的className属性会增添一个active的css,使得此时的路由高亮如下效果:

Footer.gif

export default function Footer() {
    const { pathname } = useLocation()
    if (isPathPartlyExisted(pathname)) return
    return (
        <FooterWrapper>
            <NavLink to="/bookshelf" >
                <i className="iconfont icon-shu"></i>
                <span>书架</span>
            </NavLink>
            <NavLink to="/select" >
                <i className="iconfont icon-jingxuan-"></i>
                <span>精选</span>
            </NavLink>
            <NavLink to="/find" >
                <i className="iconfont icon-faxian"></i>
                <span>发现</span>
            </NavLink>
            <NavLink to="/mine" >
                <i className="iconfont icon-wo"></i>
                <span></span>
            </NavLink>
        </FooterWrapper>
    )
}
  1. 细节
  • 由于swiper样式自带相对定位那么拥有轮播图组件就会影响Footer的固定定位会覆盖Footer,所以需要给Footer组件样式的固定定位增加权重z-index:1;

Banners:轮播图组件的实现

Banners.gif

  1. 首先要实现轮播图组件,我们得先拿到轮播图的相关数据参数。为在没有后端程序的情况下可以实现ajax请求,我使用在线接口Mock工具fastmock(网站链接在这[www.fastmock.site/#/]) 在线模拟ajax请求。
  • 在数据请求api文件夹下的request.js 进行axios 数据请求
import axios from 'axios'

export const getBanners = () =>
    axios.get('https://www.fastmock.site/mock/d561e8a3e79db6ef8c66099546e04efa/qidian/banners')
  • 在从父组件Male中的UseEffect中使用 async + await 实现 同步使用数据,数据拉取成功后,将数据作为变量传入子组件Banners将数据循环输出。
banners.map(item =>
                    (
                        <div className="swiper-slide" key={item.id}>
                            <NavLink
                                to="/eleme/all"
                                className="swiper-item"
                            >
                                <img src={item.img} alt="" width="100%" />
                            </NavLink>
                        </div>
                    )
           )
  1. 数据拿到后,我们在接下来就要使用swiper来实现轮播图,轮播图的具体实现有固定的的html结构,看官们只要按照固定格式,即可实现轮播功能其中。但要注意swiper的版本和相关参数的使用。这里笔者使用的是swiper4.5.0版本,其中的相关参数分别是:loop:是否循环轮播 dalay:循环时间 pagination:分页器。还有必须注意的是swiper只能实例化一次所以我们可以在生命周期函数useEffect 中 做个判断如:
      let swiper = null;
    useEffect(() => {
           if (swiper) {
                return
           }
        new Swiper('.btn-banners', {
            loop: true,
            autoplay: {
                delay: 3000
            },
            pagination: {
                el: '.swiper-pagination'
            }

        })
    })

或者给个空数组不然组件稍有变化,轮播图就会改变,导致轮播图的自动播放有点魔性,传空数组则表示轮播图的更新什么都不依赖。

    useEffect(() => {      
        new Swiper('.btn-banners', {
            loop: true,
            autoplay: {
                delay: 3000
            },
            pagination: {
                el: '.swiper-pagination'
            }
        })
    },[])

Bestsellers:畅销组件

Best.gif

  1. Bestsellers:畅销组件实现第一步如上一个组件一样咱们先拿到相关的数据
  • 在数据请求api文件夹下的request.js 进行axios 数据请求
export const getBestsellers = () =>
    axios.get('https://www.fastmock.site/mock/d561e8a3e79db6ef8c66099546e04efa/qidian/bestsellers')
  • 在从父组件Male中的UseEffect中使用 async + await 实现 同步使用数据,数据拉取成功后,将数据作为变量传入子组件Banners将数据循环输出。由于循环代码太长先将其封装到一个函数renderBtnBannersPage提高代码的可读性。

const renderBtnBannersPage1 = () => {
        // console.log(bestsellers)
        return bestsellers.map(item => {
            return (
                <li key={item.id}>
                    <NavLink
                        to="/eleme/all"
                        key={item.id}
                    >
                        <div className="item">
                            <p><img src={item.img} /></p>
                            <div className="content">
                                <span className='title'>{item.title}</span>
                                <p> {item.content}</p>
                                <div className="label">
                                    <p className='mear'>{item.mear}</p>
                                    <p className='type'>{item.type}</p>
                                </div>
                            </div>
                        </div>
                    </NavLink>
                </li>
            )
        })
    }
  1. <h2>更多</h2>一个Link标签 点击它可以去到More组件,
 <Wrapper>
            <div className="module">
                <div className="module-header">
                    <div className='nav'>
                        <h2>畅销精选</h2>
                        <NavLink className='icon' to="/more">
                            <h2>更多</h2>
                            <div >
                                <p className='iconfont icon-gengduo'></p>
                            </div>
                        </NavLink>
                    </div>
                </div>
                <ol className="book">
                    {renderBtnBannersPage1()}
                </ol>
            </div>
        </Wrapper>
  • 最后再用propTypes给父子组件的传值校验一下,代码如下:
Bestsellers.propTypes = {
    bestsellers: propTypes.array.isRequired
}

More组件

  1. 数据的拉取以及使用与其他组件无异,这里唯一可说的是返回路由和相关的弹性布局写法。 返回路由可通过<NavBar />来自于antd-mobile并用navigate进行路由出栈实现返回功能。
 {/* 路由出栈 */}
      <div className="detail-top">
        <NavBar
         
          onBack={() => navigate(-1)}
        >
          
        </NavBar>
  1. 还有重要的一步就是More页面是不需要Footer的所以要给Footer添加一个路由判断,前面Footer组件已经展示,感兴趣的看官可以去看一下。

Navbars:导航组件

NavBar.gif

  1. Bestsellers:畅销组件实现第一步如上一个组件一样咱们先拿到相关的数据
  • 在数据请求api文件夹下的request.js 进行axios 数据请求
export const getNavbars = () =>
    axios.get('https://www.fastmock.site/mock/d561e8a3e79db6ef8c66099546e04efa/qidian/navbars')
  • 在从父组件Male中的UseEffect中使用 async + await 实现 同步使用数据,数据拉取成功后,将数据作为变量传入子组件Banners将数据循环输出。由于循环代码太长先将其封装到一个函数

renderBtnBannersPage提高代码的可读性。其实笔者还可以在这做个二级路由来对每个NavBar进行跳转但由于笔者时间较短就未实现。

const renderBtnBannersPage1 = () => {
        // console.log(navbars)
        return navbars.map(item => {
            return (
                <NavLink
                    to="/eleme/all"
                    key={item.id}
                >
                    <div className="box">
                        <img src={item.img} />
                        <h4>
                            {item.title}
                        </h4>
                    </div>
                </NavLink>
            )
        })
    }
    return (
        <NavSwrapper>
            <div className="nav">
                <div className="nav-item">
                    {renderBtnBannersPage1()}
                </div>
            </div>
        </NavSwrapper>
    )
}
  • 最后再用propTypes给父子组件的传值校验一下,代码如下:
Navbars.propTypes = {
    navbars: propTypes.array.isRequired
}

Recommend组件

Recommend.gif

  1. 老规矩先拿到数据再做事
  • 在数据请求api文件夹下的request.js 进行axios 数据请求
export const getRecommend = () =>
    axios.get('https://www.fastmock.site/mock/d561e8a3e79db6ef8c66099546e04efa/qidian/recommend')
  • 在从父组件Male中的UseEffect中使用 async + await 实现 同步使用数据,数据拉取成功后,将数据作为变量传入子组件Banners将数据循环输出。由于我想让图片每四个排在一起所以对数据使用slice进行每四个切割。
const renderBtnBannersPage1 = () => {
        //let items = recommend.slice(0, 5)
        return recommend.slice(0, 4).map(item => {
            return (

                <NavLink
                    to="/eleme/all"
                    key={item.id}
                    className="navlink"
                >
                    <div className="item ">
                        <p><img src={item.img} /></p>
                        <div className="content">
                            <p className='title'>{item.title}</p>
                            <p className='type'>{item.type}</p>
                        </div>

                    </div>
                </NavLink>

            )
        })
    }
    const renderBtnBannersPage2 = () => {
        //let items = recommend.slice(5)
        return recommend.slice(4).map(item => {
            return (
                <NavLink
                    to="/eleme/all"
                    key={item.id}
                    className="navlink"
                >
                    <div className="item ">
                        <p><img src={item.img} /></p>
                        <div className="content">
                            <p className='title'>{item.title}</p>
                            <p className='type'>{item.type}</p>
                        </div>

                    </div>
                </NavLink>
            )
        })
    }
    return (
        <Wrapper>
            <div className="module">
                <div className="module-header">
                    <div className='nav'>
                        <h2>编辑推荐</h2>
                        <div className='icon' >
                            <h2>更多</h2>
                            <div >
                                <i className='iconfont icon-gengduo'></i>
                            </div>
                        </div>
                    </div>
                </div>
                <div className='swiper-container'>
                    <div className="swiper-wrapper">
                        <div className="swiper-slide">
                            {renderBtnBannersPage1()}
                        </div>
                        <div className="swiper-slide box">
                            {renderBtnBannersPage2()}
                        </div>
                    </div>
                </div>
                {/* <div className="swiper-scrollbar"></div> */}
            </div>
        </Wrapper>
    )
}
  1. 数据拿到后,我们在接下来就要使用swiper来实现轮播图,同样必须注意的是swiper只能实例化一次所以我们可以在生命周期函数useEffect 中 做个判断如:
let swiper = null;
    useEffect(() => {
        if (swiper) { return }
        swiper = new Swiper('.swiper-container', {
            
        })
    }, [])

SelectNav: 头部组件

Select.gif

  1. 为Select组件配置二级路由代码如下:
  • Select组件的inexpensive.jsx
let selectNavs = [
        { id: 1, desc: '男生', path: '/male' },
        { id: 2, desc: '女生', path: '/female' }
    ]
    
  • routes里的index.jsx
import Male from '../pages/Select/Male';
const Female = lazy(() => import('@/pages/Select/Female'))

<Route path="/select" element={<Select />}>
                    <Route path="/select/male" element={<Male />} />
                    <Route path="/select/female" element={<Female />} /></Route>

总结

不足与反思

  1. 由于项目时间较短,许多功能还未完善
  • 组件划分还不清楚甚至达到了复用的反作用如Male组件和Femal组件可用同一个组件,再添加点击事件对数据的所属进行判断再进行渲染。
  • Recommend组件不应该用swiper组件来写还有更加完好的方法。
  • 未写实现每本小说详情的Detail组件来复用。
  • 目前使用的数据都是由页面来进行管理,在接下来笔者会继续学习Redux的知识,做到组件与数据管理进行分离。
  • 图片延迟加载还未做,用react-lazyload 声明式组件 LazyLoad + placeholder 包住 原来的图片 。 接下来的时间笔者会继续完善该项目,希望各位看官能持续追更,陪笔者一起成长!

项目地址:github.com/ITgyw/qidia…

项目预览:itgyw.github.io/qidian1/