React hooks——前后端交互处理分页

6,431 阅读13分钟

秋招正当时,笔者想着提高一波自己的React实战能力。恰好最近读到了神三元在掘金的React Hooks 与Immutable 数据流实战。于是,在这个夏天,听着神三元云音悦的歌曲,借着这份小册,使用react+hooks简单仿造了一下极客时间的app。

我认为,对于一个优秀的前端工程狮来说,不仅仅要熟练前端,同样的,后端的流程也需要了解。这样日后在和后端对接时也会十分流畅,这里我推荐使用 mocker-api 实现模拟数据,方便和后端进行对接。

前言

在本小节中需要完成以下功能,

  1. 下拉刷新,
  2. 上拉加载更多。
  3. 数据分页返回的处理。

我们可以直接使用神三元项目中封装好的better-scroll。

先上成果图

预览地址

部分项目结构

|-- pages // 页面文件夹
|   |-- forum // 讲堂页面文件夹
|   |   |-- course // 课程页面
|   |   |   |-- Forum.css
|   |   |   |-- Forum.jsx
|   |   |   |-- style.js
|   |   |   |-- store // 课程页面 store
|   |   |       |-- actionCreators.js // action 文件
|   |   |       |-- constants.js // 常量定义文件
|   |   |       |-- index.js // store 入口文件
|   |   |       |-- reducer.js // reducer 文件

理解项目结构, 是学习React架构思想的第一步,也是最重要的一步。在我的项目中, pages 文件夹存放着各个页面的主文件夹。这里我们主要是看 forum 即讲堂这一部分内容的实现。通过阅读神三元的项目,我理解到可以分页面建立store,使得数据访问更加简洁,store 设计思路更加清楚。

前端代码

讲堂页面分析

  • forum.jsx

思路分析: forum.jsx 文件中引入封装好的 better-scroll(可以直接使用神三元封装好的better-scroll代码) 监听页面下拉和上拉事件。pulldown函数,监听下拉动作,可以实现下拉刷新。pullup函数,监听上拉动作,可以实现上拉加载。handlePullUp 函数 和 handlePullDown 函数通过props传递给公共组件 Scroll。

  1. 当浏览器监听到对应事件比如说 pulldown 下拉动作时 就会调用函数 handlePullDown, 函数handlePullDown又会调用从props解构出来的pullDownRefresh(), 该函数会dispatch 两个action,一个是changeListOffset(0),一个是getInfoList()。changeListOffset(0) 用于将 store里面的listOffset设置为0,getInfoList()用于获取详情列表数据。

代码如下:

import React, { useEffect, memo } from 'react';
import { connect } from 'react-redux';
import * as actionTypes from './store/actionCreators';
import ForumList from '../../../components/ForumList/ForumList';
import Loading from '../../../common/loading/Loading';
import Scroll from '../../../common/scroll/Scroll';
import { ListContainer } from './style';
import { forceCheck } from 'react-lazyload';
import './Forum.css';
import { renderRoutes } from 'react-router-config';

function Forum(props) {
    const { route, pathList, directionList, infoList, enterLoading, pageCount, pullUpLoading, pullDownLoading } = props;
    const { getForumListDataDispatch, getInfoListDataDispatch, pullUpRefresh, pullDownRefresh } = props;
    
    const handlePullUp = () => {
        pullUpRefresh();
    };

    const handlePullDown = () => {
        pullDownRefresh();
    };

    useEffect(() => {
        // 如果没有数据 请求一次
        if (!pathList.toJS().length || !directionList.toJS().length) {
            getForumListDataDispatch();
        }
        if (!infoList.toJS().length) {
            getInfoListDataDispatch();
        }
    }, [getForumListDataDispatch, getInfoListDataDispatch, pathList, directionList, infoList])

    return (
        <div className="forum">
            <ListContainer>
                <Scroll
                    onScroll={forceCheck}
                    pullUp={handlePullUp}
                    pullDown={handlePullDown}
                    pullUpLoading={pullUpLoading}
                    pullDownLoading={pullDownLoading}
                // pulldown监听下拉动作可以实现下拉刷新;
                // pullup监听上拉动作可以实现上拉加载;
                >
                    <div>
                        <div className="forum-xw">
                            <ForumList infoList={infoList} />
                        </div>
                    </div>
                </Scroll>
            </ListContainer>
            <Loading Loading={enterLoading} title="正在加载中..." />
            {/* 重新 render routes 一次 */}
            {renderRoutes(route.routes)}
        </div>)
}

const mapStateToProps = (state) => ({
    pathList: state.forum.pathList,
    directionList: state.forum.directionList,
    infoList: state.forum.infoList,
    enterLoading: state.forum.enterLoading,
    pageCount: state.forum.pageCount,
    pullUpLoading: state.forum.pullUpLoading,
    pullDownLoading: state.forum.pullDownLoading 
})

const mapDispatchToProps = (dispatch) => {
    return {

        getForumListDataDispatch() {
            dispatch(actionTypes.getPathList())
            dispatch(actionTypes.getDirectionList())
        },

        getInfoListDataDispatch() {
            dispatch(actionTypes.getInfoList())
        },

        // 滑到最底部刷新部分的处理
        pullUpRefresh() {
            dispatch(actionTypes.changePullUpLoading(true));
            dispatch(actionTypes.refreshMoreInfoList());
        },
        
        //顶部下拉刷新
        pullDownRefresh() {
            dispatch(actionTypes.changePullDownLoading(true));
            dispatch(actionTypes.changeListOffset(0));
            dispatch(actionTypes.getInfoList());
        }
    }
}
export default connect(mapStateToProps, mapDispatchToProps)(memo(Forum))

store 的设计

  • constants.js 文件
export const CHANGE_INFOS = 'CHANGE_INFOS';
export const CHANGE_LIST_OFFSET = 'home/singers/CHANGE_LIST_OFFSET'; // 数据分页常量
export const CHANGE_ENTER_LOADING = 'CHANGE_ENTER_LOADING';
export const CHANGE_PULLUP_LOADING = 'home/singers/PULLUP_LOADING'; // 上拉常量
export const CHANGE_PULLDOWN_LOADING = 'home/singers/PULLDOWN_LOADING'; // 下拉常量

constants.js 文件负责对常量的定义以及导出。

  • reducer.js
import * as actionTypes from './constants';

const defaultState = {
    // 一个页面只有一个 loading 值
    // 全部课程数据
    infoList:[],
    enterLoading:true,     // 加载中
    pullUpLoading: false// 上拉加载
    pullDownLoading: false// 下拉刷新
    listOffset: 0// 请求列表偏移是个数
}

export default (state = defaultState, action) => {
    switch(action.type) {
        case actionTypes.CHANGE_INFOS:
            return { ...state, infoList: action.data}
        case actionTypes.CHANGE_ENTER_LOADING:
            return { ...state, enterLoading: action.data}
        case actionTypes.CHANGE_LIST_OFFSET:
            return { ...state, listOffset: action.data}
        case actionTypes.CHANGE_PULLUP_LOADING:
            return { ...state, pullUpLoading: action.data}
        case actionTypes.CHANGE_PULLDOWN_LOADING:
            return { ...state, pullDownLoading: action.data}
        default:
            return state
    }
}

reducer 纯函数 返回状态及接受状态的更新 只有一个状态与之相对应。

  • actionCreators.js
// 负责进行数据请求
import * as actionTypes from './constants';
// 请求接口文件
import { getInfoListRequest} from '../../../../api/request';

export const changeListOffset = (data) => ({
    type: actionTypes.CHANGE_LIST_OFFSET,
    data
  });

// 进场 loading
export const changeEnterLoading = (data) => ({
    type: actionTypes.CHANGE_ENTER_LOADING,
    data
})

//滑动最底部loading
export const changePullUpLoading = (data) => ({
    type: actionTypes.CHANGE_PULLUP_LOADING,
    data
  });
  
//顶部下拉刷新loading
export const changePullDownLoading = (data) => ({
    type: actionTypes.CHANGE_PULLDOWN_LOADING,
    data
  });

export const changeInfoList = (data) => ({
    type: actionTypes.CHANGE_INFOS,
    data
})

// 获取详情列表数据
export const getInfoList = () => {
    return ( dispatch, getState) => {
        // 获取原store中的偏移量
        const offset = getState().forum.listOffset;

        // 偏移量传进接口请求中
        getInfoListRequest(offset).then(res=> {
            const data = res.infos
            dispatch(changeInfoList(data));
            // 拿到数据  EnterLoading 变成false
            dispatch(changeEnterLoading(false));
            dispatch(changePullDownLoading(false));
            dispatch(changeListOffset(data.length));
        })
        // 如果拿到错误的数据
        .catch(() => {
            console.log('详情列表数据传输错误')
        })
    }
}

// 加载更多数据
export const refreshMoreInfoList = () => {
    return ( dispatch, getState ) => {
     // 获取 原来的 listOffset 和 infoList 数据
        const offset = getState().forum.listOffset;
        const infos = getState().forum.infoList;
        getInfoListRequest(offset).then(res => {
            // 让当前的 infos 和新请求的 infos 使用拓展运算符展开并拼接成一个新的数据;
            const data = [...infos, ...res.infos]
            dispatch(changeInfoList(data));
            dispatch(changePullUpLoading(false));
            dispatch(changeListOffset(data.length));
        })
        .catch(() => {
            console.log('详情列表数据传输错误')
        })
    }
}

思路分析:

当获取详情列表数据,即调用getInfoList时,我们需要先获取原store 中的偏移量并发出请求,成功拿到数据后,我们将下拉刷新设为false,并且将listoffset偏移量设置为接收到数据的长度。

当我们需要加载更多数据时,我们需要使用 getState 先获取原有 的偏移量listOffset和infoList 数据, 得到数据之后,我们将老数据和新数据进行拼接得到一个新的data,然后再changeInfoList(data)修改store的数据,使我们之前的数据不会丢失。并且将listoffset设置为新的data的长度,保证数据分页正确。 我们来到store里面的getInfoList

export const getInfoList = () => {
    return ( dispatch, getState) => {
        // 获取原store中的偏移量
        const offset = getState().forum.listOffset;

        // 偏移量传进接口请求中
        getInfoListRequest(offset).then(res=> {
            const data = res.infos
            dispatch(changeInfoList(data));
            // 拿到数据  EnterLoading 变成false
            dispatch(changeEnterLoading(false));
            dispatch(changePullDownLoading(false));
            dispatch(changeListOffset(data.length));
        })
        // 如果拿到错误的数据
        .catch(() => {
            console.log('详情列表数据传输错误')
        })
    }
}

这个函数return 一个函数,里面接受两个参数 dispatch 和 getState,dispatch 用于修改store里面的内容,getState用于获取store里面的内容,我们先从 store 里面获取listOffset,然后将 offset 作为参数传进getInfoListRequest中,该函数会拿到结果,然后再修改store里面的数据,并将EnterLoading变成false,PullDownLoading变为false 修改store 里面的listoffset 为 当前获取数据的长度。

请求接口文件

  • config.js
import axios from 'axios';
// 推荐使用 axios 兼容性更好

export const baseUrl = "http://localhost/data"// 全局的后端 api 前缀

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 getInfoListRequest = count => {
    return axiosInstance.get(`/infos?offset=${count}`);
}

后端代码

const infos = require('./data/infos.json');

module.exports = {
    // 分页功能数据
    'GET /data/infos': (req, res) => {
        // 获取传进来的 参数offset
        const { offset = 0 } = req.query;
        // 使用 Number 转化为 Number 型
        const data = infos.slice(Number(offset) , Number(offset)  + 10);
        res.json({
            "infos":data});
    }
}

用户滑动屏幕,触发相应事件,传入的offset,可以通过req.query解构出来,如果没有传入则默认是0,我们可以根据这个 offset 对数组进行切割处理。然后返回相应的数据。

入口文件 index.js

const express = require('express');
const path = require('path');
const apiMocker = require('mocker-api');

const app = express();

apiMocker(app, path.resolve('./mocker/mocker.js'))
app.listen(8080);

以上就是本小节的主要内容,个人技术不佳,项目还有缺陷,欢迎掘友指正,我会好好改进,下面是项目源码。Github React-geek-time 项目源码

笔者正在准备秋招,欢迎各位大佬一起讨论,一起加油!