秋招正当时,笔者想着提高一波自己的React实战能力。恰好最近读到了神三元在掘金的React Hooks 与Immutable 数据流实战。于是,在这个夏天,听着神三元云音悦的歌曲,借着这份小册,使用react+hooks简单仿造了一下极客时间的app。
我认为,对于一个优秀的前端工程狮来说,不仅仅要熟练前端,同样的,后端的流程也需要了解。这样日后在和后端对接时也会十分流畅,这里我推荐使用 mocker-api 实现模拟数据,方便和后端进行对接。
前言
在本小节中需要完成以下功能,
- 下拉刷新,
- 上拉加载更多。
- 数据分页返回的处理。
我们可以直接使用神三元项目中封装好的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。
- 当浏览器监听到对应事件比如说 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 项目源码。
笔者正在准备秋招,欢迎各位大佬一起讨论,一起加油!