这个夏天,用仿小红书实战带你玩转 React + Redux

3,020 阅读10分钟

我正在参加「创意开发 投稿大赛」详情请看:掘金创意开发大赛来了!

前言

13257.jpg.png

  前段时间使用 React 框架及其他技术 简单地仿了一下小红书首页,然后经过近期对 Redux、React 的进一步学习后,我将项目的数据使用 Redux 进行了统一管理,对首页进行了优化和部分修改,加入了更好的用户体验,并新增了商城界面,让此项目的业务逻辑更加丰满。

  在这篇文章中,我将会介绍 Redux 管理数据 以及页面的优化,并且分享在项目中遇到的问题及解决方法。如果对上个仿首页项目感兴趣的朋友可以看这篇文章 —— 手摸手教你:入门级React仿小红书首页 。看完上篇再来看这篇,将会更好地发现我在最初的基础上做了哪些修改以及优化。项目源码附在最后,注意查收。

技术栈简介

react(react-router)全家桶:用于构建用户的MVVM框架
redux:状态管理容器
redux-thunk:处理异步逻辑的 redux 中间件
react-lazyload: react 懒加载库
axios: 用来请求后端api的数据
styled-components: 处理样式,体现css in js的前端工程化
antd-mobile:来自阿里的开源组件库
fastmock:免费的后端数据接口

  此外,为了更好地将 Redux 产生的效果可视化,可以在浏览器中安装插件:Redux DevTools。然后就可以在浏览器的 Redux 界面看到每次的仓库状态以及效果。

项目实现

效果预览

首页展示

首页 (1).gif

商城一览

商城2.gif

搜索实现

搜索.gif

项目结构

    src/
       api/                //网络请求代码和相关配置
       assets/             //静态文件
            font/          //图标文件
       components/         //可复用的UI组件
            common/        //通用组件
            Footer/        //底部组件
       pages/              //页面
            Home/          //首页组件
            Search/        //搜索组件
            Shop/          //商城组件
       routes/             //路由配置文件
       store/              //redux 仓库
       utils/              //工具类函数
    App.jsx                //根组件
    main.jsx               //入口文件

路由配置

  使用 react-router 对路由进行配置,并放到单独文件夹中,使其更好管理。在配置中,采用 懒加载 Lazy 实现路由懒加载效果,可以优化路由加载效率,减少一些初始化的加载过程。在实现懒加载时,需要结合 Suspense 组件 一起使用。代码如下:

import React,{ lazy,Suspense }from 'react'
import { Route,Routes } from 'react-router-dom'
import Home from '../pages/Home'
const Mine = lazy(() => import('../pages/Mine'))
const Message = lazy(() => import('../pages/Message'))
const Shop = lazy(() => import('../pages/Shop'))
const Choose = lazy(() => import('../pages/Choose'))
const Search = lazy(() => import('../pages/Search'))
const RouteConfigs = () => {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
      <Routes>
        <Route path="/" element={<Home/>}></Route>
        <Route path="/home" element={<Home/>}></Route>
        <Route path="/mine" element={<Mine/>}></Route>
        <Route path="/message" element={<Message/>}></Route>
        <Route path="/shop" element={<Shop/>}></Route>
        <Route path='/choose' element={<Choose />}></Route>
        <Route path='/search' element={<Search />}></Route>
      </Routes>
      </Suspense>
    </div>
  )
}
export default RouteConfigs

封装数据请求

  使用 fastmock 模拟数据,在原先 api 文件夹中只有 request.js 文件的基础上新增了 config.js 文件,好处是对数据请求进行封装,让代码页面简洁,使其可读性更高。

  config.js:

import axios from 'axios'
export const baseUrl =  
"https://www.fastmock.site/mock/33e7fec4e60b54344eaa2c59a55b379d/red_book/red_book";
const axiosInstance = axios.create({
    baseURL: baseUrl
})
axiosInstance.interceptors.response.use(
    res => res.data,
    err => {
      console.log(err, '网络错误~~')
    }
  )
export { axiosInstance }

  request.js:

import { axiosInstance } from "./config";
export const getListRequest = () => axiosInstance.get('/list')  
export const getFoodRequest = () => axiosInstance.get('/food')  
export const getShopRequest = () => axiosInstance.get('/shop')  
export const getSportRequest = () => axiosInstance.get('/sport')  
export const getSearchRequest = () => axiosInstance.get('/search')  

Redux 配置

  Redux 主要由三部分组成:store,reducer,action。store 和 action 都是对象,reducer 是函数。store 的作用是将 action 和 reducer 联系起来并改变 state。在项目复杂,数据繁多的情况下可以使用 Redux 来进行数据管理。

创建主仓库 store

  主仓库用于管理各个分仓库的数据。storeRedux 中最重要的部分,所有的数据都在 store 中管理,所以可以优先编写 store

  index.js

import { createStore, compose, applyMiddleware } 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)
        )
)
export default store

  reducer.js

import { combineReducers } from "redux";
import { reducer as recommendReducer } from '../pages/Shop/Recommend/store/index'
import { reducer as sportlistReducer } from "../pages/Shop/Sport/store/index";
import { reducer as likelistReducer } from "../pages/Home/Like/store/index";
import { reducer as foodlistReducer } from "../pages/Home/Food/store/index";
import { reducer as searchlistReducer } from "../pages/Search/store/index";
export default combineReducers({     // 引入并合并分仓库
    recommend: recommendReducer,
    sportlist: sportlistReducer,
    likelist: likelistReducer,
    foodlist: foodlistReducer,
    searchlist: searchlistReducer
})

  注意,还需要在入口文件 main.jsx 中加入 Provider 声明式开发

  main.jsx

import {Provider} from 'react-redux'
import store from './store'
ReactDOM.createRoot(document.getElementById('root')).render(
  <Provider store={store}>
      <App />
  </Provider>
)

创建分仓库 store

  在需要的页面下创建分仓库,来管理每个页面的状态和数据,下面以 Search 搜索页面为例,来展示 Search 分仓库的建立。

  actionCreators.js  管理相应函数来更新数据状态

import * as actionTypes from './constants'
import { getSearchRequest } from '../../../api/request'
export const changeSearch = (data) => ({
    type: actionTypes.CHANGE_SEARCH,
    data
  })
export const changeEnterLoading = (data) => ({
    type: actionTypes.ENTER_LOADING,
    data
  })
export const getSearch= () => {
    return (dispatch) => {
      getSearchRequest().then(data => {
        dispatch(changeSearch(data))
        dispatch(changeEnterLoading(false))
      })
    }
  }

  reducer.js  操作数据

import * as actionTypes from './constants'
const defaultState = {
  search: [],
  enterLoading:false
}
const reducer = (state=defaultState, action) => {
    switch(action.type) {
      case actionTypes.CHANGE_SEARCH:
        return {
          ...state,
          search: action.data
        }
      case actionTypes.ENTER_LOADING:
        return {
          ...state,
          enterLoading: action.data,
        }
      default:
        return state;
    }
  }
export default reducer

  index.js  向外输出 actionCreators、reducer 和 constants

import reducer from './reducer'
import * as actionCreators from './actionCreators'
import * as constants from './constants';
export { 
    reducer, 
    actionCreators,
    constants 
}

  constants.js  配置文件给 type 取别名

export const CHANGE_SEARCH = 'CHANGE_SEARCH'
export const ENTER_LOADING = 'ENTER_LOADING'

页面连接仓库

  该仓库是为 Search 搜索页面服务的,因此需将 Searchindex.jsx 与仓库连接,代码如下:

  index.jsx

import { connect } from "react-redux";
import { getSearch,changeEnterLoading } from "./store/actionCreators";
....
const mapStateToProps = (state) => {
  return {
    search: state.searchlist.search,
    enterLoading:state.searchlist.enterLoading
  }
}
const mapDispatchToProps = (dispatch) => {
  return {
    getSearchDispatch(query) {
      dispatch(getSearch(query))
    },
    changeEnterLoadingDispatch(data) {
      dispatch(changeEnterLoading(data))
    }
  }
}
export default connect(mapStateToProps,mapDispatchToProps)(React.memo(Search))

  然后在需要的页面做同样的操作,项目的 Redux 配置就这样搭建完毕。启动项目后打开 Redux 便可以清晰的看到所有数据。

image.png

页面的变化

  这次的页面在布局方式、用户体验以及业务逻辑等方面做出了相应改变,比如瀑布流布局、增加了点赞收藏等,并实现了之前没有实现的其他细节,比如吸顶式导航。

瀑布流布局

image.png

  页面一改最初的布局风格,将其换成更具特色的瀑布流布局。瀑布流不是什么狂拽酷炫吊炸天的特效,而是一种 限宽不限高 的页面布局方式。它能让界面看起来更加美观清爽。看起来很高级,实现起来却很简单,只需在样式中加入以下几行代码:

.container{     // 父容器
    column-count: 2;
    column-gap: 10px;
    .list{     // 子容器
      width: 100%;
      break-inside: avoid;
    }

  注意,整个项目采取的是 styled-components 处理样式,所以此处样式的格式会和传统的格式看起来有些许不一样。

  实现瀑布流式布局的关键点在于 column-count 属性,使用 column-count 属性将一个盒子分为多列展示数据。

下拉刷新

下拉.gif

  下拉刷新使用的是 antd-mobile 中的 PullToRefresh 组件,可以自定义下拉或者下拉后的显示内容。在页面中加入下拉刷新,可以增加用户体验。

import { PullToRefresh } from 'antd-mobile'
import { sleep } from 'antd-mobile/es/utils/sleep'
const XXX = (props) =>{
  async function doRefresh() {
    await sleep(1000);
  }
    return(
      <PullToRefresh
      onRefresh={doRefresh}
      canReleaseText={<h4>用力拉</h4>}
      completeText={ <h4>好啦</h4>}
      refreshingText={<h4>玩命加载中</h4>}>     
      <ListWrapper>
      ......
      </ListWrapper>
      </PullToRefresh>
    )
 }

  关于它的更多用法可以参考 antd-mobile官方文档 ,里面会介绍地更加详细。

点赞收藏

点赞.gif

  看过上篇项目文章的朋友都知道,原来的首页中未能实现点赞收藏,因此页面十分单调。这次将点赞收藏加入首页,让页面的业务逻辑更加丰富,并给用户带来更好的交互感。具体实现如下:

import React,{useState} from 'react'
import classnames from 'classnames'
const ListThree=({source}) =>{
  const [isLike, SetIsLike] = useState(false)     // 定义状态
  const changeLike = () => {     // 设置状态
      SetIsLike(!isLike)
  }
  return (
    <div>
       <div className="list">
        ......
            <i className={classnames("iconfont",
                  {"icon-aixin1": !isLike},
                  {"icon-aixin2": isLike},
                  {"active": isLike}
                )}  onClick={()=>changeLike()}>
            </i>
        ......
    </div>
  )
}
export default ListThree

  定义状态,设置状态取反函数,用 classname 动态类名控制最初样式。当点击图标时触发取反函数,让图标发生改变。

图片懒加载

  图片懒加载的加入,能让首屏的加载速度提升。从远程请求过来的图片资源在需要的时候加载出来,不需要的时候使用占位图片,进而优化用户体验。具体实现如下:

import LazyLoad from 'react-lazyload'
import red from '../../assets/red.jpg'
const ListOne=({info}) =>{
  return (
    <div>
       <div className="list">
           <LazyLoad
              placeholder={<img width="100%" 
              height="100%" src={red} className="list-img"/>}>
            <img src={info.img} className="list-img" />
           </LazyLoad>
            ......
        </div>
    </div>
  )
}
export default ListOne

其他细节

  在其他方面,我将 css 样式部分进行了微调,美化了页面,并保留了页面上原有的细节。将导航栏实现吸顶,让页面在往下滑动的过程中使其一直保持在上方。

新增的商城页面

  这次新增了商城页面模块,整体设计和布局与首页一样,都采用了瀑布流式布局、下拉刷新、懒加载等。在商城页面中加入了一个弹出框,Grid 布局 以及购物车。

弹出框以及 Grid 布局

弹出框.gif

  弹出框使用的是 antd-mobile 中的 Popup 组件,从屏幕中弹出或滑出一块自定义内容区。可以用 position 指定弹出的位置,bodyStyle 设置内容区域样式。实现如下:

import { Popup } from "antd-mobile";
const Shop = () =>{
    const [visible2, setVisible2] = useState(false)
    <Popup 
        visible={visible2}
        onMaskClick={() => { setVisible2(false) }}
        position='top'
        bodyStyle={{ height: '215px' }}>
        ......
    </Popup>
}
export default Shop

  弹出框里面的内容采用强大的 Grid 网格布局grid-template-columns 属性声明每一列宽度。grid-template-rows 属性声明每一行的高度。grid-gap 属性声明行间距和列间距。repeat()函数 用来简化重复的值。实现如下:

 .grid{
    padding: 15px;
    display: grid;
    grid-template-columns: repeat(4, 75px);   
    grid-gap: 15px;
    grid-template-rows: 60px 60px;
    }

购物车

  在商城页面上有个购物车小图标,在页面滑动的过程中始终保持不动,同吸顶导航栏原理一样,添加如下代码即可:

    position: fixed;
    bottom: 80px;
    right: 30px;
    z-index: 9999;

  比较遗憾的是未能实现购物车的相关功能。但是后面会继续将此项目继续完善,完成购物车功能。

搜索功能

  虽然未能实现购物车,但是实现了搜索功能。这次页面中最大的改变就是通过 Redux 完成了简单的模糊搜索功能。

  搜索分为两部分,由 Search 组件和 SearchBox 组件完成,并在搜索上加入了防抖函数,具体实现如下:

export const debounce = (func, delay) => {
    let timer;
    return function (...args) {
        if(timer) {
        clearTimeout(timer);
        }
        timer = setTimeout(() => {
        func.apply(this, args);
        clearTimeout(timer);
        }, delay);
    };
  };

  用户输入数据后,对数据进行防抖处理,然后将输入的值传入 Search 组件,Search 组件根据传过来的值 dispatch 并对返回的搜索结果进行渲染。在项目中的数据里,有 title 关键字和 content 内容两种标签,当不论触发那种标签时,都可以显示出结果列表。如下所示:

 search.filter
 (item => item.title.indexOf(query) != -1||item.content.indexOf(query)!=-1)

  具体实现方式及代码可以通过文章最后的项目源码地址查看。

性能优化

懒加载

  懒加载简单来说就是延迟加载或按需加载,加上了它,就会有加载速度上的优势,能带来更好的用户体验。

路由懒加载
结合 lazySuspense 实现懒加载,具体实现可以看前面的路由配置部分。

图片懒加载
可以减少 http 的请求,在用户需要的时候加载出图片,提高页面加载速度,具体实现可以看前面的图片懒加载部分。

memo

  memo 的作用是可以实现减少渲染重复未变数据。如果你的组件在相同 props 的情况下渲染相同的结果,那么你可以通过将其包装在 React.memo 中调用,以此通过记忆组件渲染结果的方式来提高组件的性能表现。

总结

  以上就是我对这整个项目的分享了。在这个项目中,我借鉴了来自 神三元大佬loading 等组件以及 debounce 防抖函数。本项目后期还会继续优化和完善相应功能,如果觉得还不错的话,欢迎点赞收藏和评论。另外,如果有什么问题或者建议欢迎在评论区讨论。这个夏天,让我们一起动起来 o( ̄▽ ̄)

  项目源码地址:red_book