首页开发(3)

实现“加载更多”的换页功能

实现功能: 在List.js中增加“加载更多”按钮点击按钮后换页

实现 点击 加载更多:

  1. 模拟接口数据(public-api-homeList.json)
  2. 定义组件(style.js)
  3. 点击后要发送AJAX请求,AJAX请求是写在action中的(actionCreators.js)
  4. 使用组件绑定点击事件,在事件函数中调用方法创建并派发action(相当于发送AJAX请求)(List.js)
  5. store拿到action后打包action和state传给reducer
  6. reducer拿到action和state后去改变articleList后返回新state(reducer.js)
  7. 页面重新渲染

实现 换页:

  1. 在reducer中新增 articlePage 用于存储当前页数,初始值为1(reducer.js)
  2. 在List.js中每点击一次按钮就使articlePage+1,所以要在调用的函数中传入page
  3. 要使 articlePage+1 改变就要通过action,所以在actionCreators.js中进行设置。
    1. 使AJAX请求的接口地址上使用articlePage,这也就保证了每次请求的地址都是带有不同页数的
    2. 要让articlePage+1就要通过action改变store中数据
  4. reducer其实还是返回homeList.json中的模拟数据,只是方便了后端。

实现 加载更多数据

定义 组件LoadMore

在style.js中**定义 组件LoadMore**:

1
2
3
4
5
6
7
8
9
10
export const LoadMore = styled.div`
width: 100%;
line-height: 40px;
margin: 30px 0;
background: #a5a5a5;
text-align: center;
border-radius: 20px;
color: #fff;
cursor: pointer;
`;

模拟接口数据

在public-api下新建homeList.json文件用于模拟接口数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"sucess": true,
"data": [
{
"id": 4,
"title": "疫情就要结束了!",
"desc": "因为一个人,人民摘掉口罩的愿望可能成为了泡影,学生返校,工人正常复工的时间可能又要推迟。一个人的不重视,就可能会导致全国人民努力白费。我不知道为...",
"imgUrl": "https://i0.wp.com/ww1.sinaimg.cn/large/005H7IVsgy1gcuniztyfhj30dw09tai6.jpg"
},
{
"id": 5,
"title": "在家长厨艺",
"desc": "今年,大家都过了一个 长长长长长长的假期 每天朋友圈里 除了有关疫情的新闻 就是晒自己做的“黑暗料理” 在这个没有了外卖的日子里 蛋糕、奶茶、火...",
"imgUrl": "https://i0.wp.com/ww1.sinaimg.cn/large/005H7IVsgy1gcunif28rwj30rs0iitzs.jpg"
},
{
"id": 6,
"title": "大家要坚持戴口罩!",
"desc": "疫情对普通人的影响,很大程度上表现为一场魔幻式的恐慌。从朋友圈疯传的段子就可以看出,最搞笑的是某宝平台上的兽用双黄莲也被抢购一空。虽然我国制度优...",
"imgUrl": "https://i0.wp.com/ww1.sinaimg.cn/large/005H7IVsgy1gcunhi0u4qj30ib0au79t.jpg"
}
]
}

使用组件,绑定点击事件

List.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import React, { Component, Fragment } from 'react';
import {
ListItem,
ListInfo,
LoadMore,
} from "../style";
import { connect } from "react-redux";
import { actionCreators } from "../store"

class List extends Component {
render() {
const { list, getMoreList } = this.props;
return (
<Fragment>
{
list.map((item, index) => {
return (
<ListItem key={index}>
<img
className="pic"
src={item.get("imgUrl")}
alt={item.get("title")}
/>
<ListInfo>
<h3 className="title">{item.get("title")}</h3>
<p className="desc">{item.get("desc")}</p>
</ListInfo>
</ListItem>
)
})
}
<LoadMore onClick={getMoreList}>加载更多</LoadMore>
</Fragment>
)
}
}

const mapState = (state) => ({
list: state.getIn(["home", "articleList"])
})

const mapDispatch = (dispatch) => ({
getMoreList() {
dispatch(actionCreators.getMoreList());
}
})

export default connect(mapState, mapDispatch)(List);
  1. 在List.js中引入并使用 组件LoadMore,绑定点击事件,点击以后我们是要发送AJAX请求获取数据的,获取到的数据改变state中数据让页面重新渲染
    1. AJAX请求要放到action中,在actionCreators.js中新建**getMoreList()用于创建 发送AJAX请求的action,新建addHomeList()**用于创建 改变store中数据的action
    2. 派发action给store就要在connect方法的参数2(回调函数)中**新建函数getMoreList()**用于派发action
    3. 事件绑定函数中只需**调用函数getMoreList()**即可完成AJAX的发送

创建2个action

actionCreators.js中添加以下代码,创建发送AJAX请求和改变state数据的action:

1
import { fromJS } from "immutable";
1
2
3
4
5
6
7
8
9
10
11
const addHomeList = (list) => ({
type: constants.ADD_HOME_List,
list: fromJS(list)
})
export const getMoreList = () => {
return (dispatch) => {
axios.get("/api/homeList.json").then((res) => {
const result = res.data.data;
dispatch(addHomeList(result));
})
}
  1. 新建**getMoreList()**用于创建 发送AJAX请求的action
  2. 新建**addHomeList()**用于创建 改变store中数据的action
    • 用于改变store中的数据的数据必须是immutable类型的。
    • immutable提供的fromJS()和List()都可以使普通数组转换为immutable数组
      • 但是List只能使数组变immutable,数组元素若为对象,则还是普通对象。fromJS()是将数组元素也immutable化的。

reducer处理articleList

reducer接收到action后,使用concat()将 articleList变成合并了新数据后的数组,返回给state:

1
2
case constants.ADD_HOME_List:
return state.set("articleList", state.get("articleList").concat(action.list))

实现 换页

reducer新增articlePage管理当前页码

  1. 在reducer中给state新增一个默认数据articlePage:1,默认起始页为1,接下来每点击一次“加载更多”就使articlePage+1
    1
    2
    3
    4
    5
    6
    const defaultState = fromJS({
    topicList: [],
    articleList: [],
    recommendList: [],
    articlePage:1,
    });

List中获取articlePage传给action使用

在List.js中获取articlePage:

1
2
3
4
const mapState = (state) => ({
list: state.getIn(["home", "articleList"]),
page: state.getIn(["home", "articlePage"])
})

调用点击事件绑定函数getMoreList()时传入articlePage:

1
const { list, page, getMoreList } = this.props;
1
<LoadMore onClick={() => getMoreList(page)}>加载更多</LoadMore>

getMoreList()拿到page后传给 actionCreators.js中的getMoreList():

1
2
3
4
5
const mapDispatch = (dispatch) => ({
getMoreList(page) {
dispatch(actionCreators.getMoreList(page));
}
})

actionCreators中page+1

actionCreators.js中,在**请求后端接口数据时加上page("/api/homeList.json?page=" + page**)

1
2
3
4
5
6
7
8
export const getMoreList = (page) => {
return (dispatch) => {
axios.get("/api/homeList.json?page=" + page).then((res) => {
const result = res.data.data;
dispatch(addHomeList(result));
})
}
}

可以看到每次点击“加载更多”请求的都是第一页:
结果
把page也通过addHomeList
page+1
传递给store

reducer处理articlePage

action传递了nextPage,reducer就将接收到的nextPage放到state中替换掉articlePage,则每一次点击后请求的接口都带有不同的页数

1
2
3
4
5
case constants.ADD_HOME_List:
return state.merge({
"articleList": state.get("articleList").concat(action.list),
"articlePage": action.nextPage
})

这样一来每次点击请求的页码都不同:
效果

注意
后端拿到不同的页码就可以返回不同的数据了。(但实际上我们每次返回的还是homeList.json中的模拟数据)


实现 返回顶部 功能

  • 在home-index.js中我们实现一个点击以后返回顶部的按钮。

style.js中定义组件BackTop

由于是一直在右下角的,所以该按钮是固定定位的:

1
2
3
4
5
6
7
8
9
10
export const BackTop = styled.div`
position: fixed;
right: 100px;
bottom: 100px;
width: 60px;
line-height: 60px;
text-align: center;
border: 1px solid #ccc;
font-size: 14px;
`;

home-index.js中使用BackTop

home-index.js中使用BackTop
效果

BackTop绑定点击事件

要想让按钮点击回到顶部,就需要给他绑定点击事件

1
<BackTop onClick={this.handleScrollTop}>返回顶部</BackTop>

由于此时和reducer并不关系,只是单纯回到顶部,所以只需要在Home组件中书写函数即可:

1
2
3
handleScrollTop() {
window.scrollTo(0, 0);
}

我们需要借助window.scrollTo()方法把内容滚动到指定的坐标scrollTo(x 坐标,y 坐标)

下滑到一定位置再显示该按钮

实现功能:此时按钮一直显示在页面上,我们要实现下滑到一定位置再显示该按钮

  1. reducer中添加showScroll:false默认不显示按钮reducer.js
  2. home-index.js中拿到数据showScrollhome-index.js
  3. 使用三元运算符根据showScroll决定是否显示按钮三元运算符
  4. 使用addEventListener()监听onScroll事件,元素滚动条在滚动时就触发 changeScrollTopShow():滚动时就触发 changeScrollTopShow()滚动时监听成功
  5. 在 changeScrollTopShow()中根据document.documentElement.scrollTop得到滚动距离得到滚动距离往下滚动由于我们需要通过改变store中数据来决定是否显示按钮,所以函数changeScrollTopShow要放在connect的参数2中。
  6. 在changeScrollTopShow()中设置判断,滚动距离顶部距离大于400才显示,否则隐藏按钮,要修改store中数据就要派发action,所以需要在 actionCreators.js中创建对应actionchangeScrollTopShowactionCreators.js
  7. reducer中处理actionreducer中处理action
  8. 注意:在home组件中给window绑定了一个事件,那么移除home组件时要记得解绑:记得解绑

优化代码

修改state中数据的代码拿出来放在函数里,在switch…case中通过调用函数对state进行修改,精简switch…case的代码:
优化代码


首页性能优化(PureComponent)

  • 我们在首页所有组件都使用connect与store做了链接,这也就导致只要store发生改变,那么每个组件都会被重新渲染,也就是render函数会被重新执行,也就是说不管更新的数据和本组件是否有关系,本组件都会重新渲染,这样就导致性能不高

解决方法

  • 所以可以使用 shouldCompUpdate()优化代码,判断只有和本组件有关的数据更新时,本组件的render函数才执行,否则return false不让组件重新渲染。通过避免虚拟DOM的比对来提高性能
  • 但是手写shouldCompUpdate太过麻烦,所以我们可以使用react提供的PureComponent
  • 前提条件:必须使用immutable管理数据才能使用PureComponent,否则坑很多。
  • 在首页index.js及其内部组件中都使用PureComponent代替Component以Topic为例

路由跳转(react-router-dom的Link组件)

  • 使用react-router-dom的Link组件帮助进行页面跳转,Link组件类似a标签,to属性类似a标签的href属性。
  • 使用Link组件可使页面跳转时不发送另一个http请求,提升加载速度,提高性能。
  • 注意:react是 单页应用的跳转,也就是说,不管怎么进行页面跳转,整个网站只会加载一次html文件,这也就决定了不能使用<a>标签进行页面跳转
    • 如果使用<a>标签进行页面跳转,跳转时会发送HTTP请求。
    • 借助react-router-dom的Link组件实现跳转则不会,因此加载速度会快很多,借此也可提高性能。

首页到详情页

希望点击博客时可跳转到详情页:
博客

在List.js中引入react-router-dom的Link组件

1
import { Link } from "react-router-dom";

使用Link组件
可以看到跳转时并没有发送另一个HTTP请求:
效果


详情页到首页

实现功能:点击Logo从详情页跳转到首页。

在common-header-styles.js中,原本我们的Logo组件使用的是a标签,现在改成div

1
2
3
4
5
6
7
8
9
10
11
export const Logo = styled.div`
position:absolute;
display:block;
top:0;
left:0;
height:56px;
width:100px;
background:url(${LogoPic});
// contain:把x轴拉满,y轴够长的话会平铺重复显示图片
background-size:contain;
`;

在common-header-index.js中引入并使用Link组件包裹Logo组件,使之可跳转首页:

1
import { Link } from "react-router-dom";
1
2
3
<Link to="/">
<Logo />
</Link>

此时会报错Error: Invariant failed: You should not use <Link> outside a <Router>也就是说Link不应该在router的外部
报错
关于Router组件可复习这篇笔记
App.js中可以看到,Header和BrowserRouter组件是并列关系,也就是说,处于Header组件中的Link组件和BrowserRouter组件是并列关系,Link也就在router的外部了:
App.js
解决方法:将Header组件放入BrowserRouter组件内部:
将Header组件放入BrowserRouter组件内部

,