(13)首页开发——⑤ “加载更多”功能实现 | React.js 项目实战:PC 端“简书”开发
转载请注明出处,未经同意,不可修改文章内容。
🔥🔥🔥“前端一万小时”两大明星专栏——“从零基础到轻松就业”、“前端面试刷题”,已于本月大改版,合二为一,干货满满,欢迎点击公众号菜单栏各模块了解。
1 需求
❓查看“简书”官网,并实现需求:当点击“加载更多”时,会异步加载出更多的“数据”并显示在页面上。
✔️需求分析——又是发送 AJAX 获取“数据”的流程,其可总结为:
- 首先,这里需要绑定一个 click 点击事件;
- 然后,派发一个 action;
- 借助 Redux-thunk 这个“中间件”在 action 里边去写异步的操作;
- 请求到“数据”后,再派发一个“同步”的 action;
- 当 reducer 接收到这个“同步”的 action 后,就去改变它的“数据”;
- “数据”变了,页面也就跟着发生了变化。
2 mock “加载更多”数据
1️⃣在项目的 public 目录下 api
文件夹中新增一个 homeList.json
文件:
2️⃣编写 mock 数据 homeList.json
中的内容:
{
"success": true,
"data": {
"moreArticleList": [{
"id": 4,
"title": "作为骨科医生,我必须告诉所有女性这个真相!",
"desc": "“女性进化的程度远比男性更高”,这是近年来坊间广为流传的说法。随着文明和社会的发展,伟大的女性们通过自强不息的奋斗,证明着她们理应同男性一样被平…",
"imgUrl": "https://qdywxs.github.io/jianshu-images/article-img04.jpg",
"author": "骨科医生路遥",
"discuss": 31,
"love": 2732,
"money": 3
},{
"id": 5,
"title": "认真做早餐就是向生活致敬",
"desc": "上周在下厨房美食 APP 看到第二届早餐马拉松活动,立即报名参加,活动口号是“坚持 21 天早餐打卡,养成自律习惯”,不禁莞尔一笑,早在去年我就参加两次…",
"imgUrl": "https://qdywxs.github.io/jianshu-images/article-img05.jpg",
"author": "冷月花魂烘焙",
"discuss": 31,
"love": 2519,
"money": 12
},{
"id": 6,
"title": "从120斤到95斤的经验分享",
"desc": "文章分为五个部分: 1.写在前面的话 2.120斤与95斤 3.减肥方法分析 4.饮食与运动(干货) 5.写在最后 一、写在前面的话 我一直觉得…",
"imgUrl": "https://qdywxs.github.io/jianshu-images/article-img06.jpg",
"author": "路阳啊",
"discuss": 21,
"love": 6554,
"money": 3
},{
"id": 7,
"title": "阅读对一个人最深广的魅力",
"desc": "大约在钱小能上一年级的时候,因他的班主任吴老师推荐,我读到这么一本书——《朗读手册》。这是一个叫吉姆·崔利斯的美国人写的,他是著名的阅读研究专家…",
"imgUrl": "https://qdywxs.github.io/jianshu-images/article-img07.jpg",
"author": "梅拾璎",
"discuss": 395,
"love": 3030,
"money": 3
},{
"id": 8,
"title": "人物画:怎么在一个月内从零基础到入门",
"desc": "先放一张最近的画,不然你说我骗你。 我是零基础,完全零基础,特别零基础。一直非常非常喜欢画画,可是从来没正经上过课跟过老师学,原因很简单——穷!…",
"imgUrl": "https://qdywxs.github.io/jianshu-images/article-img08.jpg",
"author": "9号大头菜",
"discuss": 21,
"love": 8370,
"money": 16
}]
}
}
3 编写“加载更多”逻辑代码
1️⃣打开 home 目录下 components 文件夹中 Content.js
文件:
import React, {Component} from "react";
import {Link} from "react-router-dom";
import {
Item,
Cover,
Details,
Title,
Foot,
LoadMore
} from "../style";
import { connect } from "react-redux";
import {actionCreators} from "../store"; // ❗️引入 actionCreators!
class Content extends Component {
render() {
return(
<div>
{
this.props.articleList.map((item) => {
return (
<Item key={item.get("id")}>
<Cover>
<Link to="/detail"><img src={item.get("imgUrl")} alt="" /></Link>
</Cover>
<Details>
<Link to="/detail">
<Title>
{item.get("title")}
</Title>
</Link>
<p>
{item.get("desc")}
</p>
<Foot>
<Link to="/"><span className="username">{item.get("author")}</span></Link>
<span className="iconfont icon-comment"></span><span>{item.get("discuss")}</span>
<span className="iconfont icon-heart"></span><span>{item.get("love")}</span>
<span className="iconfont icon-money"></span><span>{item.get("money")}</span>
</Foot>
</Details>
</Item>
)
})
}
<LoadMore
onClick={this.props.getMoreList}
> {/*
1️⃣-④:给 LoadMore 样式组件绑定一个“事件 onClick”,❓可这个
“事件”应该怎样被调用呢?
*/}
{/*
1️⃣-⑥:因此可以通过 this.props.getMoreList 来调用 store
的 getMoreList;
*/}
加载更多
</LoadMore>
</div>
)
}
}
const mapStateToProps = (state) => {
return {
articleList: state.getIn(["home", "articleList"])
}
}
/*
❗️❗️❗️1️⃣-②:接下来,我们定义哪些“用户的操作”
应该当作 action,并传给 store;
*/
const mapDispatchToProps = (dispatch) => { /*
1️⃣-③:把 store 里的“dispatch 方法”
作为“参数”传递给 mapDispatchToProps;
*/
return {
getMoreList() { /*
1️⃣-⑤:在这里定义用户的“onClick 操作”会被当作 action
传给 store;
*/
/*
1️⃣-⑦:Redux-thunk 中,“异步”代码我们是放在 action 中进行。
这里我们仅作方法的“调用”;
*/
const action = actionCreators.getMoreList();
dispatch(action)
}
}
}
/*
❗️1️⃣-①:给 connect 传递第 3 个参数——mapDispatchToProps。
mapDispatchToPropos 直译为:我们把 store 的 dispatch 方法“挂载”到
Content 组件的 props 上。
即,我们可以定义哪些“用户的操作”应该当作 action,并传给 store!
*/
export default connect(mapStateToProps, mapDispatchToProps)(Content);
1️⃣-⑧:打开 home 目录下 store 中的 actionCreators.js
文件,定义这个 action;
import axios from "axios";
import {INIT_HOME_DATA} from "./actionTypes";
import {fromJS} from "immutable";
const initHomeData = (result) => ({
type: INIT_HOME_DATA,
labelList: fromJS(result.labelList),
articleList: fromJS(result.articleList),
panelsList: fromJS(result.panelsList)
});
export const getHomeInfo = () => {
return(dispatch) => {
axios.get("/api/homeData.json")
.then((res) => {
const result = res.data.data;
const action = initHomeData(result);
dispatch(action);
})
.catch(() => {alert("error")})
}
}
// 1️⃣-⑨:在 action 中添加 AJAX“异步”代码;
export const getMoreList = () => {
return(dispatch) => {
axios.get("/api/homeList.json")
.then((res) => {
const result = res.data.data;
})
.catch(() => {alert("error")})
}
}
返回页面,查看“数据”是否成功获取到(已成功获取):
2️⃣既然“数据”已成功获取,接下来就用 AJAX 获取到的“数据”来丰富首页的文章列表(每点击一次“加载更多”,都在已有列表的基础上再加上 AJAX 获取到的“数据”)。又是“修改数据”的套路,那我们继续走 Redux 的工作流程:
2️⃣-①:打开 home 目录下 store 中的 actionTypes.js
文件;
export const INIT_HOME_DATA = "init_home_data";
// ❗️定义好常量~
export const ADD_HOME_DATA ="add_home_data";
2️⃣-②:返回 home 目录下 store 中的 actionCreators.js
文件;
import axios from "axios";
// 2️⃣-③:引入常量;
import {INIT_HOME_DATA, ADD_HOME_DATA} from "./actionTypes";
import {fromJS} from "immutable";
const initHomeData = (result) => ({
type: INIT_HOME_DATA,
labelList: fromJS(result.labelList),
articleList: fromJS(result.articleList),
panelsList: fromJS(result.panelsList)
});
export const getHomeInfo = () => {
return(dispatch) => {
axios.get("/api/homeData.json")
.then((res) => {
const result = res.data.data;
const action = initHomeData(result);
dispatch(action);
})
.catch(() => {alert("error")})
}
}
// 2️⃣-⑤:在这里定义 action;
const addHomeData = (result) => ({
type: ADD_HOME_DATA,
/*
❗️❗️❗️2️⃣-⑥:这里请一定注意,这里的 data 是从“接口”获取到的“数组”对象,
它是一个“JS 对象”。
同理,这里也应该将 list 转换为“immutable 对象”!
*/
moreArticleList: fromJS(result.moreArticleList)
})
export const getMoreList = () => {
return(dispatch) => {
axios.get("/api/homeList.json")
.then((res) => {
const result = res.data.data;
// 2️⃣-④:获取到“数据”后,往 reduce 里的 articleList 里加内容;
const action = addHomeData(result);
dispatch(action); // ❗️2️⃣-⑦:将这个 action 发送给 reducer!
})
.catch(() => {alert("error")})
}
}
3️⃣打开 home 目录下 store 中的 reducer.js
文件:
🔗前置知识:
《JavaScript 基础——JS 数组:① ES3 数组方法》——熟悉 concat()
“合成数组”的用法!
import {fromJS} from "immutable";
// 3️⃣-①:先引入“常量”;
import {INIT_HOME_DATA, ADD_HOME_DATA} from "./actionTypes";
const defaultState = fromJS({
labelList: [],
articleList: [],
panelsList: []
})
export default (state=defaultState, action) => {
if(action.type === INIT_HOME_DATA) {
return state.merge({
labelList: action.labelList,
articleList: action.articleList,
panelsList: action.panelsList
})
}
// 3️⃣-②:编写“增加”数据的逻辑!
if(action.type === ADD_HOME_DATA) {
return state.set("articleList", state.get("articleList").concat(action.moreArticleList))
}
return state;
}
❗️❗️❗️当然,由于我们是自己 mock 的数据,目的是为了跑通程序,此时此刻你去页面检查时,肯定会涉及到 key 值的报错——因为当你第二次点击“加载更多”时,我们 mock 的数据是死的且有限的,用 id 值作为的 key 值就会开始重复了!故,你可以去 Content.js
文件中将 key 值用 index 临时表示。
打开 home 目录下 components 文件夹中的 Content.js
文件:
import React, {Component} from "react";
import {Link} from "react-router-dom";
import {
Item,
Cover,
Details,
Title,
Foot,
LoadMore
} from "../style";
import { connect } from "react-redux";
import {actionCreators} from "../store";
class Content extends Component {
render() {
return(
<div>
{
this.props.articleList.map((item, index) => { // ❗️❗️❗️用 index 临时作为 key 值!
return (
<Item key={index}> {/* ❗️用 index 临时作为 key 值! */}
<Cover>
<Link to="/detail"><img src={item.get("imgUrl")} alt="" /></Link>
</Cover>
<Details>
<Link to="/detail">
<Title>
{item.get("title")}
</Title>
</Link>
<p>
{item.get("desc")}
</p>
<Foot>
<Link to="/"><span className="username">{item.get("author")}</span></Link>
<span className="iconfont icon-comment"></span><span>{item.get("discuss")}</span>
<span className="iconfont icon-heart"></span><span>{item.get("love")}</span>
<span className="iconfont icon-money"></span><span>{item.get("money")}</span>
</Foot>
</Details>
</Item>
)
})
}
<LoadMore
onClick={this.props.getMoreList}
>
加载更多
</LoadMore>
</div>
)
}
}
const mapStateToProps = (state) => {
return {
articleList: state.getIn(["home", "articleList"])
}
}
const mapDispatchToProps = (dispatch) => {
return {
getMoreList() {
const action = actionCreators.getMoreList();
dispatch(action)
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Content);
❓为什么是“临时表示”?
因为实际项目中,当点击“加载更多”时,会涉及到“分页”。即,你每点击一次,后端返回给你的“数据”都是不同“页”的。既然都不同“页”了,那 id 作为“唯一”的 key 值肯定就不会重复,也就不会报错。
先返回页面查看下效果:
4 怎么做数据的“分页”
1️⃣打开 home 目录下 store 中的 reducer.js
文件:
import {fromJS} from "immutable";
import {INIT_HOME_DATA, ADD_HOME_DATA} from "./actionTypes";
const defaultState = fromJS({
labelList: [],
articleList: [],
panelsList: [],
// ❗️定义一个新的“数据项”articlePage,使其初识化为 1。
articlePage: 1
})
export default (state=defaultState, action) => {
if(action.type === INIT_HOME_DATA) {
return state.merge({
labelList: action.labelList,
articleList: action.articleList,
panelsList: action.panelsList
})
}
if(action.type === ADD_HOME_DATA) {
return state.set("articleList", state.get("articleList").concat(action.moreArticleList))
}
return state;
}
2️⃣回到 home 目录下 components 文件夹中的 Content.js
文件:
import React, {Component} from "react";
import {Link} from "react-router-dom";
import {
Item,
Cover,
Details,
Title,
Foot,
LoadMore
} from "../style";
import { connect } from "react-redux";
import {actionCreators} from "../store"; // ❗️引入 actionCreators!
class Content extends Component {
render() {
return(
<div>
{
this.props.articleList.map((item, index) => {
return (
<Item key={index}>
<Cover>
<Link to="/detail"><img src={item.get("imgUrl")} alt="" /></Link>
</Cover>
<Details>
<Link to="/detail">
<Title>
{item.get("title")}
</Title>
</Link>
<p>
{item.get("desc")}
</p>
<Foot>
<Link to="/"><span className="username">{item.get("author")}</span></Link>
<span className="iconfont icon-comment"></span><span>{item.get("discuss")}</span>
<span className="iconfont icon-heart"></span><span>{item.get("love")}</span>
<span className="iconfont icon-money"></span><span>{item.get("money")}</span>
</Foot>
</Details>
</Item>
)
})
}
{/*
❗️2️⃣-②:当“点击”时,我们可以通过“箭头函数”的形式将“页码”传递
给 getMoreList!
先注释掉下面这几行代码,我们需要重新改写~
<LoadMore
onClick={this.props.getMoreList}
>
加载更多
</LoadMore>
*/}
<LoadMore
onClick={() => this.props.getMoreList(this.props.page)}
>
加载更多
</LoadMore>
</div>
)
}
}
const mapStateToProps = (state) => {
return {
articleList: state.getIn(["home", "articleList"]),
// ❗️2️⃣-①:在这里拿到目前是第几页;
page: state.getIn(["home", "articlePage"])
}
}
const mapDispatchToProps = (dispatch) => {
return {
// 2️⃣-③:相应地,getMoreList 就可以接收到这个 page;
getMoreList(page) {
const action = actionCreators.getMoreList(page); /*
2️⃣-④:同时,会将 page
actionCreators;
*/
dispatch(action)
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Content);
2️⃣-⑤:打开 home 目录下 store 中的 actionCreators.js
文件;
import axios from "axios";
import {INIT_HOME_DATA, ADD_HOME_DATA} from "./actionTypes";
import {fromJS} from "immutable";
const initHomeData = (result) => ({
type: INIT_HOME_DATA,
labelList: fromJS(result.labelList),
articleList: fromJS(result.articleList),
panelsList: fromJS(result.panelsList)
});
export const getHomeInfo = () => {
return(dispatch) => {
axios.get("/api/homeData.json")
.then((res) => {
const result = res.data.data;
const action = initHomeData(result);
dispatch(action);
})
.catch(() => {alert("error")})
}
}
const addHomeData = (result, nextPage) => ({ // 2️⃣-⑨:这里可以接收到这个 nextPage;
type: ADD_HOME_DATA,
moreArticleList: fromJS(result.moreArticleList),
// ❗️❗️❗️2️⃣-⑩:继而,这里就可以将 nextPage 传递给 reducer;
nextPage
})
export const getMoreList = (page) => { // ❗️2️⃣-⑥:相应地,getMoreList 就会拿到这个 page;
return(dispatch) => {
/*
❗️2️⃣-⑦:在请求后端接口时,我们就可以给“接口”带一个“参数”进去,使其等于传递过来的 page!
先注释掉下面这行代码,我们要给“接口”带“参数”~
axios.get("/api/homeList.json")
*/
axios.get("/api/homeList.json?page=" + page)
.then((res) => {
const result = res.data.data;
const action = addHomeData(result, page + 1); /*
❗️❗️❗️2️⃣-⑧:当我们派发 addHomeData
这个 action 时,我们不仅可以把获取到的“数据”
result 传递给它,我们还可以将“下一页”page + 1
也传递给它!
*/
dispatch(action);
})
.catch(() => {alert("error")})
}
}
2️⃣-⑪:接着打开 home 目录下 store 中的 reducer.js
文件;
import {fromJS} from "immutable";
import {INIT_HOME_DATA, ADD_HOME_DATA} from "./actionTypes";
const defaultState = fromJS({
labelList: [],
articleList: [],
panelsList: [],
articlePage: 1
})
export default (state=defaultState, action) => {
if(action.type === INIT_HOME_DATA) {
return state.merge({
labelList: action.labelList,
articleList: action.articleList,
panelsList: action.panelsList
})
}
if(action.type === ADD_HOME_DATA) {
/*
❗️2️⃣-⑫:替换初的 articlePage!
先注释掉下面这行代码,我们需要用 state.merge() 来改写~
return state.set("articleList", state.get("articleList").concat(action.moreArticleList))
*/
return state.merge({
"articleList": state.get("articleList").concat(action.moreArticleList),
"articlePage": action.nextPage // ❗️2️⃣-⑬:这行代码写完,我们的“页码”就自动加了 1!
})
}
return state;
}
返回页面控制台查看(一切按要求显示,无报错、无警告):
下一篇,我们完成“首页”最后一个功能的开发——返回顶部。
祝好,qdywxs ♥ you!
转载自:https://juejin.cn/post/7350605277517512745