项目中使用到分页的场景很多,例如:后管系统中的表格分页、移动端列表页上拉加载更多。这些场景使用一些分页加载的组件可以实现,但是还是有痛点,每次都得写一套大差不差的逻辑。因此,在此文中我们使用hooks来封装一下通用的逻辑,简化使用。
Vue中composition API
自定义composition API的封装
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
| import { reactive } from '@vue/composition-api'; import { debounce } from 'lodash';
const usePager = (fetch, { pageSize = 20, } = {}) => { const data = reactive({ pageIndex: 1, pageSize: pageSize, total: 0, });
const refresh = debounce(async () => { const { pageIndex, pageSize } = data; const { total, pageIndex: newPageIndex } = await fetch({ pageIndex, pageSize }); data.total = total; newPageIndex && (data.pageIndex = newPageIndex); }, 50); const handleSizeChange = (pageSize) => { data.pageIndex = 1; data.pageSize = pageSize; refresh(); }; const handleCurrentChange = (pageIndex) => { data.pageIndex = pageIndex; refresh(); }; const reset = () => { handleSizeChange(pageSize); };
refresh(); return [data, { handleSizeChange, handleCurrentChange, reset, }]; };
export default usePager;
|
分页组件的封装,这里基于element。此处没有将usePager和组件写在一起,因为如果换用了UI组件,我们希望分页的逻辑还是可以复用的。
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
| <template> <el-pagination :current-page="pageInfo.pageIndex" :page-sizes="pageSizes" :page-size="pageInfo.pageSize" :total="pageInfo.total" :layout="layout" v-bind="$attrs" @size-change="handleSizeChange" @current-change="handleCurrentChange" /> </template>
<script> import usePager from './usePager';
export default { props: { pageSizes: { type: Array, default: () => [20, 30, 40, 50] }, layout: { type: String, default: 'total, sizes, prev, pager, next, jumper' }, fetch: { type: Function, required: true, } }, setup(props) { const { fetch } = props; // 此处未使用到reset const [ pageInfo, { handleSizeChange, handleCurrentChange }] = usePager(fetch);
return { pageInfo, handleSizeChange, handleCurrentChange }; }, }; </script>
|
组件的使用,这里仅展示分页相关的内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| <template> <div> <custom-pager :fetch="fetch" /> </div> </template>
<script> export default { setup() { const tableData = ref([]); // 自己实现fetch,返回total即可 const fetch = async ({ pageSize, pageIndex }) => { const { data } = await getData({ pageSize, pageIndex }); tableData.value = data.list; return { total: tableData.total }; }; return { tableData, fetch }; }, }; </script>
|
React中hooks
分页自定义hooks的实现
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 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84
| import { useState, useEffect, useRef } from 'react' import { useImmer } from 'use-immer'
const initialState = { loading: false, hasMore: true, pageIndex: 1, pageSize: 10 }
export default (props) => { const { fetch, auto = true, pageSize = 10 } = props
const [page, setPage] = useImmer({ ...initialState, pageSize }) const totalRef = useRef(0)
const [hasNext, setHasNext] = useState(true)
useEffect(() => { if (auto) { handleFetch() } }, [page.pageIndex])
const handleFetch = async () => { setPage((v) => { v.loading = true }) const { total } = await fetch(page) totalRef.current = total setPage((v) => { if (!total || total <= page.pageSize * page.pageIndex) { v.hasMore = false } else { v.hasMore = true } v.loading = false }) }
const nextPage = () => { const curPage = page.pageIndex + 1 if (!totalRef.current || curPage > Math.ceil(+totalRef.current / page.pageSize)) { setPage((v) => { v.hasMore = false }) return } else { setPage((v) => { v.pageIndex = curPage }) } }
const getTotal = () => { return totalRef.current }
const reset = async () => { totalRef.current = 0 await setPage((v) => { v.pageIndex = 1 v.hasMore = true })
if (!auto || page.pageIndex == 1) { handleFetch() } }
return { page, getTotal, nextPage, reset } }
|
ScrollView
的封装,这里举例在Taro小程序中开发的场景实现自定义的滚动加载UI组件,也可以结合对应的UI组件实现二次封装。
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 49 50 51 52
| function CustomScrollView (props, ref) { const { children, fetch, auto = true } = props
const { page, getTotal, nextPage, reset } = usePage({ fetch, auto }) const wrapRef = useRef(null) useEffect(() => { let observer = Taro.createIntersectionObserver(Taro.getCurrentInstance().page, { observeAll: true }) setTimeout(() => { observer.relativeToViewport({ bottom: 0 }).observe('.scrollview-bottom', (res) => { if (res.intersectionRatio > 0) { if (page.hasMore && !page.loading) { nextPage() } } }) }, 0)
return () => { observer.disconnect() } }, [page])
useImperativeHandle(ref, () => ({ reset: () => { reset() } }))
return ( <View ref={wrapRef}> <View className='custom-scrollview-body'>{children}</View> {page.loading && <div>正在加载...</div>} {!page.loading && !page.hasMore && getTotal() > 0 && ( <div className='no-more'>没有更多数据了</div> )} <View className='scrollview-bottom'></View> </View> ) }
export default React.forwardRef(CustomScrollView)
|
ScrollView
的使用,此处只保留分页相关的部分,其他内容省略
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
| function GoodsList () { const goodsRef = useRef(null) const [goodsList, setGoodsList] = useState([]) async function refreshGoods ({ pageIndex, pageSize }) { const data = await getGoodsList({ pageIndex, pageSize }) setGoodsList([ ...goodsList, ...data.list ]) return { total: data.total }; } function resetData (){ setGoodsList([]) goodsRef.current.reset() } return ( <View className='goods-list'> {/* 其他逻辑省略... */} <View className='content'> <CustomScrollView className='list-scroll' ref={goodsRef} fetch={refreshGoods}> { goodsList.map(goodsInfo => <GoodsItem {...goodsInfo} key={goodsInfo.itemId} />) } </CustomScrollView> </View> </View> ) } export default GoodsList
|
React中使用装饰器
在class方式的开发中,可以使用装饰器来单独封装分页的逻辑(Vue中的话用mixin也可以,但是封装到分页组件内部逻辑中可能更为方便)。以下是装饰器的写法:
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 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
| export default function withPager(Component) { return class CustomPagerComponent extends Component { constructor(props) { super(props) const { pageSize = 10, pageIndex = 0, pageTotal = 0 } = props || {}
const page = { hasMore: true, isLoading: false, total: pageTotal, pageIndex: pageIndex, pageSize: pageSize }
this.state.page = page }
nextPage = async () => { const { page } = this.state if (!page.hasMore || page.isLoading) return
page.isLoading = true this.setState({ page })
const { pageIndex, pageSize } = page const curPage = pageIndex + 1 const { total } = await this.fetch({ pageIndex: curPage, pageSize }) if (!total || curPage >= Math.ceil(+total / pageSize)) { page.hasMore = false }
this.setState({ page: { ...page, total, pageIndex: curPage, isLoading: false, } }) } reset(cb = () => { }) { const page = { ...(this.state.page || {}), pageIndex: 0, total: 0, isLoading: false, hasMore: true } this.setState({ page }, cb) } } }
|
直接在对应页面中使用,这里结合小程序的ScrollView
使用,同样只展示分页相关的逻辑。
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 49 50 51 52 53 54 55 56 57 58
| @withPager export default class GoodsList extends Component { constructor(props) { super(props); this.state = { ...this.state, goodsList: [] } } componentDidMount() { this.nextPage(); } async function fetch ({ pageIndex, pageSize }) { const data = await getGoodsList({ pageIndex, pageSize }) this.setState({ goodsList: [ ...this.state.goodsList, ...data.list ] }) return { total: data.total_count }; } function resetData (){ this.reset(); this.setState({ goodsList: [] }) } render() { const { goodsList } = this.state; return ( <View className='goods-list'> {/* 其他逻辑省略... */} <View className='content'> <ScrollView className='list-scroll' onScrollToLower={this.nextPage} scrollY > { goodsList.map(goodsInfo => <GoodsItem {...goodsInfo} key={goodsInfo.itemId} />) } </CustomScrollView> </View> </View> ) }
|