一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第10天,点击查看活动详情。
大家都知道移动端的项目,下拉刷新,触底加载更多是必备的,而且大多是通过组件库实现的,今天我们使用antd-mobile里面的组件实现这两个功能,把一整套的流程都跑通,虽然组件库可能用的不一样,但基本的流程思路都是一样的,掌握了这个组件库,其他的也都是差不多的!
触底加载更多
这里我们使用使用antd-mobile的InfiniteScroll组件,详细的可以去查阅官网
基本格式
{/* 无限加载组件 */}
<InfiniteScroll loadMore={loadMore} hasMore={hasMore} />
加载原理
- 如果hasMore是true,当InfiniteScroll出现在可视区,或者它在距离视口一段距离(默认250px)时,就去调用loadMore
- loadMore必须是一个返回promise的函数
核心逻辑
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组件
这个组件的使用方式是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] } }
}
因为下拉刷新需要一直获取最新的数据,所以只需要把当前最新数据插入到旧数据前面就行,逻辑要比触底加载简单一些