likes
comments
collection
share

(13)首页开发——⑤ “加载更多”功能实现 | React.js 项目实战:PC 端“简书”开发

作者站长头像
站长
· 阅读数 10
转载请注明出处,未经同意,不可修改文章内容。

🔥🔥🔥“前端一万小时”两大明星专栏——“从零基础到轻松就业”、“前端面试刷题”,已于本月大改版,合二为一,干货满满,欢迎点击公众号菜单栏各模块了解。

1 需求

❓查看“简书”官网,并实现需求:当点击“加载更多”时,会异步加载出更多的“数据”并显示在页面上。 (13)首页开发——⑤ “加载更多”功能实现 | React.js 项目实战:PC 端“简书”开发

✔️需求分析——又是发送 AJAX 获取“数据”的流程,其可总结为:

  1. 首先,这里需要绑定一个 click 点击事件;
  2. 然后,派发一个 action;
  3. 借助 Redux-thunk 这个“中间件”在 action 里边去写异步的操作;
  4. 请求到“数据”后,再派发一个“同步”的 action;
  5. 当 reducer 接收到这个“同步”的 action 后,就去改变它的“数据”;
  6. “数据”变了,页面也就跟着发生了变化。

2 mock “加载更多”数据

1️⃣在项目的 public 目录下 api 文件夹中新增一个 homeList.json 文件: (13)首页开发——⑤ “加载更多”功能实现 | React.js 项目实战:PC 端“简书”开发

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">&#xe602;</span><span>{item.get("discuss")}</span>
                    <span className="iconfont icon-heart">&#xe8f4;</span><span>{item.get("love")}</span>
                    <span className="iconfont icon-money">&#xe607;</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")})
  }
}

返回页面,查看“数据”是否成功获取到(已成功获取): (13)首页开发——⑤ “加载更多”功能实现 | React.js 项目实战:PC 端“简书”开发

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">&#xe602;</span><span>{item.get("discuss")}</span>
                    <span className="iconfont icon-heart">&#xe8f4;</span><span>{item.get("love")}</span>
                    <span className="iconfont icon-money">&#xe607;</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 值肯定就不会重复,也就不会报错。

先返回页面查看下效果: (13)首页开发——⑤ “加载更多”功能实现 | React.js 项目实战:PC 端“简书”开发

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">&#xe602;</span><span>{item.get("discuss")}</span>
                    <span className="iconfont icon-heart">&#xe8f4;</span><span>{item.get("love")}</span>
                    <span className="iconfont icon-money">&#xe607;</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;
}

返回页面控制台查看(一切按要求显示,无报错、无警告): (13)首页开发——⑤ “加载更多”功能实现 | React.js 项目实战:PC 端“简书”开发

下一篇,我们完成“首页”最后一个功能的开发——返回顶部。

祝好,qdywxs ♥ you!

转载自:https://juejin.cn/post/7350605277517512745
评论
请登录