整个小红书刷个够吧 -(React Hooks+Redux 入门级实战项目)

2,120 阅读8分钟

image.png

前言

通过一段时间对React的学习,对React Hooks以及Rudex的React全家桶的学习,检验自己学习成果的时候到了,正所谓是骡子是马,拉出来溜溜就知道了。那不如就用小红书出来练练手吧,在一个小项目上对自己学的东西做个总结,正所谓实践出真理。

项目简介

  • react全家桶:react+react-router+redux
  • redux-thunk:处理异步逻辑的redux中间件
  • styled-components:css in js 的工程化工具
  • axios:用来请求后端api数据
  • react-lazyload:react 懒加载库
  • better-scroll:提升移动端滑动体验
  • 坚持前端MVVM的设计理念,遵循组件化、模块化的编程思想

直接上成果吧

小红书.gif

项目结构

react-redBook/
    node_modules/
    src/
        api/             //网络请求代码和相关配置
        assets/          //静态文件
        components/      //可复用的UI组件
        pages/           //页面
        routes/          //路由配置文件
        store/           //redux 相关文件
        utils/           //工具类函数
        App.jsx          //根组件
        main.jsx         //入口文件
        style.js        //默认样式
    index.html 
    package.json
    readme.md
    vite.config.js

前端的总体设计

路由配置

本项目使用react-router对路由进行配置

  • router/index.js代码如下
import { lazy,Suspense } from 'react'
import { Routes,Route,Navigate } from 'react-router-dom'
const Detail=lazy(()=>import('../components/Detail'))
import Footer from '../components/Footer'
import Home from '../pages/Home'
const ShopCart=lazy(()=>import('../components/ShopCart'))
const Add=lazy(()=>import('../pages/Add'))
const Mine=lazy(()=>import('../pages/Mine'))
const Order=lazy(()=>import('../pages/Order'))
const Shop=lazy(()=>import('../pages/Shop'))



const RouterConfig=()=>{
    return(
        <Suspense fallback={null}>
            <Routes>
            <Route path='/' element={<Navigate to='/home' replace={true}/>}></Route>
            <Route path="/home" element={<Home/>}/>
            <Route path="/shop" element={<Shop/>}/>
            <Route path="/add" element={<Add/>}/>
            <Route path="/mine" element={<Mine/>}/>
            <Route path="/order" element={<Order/>}/>
            <Route path="/detail" element={<Detail/>}/>
            <Route path="/shopcart" element={<ShopCart/>}/>
            </Routes>
            <Footer/>
        </Suspense>
    )
}

export default RouterConfig

这里使用了 React 提供的 Suspense 和 Lazy 实现了动态路由

这里讲一下为啥用动态路由:

按需加载的作用:主要可以减少首页请求的文件的大小。当我们做的项目够大时,一个首当其冲的问题就是所需加载的 JavaScript 的大小。程序应当只加载当前渲染页所需的 JavaScript。在用户浏览过程中按需加载,这样可以提高首屏加载效率

数据流管理Redux

路由已经配置好了,接下来该是页面组件的编写了,但在页面编写之前,我们应该进行数据流的管理,这样才能使页面组件注入灵魂。

在这里对redux的工作原理就不多说了,建议看技术胖的redux(注意跟上车速)。在本项目中我把reducer进行了拆分,每个页面都有自己独立管理的state的reducer函数,然后合并为一个reducer。

  • store/reducer.js代码如下
import { combineReducers } from "redux";

import { reducer as FoundReducer } from '../pages/Home/Found/store'
import { reducer as ShopinfoReducer } from "../pages/Shop/store";
import { reducer as CardListReducer } from "../pages/Home/City/store";
import { reducer as SearchReducer } from '../components/Search/store';
import { reducer as ShopcartReducer } from '../components/ShopCart/store'
import { reducer as CartstoreReducer } from '../components/cartstore/store'

export default combineReducers({
    Found: FoundReducer,
    Shop: ShopinfoReducer,
    Card: CardListReducer,
    Search: SearchReducer,
    Shopcart: ShopcartReducer,
    Cartstore: CartstoreReducer
})

然后在store中创建一个reducer仓库进行数据的派发已经对数据操作,每当我们在store上dispatch一个action,store上的数据就会进行发生改变,并进行数据的重新派发。

  • store/index.js
import { applyMiddleware, createStore, compose } from 'redux'
import reducer from './reducer'
import thunk from 'redux-thunk'
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
const store = createStore(reducer, composeEnhancers(applyMiddleware(thunk)))
    // 比如在Dispatch一个Action之后,到达reducer之前,进行一些额外的操作,就需要用到middleware(中间件)


export default store

redux-thunk 可以实现redux处理异步actionapplyMiddleware就是增强了原始createStore返回的dispatch的功能。

const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;作用是让Redux DevTools生效,方便开发人员查看数据流。

总的store创建好了,接下来以购物车页面的store为例进行讲解使用方法。

结构为:

shopcart/
      store/   
         actionCreators.js
         constants.js
         index.js
         reducer.js
  • shopcart/store/reducer.js

该文件的创建用来为shopcart这个组件进行服务,reducer会接收到两个参数,一个为之前的状态(state),另一个为动作对象(action)

import * as actionTypes from './constants'

const defaultState = {
    shopCart: []
}

export default (state = defaultState, action) => {
    switch (action.type) {
        case actionTypes.CHANGE_CARTSTORE:
            return {
                ...state,
                shopCart: [...state.shopCart, action.data]
            }
        case actionTypes.CHANGE_ISSHOW:
            return {
                ...state,
                shopCart: [...action.data]
            }
        default:
            return state
    }
}
  • shopcart/store/actionCreators.js

当界面调用这个文件导出的接口时,dispatch就会修改对应的数据,对页面进行输出展示

import * as actionTypes from './constants'

const changecartstore = (data) => ({
    type: actionTypes.CHANGE_CARTSTORE,
    data
})

const changeisshow = (data) => ({
    type: actionTypes.CHANGE_ISSHOW,
    data
})

export const getcartstoreList = (data) => {
    return (dispatch) => {
        dispatch(changecartstore(data))
    }
}

export const getisshowList = (data) => {
    return (dispatch) => {
        dispatch(changeisshow(data))
    }
}

Redux仓库创建好后,要使数据流向我们的界面,就需要用到一个工具了,这时候react-redux就出来了。

react-redux提供了两个重要的对象,ProviderconnectProvider使 store通过props传递给子组件,不管层级、(相对于redux,省去了store.subscribe)。connect方法 通过mapStateToProps获取store的对应值,通过mapDispatchToProps ,改写store的值

import { Provider } from 'react-redux'
import store from './store'


ReactDOM.createRoot(document.getElementById('root')).render(
  <Provider store={store}>
     <BrowserRouter>
    <App />
  </BrowserRouter>
  </Provider>
 
)

然后在Provider包裹的组件中使用connect,这样才能使数据流入到页面。

const mapStateToProps=(state)=>{
  return{
    show:state.Shopcart.isShow,
   shopCart:state.Cartstore.shopCart,
   quanselect:state.Shopcart.quanselect
  }
}
const mapDispatchToProps=(dispatch)=>{
  return{
    getisshowDispatch(data){
      dispatch(getisshowList(data))
    },
    getquanselectDispatch(data){
      dispatch(getquanselect(data))
    }

  }
}

export default connect(mapStateToProps,mapDispatchToProps)(React.memo(GouCart))

到这。我们的数据就算是流通了,也就可以在页面操作数据了,数据拿到了,就开始写页面吧。

页面编写

商品详情页面开发

详情页.gif 当在项目首页点击需要查看的商品时,进行路由跳转,跳转到对应商品的页面,那是怎么拿到对应数据的呢?在这里我采用通过路由传参的方式,把一个商品独一无二的id通过路由传过去,在进行接口的请求。把id相等的数据过滤出来进行页面的渲染。

主要的部分代码

    const {shopnum,detail}=props
    const [detaill,setDetaill]=useState([])   
    const [visible,setVisible]=useState(false)
    const [search] = useSearchParams()
    const detailid= search.get('id') || ''
     
       
useEffect(()=>{
    let detaildataa= detail.filter((item)=>
    detailid ==  item.id
     )
     setDetaill(detaildataa)
    
},[detail])

在这里有一个底部选择商品弹出层,点击加入购物车,弹出层弹出,选择商品的类型,点击确认,购物车就有相应的数据,当购物车没有商品时,购物车的数字不显示,有数据底部的购物车的数字发生相应的变化。

主要实现代码

const  addCart=(dataa)=> {
let data=dataa[0]
if(cartstore.every(item=>item.id!==data.id)){
  let list={
    id:data.id,
    images:data.img[0],
    num:1,
    price:data.price,
    select:false,
    title:data.title,
    store:data.store,
    quanselect:false
  }
getaddstoreDispatch(list)
}
        successToast()       
     }

购物车页面

购物车.gif 选择后的商品进入到购物车中,购物车可以勾选是否购买,选择购买的商品可以进行数量的增加,底部价格自动变化。还能对商品进行删除操作,勾选需要删除的商品,点击删除。

购物车的部分操作函数

const removecart=()=>{
  success()
  shopCart.forEach((a,index)=>{
    if(a.id==item.id){
      shopCart.splice(index,1)
    }
  })
  getisshowDispatch(shopCart)

 }
const reduceNum=()=>{
  
  shopCart.map((a,index)=>{
if(a.id==item.id){
  if(a.num==1){
    a.num==1
  }else{
    a.num--
  }
}
  })
  getisshowDispatch(shopCart)
   
   
}
const addNum=()=>{
   shopCart.map((a,index)=>{
      if(a.id==item.id){
        a.num++
      }
    })
    getisshowDispatch(shopCart)
}
const isSelect=()=>{
  shopCart.forEach(a=>{
    if(a.id==item.id){
      a.select=!a.select
    }
  })
  getisshowDispatch(shopCart)
  if(shopCart.every(a=>a.select==true)){
    getquanselectDispatch(true)
  }else{
    getquanselectDispatch(false)
  }
  

}
const handleSelect=()=>{
  getquanselectDispatch(!quanselect)
  if(quanselect){
    shopCart.map(a=>{
      a.select=false
    })
    getisshowDispatch(shopCart)
  }else{
    shopCart.map(a=>{
      a.select=true
    })
    getisshowDispatch(shopCart)
  }
 
}

主要是先在页面组件进行判断找到需要操作的数据,然后dispatch一个action对整个购物车数据进行更新,然后页面就可以获得新的数据进行页面的渲染。

短视频数据查看页面

短视频.gif 这个页面的下拉刷新以及加载状态,安利到了一波神三元大佬的scoll以及loading组件,超级好用,可以看看他写的掘金小册(真的细节)

import React, { useEffect ,useState} from 'react'
import './index.css'
import Loading from '../../../components/Loading'
import CardList from '../../../components/CardList'
import {Tabs} from 'antd-mobile'
import { connect } from 'react-redux'
import { getFoundList } from './store/actionCreators'



 function Found(props) {
  let tablist=['推荐','视频','直播','美食','学习','体育','旅行','职场','科技数码','摄影','音乐','舞蹈','汽车']
  const {listData,show,getFoundlistDispatch}=props
  
  useEffect(()=>{
    getFoundlistDispatch()
},[])
  return (
   
    show ?<Loading/>:
    <Tabs defaultActiveKey='0' className='tab'>
{
  tablist.map((item,index)=>(
<Tabs.Tab title={item} key={index} className='tab-s'>
      <CardList list={listData} key={index}></CardList>
    </Tabs.Tab>
  )
  )   
}     
 </Tabs>
  )
}
const mapStateToProps=(state)=>{
  return{
    listData:state.Found.dataList,
    show:state.Found.show
  }
}
const mapDispatchToProps=(dispatch)=>{
  return {
    getFoundlistDispatch(){
      dispatch(getFoundList())
    }
  }
}

export default connect(mapStateToProps,mapDispatchToProps)(Found)

此处的下拉刷新,当下拉刷新时对请求的数据进行截取,每次截取四个进行刷新替换。当数据长度到达一定值,不在加载数据,弹出提示框。以及此处的Lazyload实现图片懒加载功能,当图片在可见视野之内时才开始加载图片

样式组件 styled-components

styled-components是对react写的一套css in js 框架,也就是说在js中写css,相当与(sass、less),这能加快我们网页的开发速度。

当然他的用法也比较简单,直接在页面中以组件的方式引入就好了。

  • shopcart/index.jsx
import {ShopWrapper} from './style'
return (
    <ShopWrapper></ShopWrapper>
)
  • shopcart/style.js
import styled from "styled-components";

export const ShopWrapper = styled.div `
.....
`

项目的优化部分

路由懒加载

在路由的配置中,我们使用了React的lazySuspense组合实现路由懒加载效果,可以避免一些初始不必要的加载,优化首屏的加载效率

在React中使用lazy后,需要使用到Suspense,Suspense组件有一个属性为fallback,在其包裹的组件为渲染出来之前,会调用fallback函数,可以包含多个懒加载的组件

onst ShopCart=lazy(()=>import('../components/ShopCart'))
const Add=lazy(()=>import('../pages/Add'))
const Mine=lazy(()=>import('../pages/Mine'))
const Order=lazy(()=>import('../pages/Order'))
const Shop=lazy(()=>import('../pages/Shop'))



const RouterConfig=()=>{
    return(
        <Suspense fallback={null}>
            <Routes>
            <Route path='/' element={<Navigate to='/home' replace={true}/>}></Route>
            <Route path="/home" element={<Home/>}/>
            <Route path="/shop" element={<Shop/>}/>
            <Route path="/add" element={<Add/>}/>
            <Route path="/mine" element={<Mine/>}/>
            <Route path="/order" element={<Order/>}/>
            <Route path="/detail" element={<Detail/>}/>
            <Route path="/shopcart" element={<ShopCart/>}/>
            </Routes>
            <Footer/>
        </Suspense>
    )
}

页面渲染优化Memo

组件性能优化,主要用于子组件当中当我们的父组件数据复杂,多项改变状态的地方,当父组件的改变,没有影响到子组件(props未变,没有props), 组件外面都加memo后就会使用上一次加载的数据,不会进行更新。以此通过记忆组件渲染的方式来提升性能的表现

也就是说使用React.memo之后可以避免不必要组件跟随页面更新而重新进行渲染

import memo from 'React';

const shopcart=()=>{}

export default memo(ShopCart)

图片懒加载

引用react-lazyload实现图片懒加载效果

import LazyLoad from 'React-lazyload';
 <LazyLoad 
        placeholder={<img width="100%" 
        height="100%" src={jiazai}/>}>
          <img 
              width="100%" 
              height="100%"
              src={item.images + "?param=300x300"}/>
 </LazyLoad>

总结

这个小项目也是对自己学习react的一个总结吧,功能还有不全的地方,以及一些页面的布局也有不完善的地方。但对于我来说也是从理论到实践的一大步。我还会持续更新,你要是觉的对你有帮助就帮忙点点赞吧!

源码