移动端实现无限制下拉刷新和触底加载

2,033 阅读5分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第10天,点击查看活动详情

大家都知道移动端的项目,下拉刷新,触底加载更多是必备的,而且大多是通过组件库实现的,今天我们使用antd-mobile里面的组件实现这两个功能,把一整套的流程都跑通,虽然组件库可能用的不一样,但基本的流程思路都是一样的,掌握了这个组件库,其他的也都是差不多的!

触底加载更多

这里我们使用使用antd-mobile的InfiniteScroll组件,详细的可以去查阅官网

基本格式

{/* 无限加载组件 */}
<InfiniteScroll loadMore={loadMore} hasMore={hasMore} />

加载原理

  • 如果hasMore是true,当InfiniteScroll出现在可视区,或者它在距离视口一段距离(默认250px)时,就去调用loadMore
  • loadMore必须是一个返回promise的函数

image.png 核心逻辑

import { getArticleList, getNewArticles } from '@/store/actions/article'
import { RootState } from '@/types/store'
//导入InfiniteScroll组件
import { InfiniteScroll} from 'antd-mobile'
import { useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import ArticleItem from '../ArticleItem'
import styles from './index.module.scss'

//定义接收参数的类型
type Props = {
id:string
}
const ArticleList = ({ id }:Props) => {
const dispatch = useDispatch()

//获取article文章数据
const article = useSelector((state:RootState) => state.article)

//加了?就是可选链,有的话就在article[Number(id)]?上取值,没有的话值就是undefined,undefined转化布尔类型是false
const articleList = article[Number(id)]?.articles || []
//因为要获取不同的数据,所以需要传入不同的时间戳
const timestamp = article[Number(id)]?.timestamp || Date.now()

//当InfiniteScroll组件出现在视口,loadMore函数就会被触发,要求返回的必须是一个promise的函数
const loadMore = async () => {
//在loadMore函数里发请求获取新的数据
await dispatch(getArticleList(Number(id), timestamp || Date.now()))
}
return (
<div className={styles.root}>
  {/* 文章列表中的每一项 */}
  {articleList.map((item, index) => (
 <div key={index} className="article-item">
 //文章组件
  <ArticleItem item={item} />
 </div>
  ))}
  <InfiniteScroll loadMore={loadMore} hasMore={articleList.length < 50} />
</div>
)
}
export default ArticleList

这里hasMore={articleList.length < 50}是停止下拉加载的条件,当数组的长度小于50,也就是说最多五十个数据,五十个数据后就不能在下滑加载了。

一般停止下滑加载的条件我们需要和后端约定

  • 可以是数组的长度到达多后停止加载
  • 也可以是后端返回给我们的数组为空时停止加载
  • 或者时间戳为null都可以,具体的情况我们可以和后端约定

action里的代码

export const getArticleList = (channel_id:number,  timestamp:number):RootThunkAction => {
return async (dispatch) => {
const res = await request.get<ApiResponse<{pre_timestamp:number, results:Article[]}>>('/articles', { params: { channel_id, timestamp } })
dispatch({
  type: 'article/saveChannelArticles',
  //按照后端要求的数据类型,把返回值存入到reducer中
  payload: {
    channel_id,
    timestamp: res.data.data.pre_timestamp,
    articles: res.data.data.results
  }
})
}
}

因为频道有很多个,当我点击娱乐模块时获取娱乐模块的数据,点击历史模块时获取历史模块的数据,可是当我在从历史模块点到娱乐模块,并且没有往下滑动,此时我还需要重新发一次请求获取数据吗? 答案肯定是不需要

所以在reducer里做判断

//导入定义的文章数据类型
import { Article } from '@/types/data'
//导入定义的action类型
import { ArticleActon } from '@/types/store'

//定义state的数据类型
type State = {

// Chanenls已经是一个数组
//因为这里的当作属性名,并且也是不断变化的,所以通过动态属性取值
[channel_id:number]:{
//时间搓类型
 timestamp:number,
 //我们需要渲染的数据是一个Article类型的数组
 articles:Article[]
}
}
这里直接断言初始值时是State类型,可以避免在里面写很多空数据
const initialState = {} as State

const article = (state = initialState, action:ArticleActon):State => {

if (action.type === 'article/saveChannelArticles') {

//从action传过来的数据中结构出需要的数据
const { channel_id, timestamp, articles } = action.payload

//在旧数据里通过channel_id属性名取出值
const oldObj = state[channel_id]

//如果oldObj有值,说明这个频道已经获取过值,后面获取的只用加到后面即可
if (oldObj) {
//把旧值的时间戳更新成穿过来的时间戳
  oldObj.timestamp = timestamp
  
  //已经或去过一次,直接push到数组的最后面即可
  oldObj.articles.push(...articles)
  
  // 因为channel_id也是参数,所以不能直接写channel_id,要用中括号取值[channel_id]
  //return 出加了新数据的数组,页面会同步更新
  return { ...state, [channel_id]: { ...oldObj } }
} else {

  oldObj没有值,说明第一次点击这个频道,我们直接return 穿过来的数据即可
  return { ...state, [channel_id]: { timestamp, articles } }
}
return state
}
export default article

这里有一个重要点:

在react中是浅比较
[channel_id]: oldObj 和{ ...oldObj }是不一样的

[channel_id]: oldObj 直接把这个对象赋值给他,对象的地址没有发生变化,不会触发视图的更新

{ ...oldObj }把oldObj放入新对象里展开,对象的地址发生了变化,react才会更新视图数据

所以我们一般在reducer里返回state时,如果state是一个数组,我们会把state在数组里展开,后面放上新增加的值[...state,新的值],如果是对象就在对象里展开state,这样才能触发视图的更新

无线下拉刷新

下拉刷新使用的是antd-mobile组件库里的PullToRefresh组件

image.png

这个组件的使用方式是PullToRefresh包裹整个的内容区域,当下拉刷新是会触发onRefresh函数

<PullToRefresh onRefresh={onRefresh}>
 内容区域
</PullToRefresh>

核心代码

//导入对应action函数
import {  getNewArticles } from '@/store/actions/article'

//导入PullToRefresh组件
import { InfiniteScroll, PullToRefresh } from 'antd-mobile'

//当下拉刷新时触发onRefresh函数,要求返回的也是一个promise函数
 const onRefresh = async () => {
 
 //因为下拉刷新要求获取的是最新的数据,所以需要我们传现在的时间戳
await dispatch(getNewArticles(Number(id), Date.now()))
}

return (
<div className={styles.root}>
  {/* 文章列表中的每一项 */}
  
  //包裹内容区域
  <PullToRefresh onRefresh={onRefresh}>
  {articleList.map((item, index) => (
 <div key={index} className="article-item">
  <ArticleItem item={item} />
 </div>
  ))}
  </PullToRefresh>
  
  <InfiniteScroll loadMore={loadMore} hasMore={articleList.length < 50} />
</div>
 )

在action里定义请求函数

//这里和触底加载的逻辑一样,只不过要传入当前的时间戳
export const getNewArticles = (channel_id:number, timestamp:number):RootThunkAction => {
return async (dispatch) => {
const res = await request.get<ApiResponse<{pre_timestamp:number, results:Article[]}>>('/articles', { params: { channel_id, timestamp } })
dispatch({
  type: 'article/getNewArticles',
  payload: {
    channel_id,
    timestamp: res.data.data.pre_timestamp,
    articles: res.data.data.results
  }
})
}
}

reducer里处理数据

//当action.type === 'article/getNewArticles'时,处理下拉刷新数据
else if (action.type === 'article/getNewArticles') {

const { channel_id, timestamp, articles } = action.payload

//获取到旧的数据,防止获取失败给个空数组
const oldArticle = state[action.payload.channel_id]?.articles || []

//直接把action里传来新的数据加到旧的数据前,return出去就行
return { ...state, [channel_id]: { timestamp, articles: [...articles, ...oldArticle] } }
}

因为下拉刷新需要一直获取最新的数据,所以只需要把当前最新数据插入到旧数据前面就行,逻辑要比触底加载简单一些