美文网首页
H5上拉加载

H5上拉加载

作者: 书中自有颜如玉__ | 来源:发表于2022-08-23 17:56 被阅读0次

    好久没记笔记了,最近又做了h5的开发,使用了 antd-mobile 的 PullToRefresh 拉动刷新,但是毛病很多,最后还是决定自己封装。

    踩坑

    原理和之前做的下拉刷新一样,只是拉动的方向不一样,上拉加载需要判断滚动条处于底部位置,这个地方也有坑。

    1、正常情况下我们认为 scrollHeight - scrollTop - clientHeight === 0 则判断滚动条处于底部位置,但是真机测试发现这个数值不为零,而是0.xxx,所以我使用了5,我估计这也是 antd-mobile 的 PullToRefresh 失效的原因。

    2、对于全局滚动条的判断就更夸张了,在三星手机上 scrollHeight - scrollTop - clientHeight 大概是55.xxx,所以全局位置的判断我用了一个更大的值 100。

    废话不多说,上代码

    1、index.tsx

    /**
     * 上拉加载组件
     * 
     * lvxh
     */
    import { CSSProperties, useCallback, useEffect, useRef, useState } from 'react'
    import styles from './index.module.less'
    import { geiComtainer } from './utils'
    import { useCalculateHeight } from './useCalculateHeight'
    import { useTouchHandle } from './useTouchHandle'
    import { ReactNode } from 'react'
    
    interface Props {
      children: ReactNode // 子元素
      loadMore: (page: number) => Promise<any> // 上拉加载函数,会传page参数,需要返回一个 Promise 以更新加载提示
      pageSize?: number // pageSize
      initPage?: number // 初始page,默认1
      total?: number // 分页里的总数据,传入可以在加载完所有数据就停止加载并给出提示,不传会一直加载
      fixedHeight?: boolean // 是否不改变滚动元素高度,不改变的话 height = 窗口高度,不传 height = 窗口高度 - 列表距离顶部高度
      resetPage?: boolean | string // 是否重置 page,一个页面多个列表共用一个下拉刷新时重置状态,传入列表唯一的key即可
      docIsBottom?: boolean // 是否监听 html 的滚动条也到底部才触发更新,默认为 true
      height?: number | string // 支持自己配置height,不传值则使用 useCalculateHeight 计算出的height
      distanceToRefresh?: number // 刷新距离,默认值 20
      damping?: number // 拉动距离限制, 建议小于 200 默认值 100
      textStyle?: CSSProperties // 底部提示语样式
    }
    
    export default ({ 
      children, 
      loadMore, 
      pageSize = 10, 
      initPage = 1, 
      total, 
      fixedHeight, 
      resetPage, 
      docIsBottom = true,
      height,
      distanceToRefresh = 20,
      damping = 100,
      textStyle
    }: Props) => {
      const [show, setShow] = useState(false) // 提示元素隐藏
      const [text, setText] = useState('') // 提示信息
      const pageRef = useRef<number>(initPage)
      const totalRef = useRef<any>(total) // 执行加载标志
      const pageSizeRef = useRef<number>(pageSize)
      const [_height] = useCalculateHeight({ fixedHeight })
    
      useEffect(() => {
        if (total) totalRef.current = total
      }, [total])
      
      useEffect(() => {
        if (pageSize) pageSizeRef.current = pageSize
      }, [pageSize])
    
      useEffect(() => {
        if (resetPage) pageRef.current = initPage
      }, [resetPage, initPage])
    
      // 加载函数,每次 page加1,并且loadMore需返回一个Promies
      const loadMoreHandle = useCallback(async () => {
        pageRef.current++
        if (typeof loadMore === 'function') {
          await loadMore(pageRef.current)
          setShow(false)
          setText('')
          const container: any = geiComtainer()
          if (container) {
            // 加载完滚动条上移30,让用户看到新加载的数据
            container.scrollTop = container?.scrollTop + 30
          }
        }
      }, [loadMore])
    
      const [touchstart, touchmove, touchend] = useTouchHandle({ 
        totalRef, 
        pageRef,
        pageSizeRef,
        docIsBottom, 
        distanceToRefresh,
        damping,
        setText, 
        setShow, 
        loadMoreHandle 
      })
    
      const init = useCallback(() => {
        const container: any = geiComtainer()
        container?.addEventListener('touchstart', touchstart, false)
        container?.addEventListener('touchmove', touchmove, false)
        container?.addEventListener('touchend', touchend, false)
      }, [touchstart, touchmove, touchend])
    
      const remove = useCallback(() => {
        const container: any = geiComtainer()
        container?.removeEventListener('touchstart', touchstart, false)
        container?.removeEventListener('touchmove', touchmove, false)
        container?.removeEventListener('touchend', touchend, false)
      }, [touchstart, touchmove, touchend])
    
      useEffect(() => {
        const timer = setTimeout(() => {
          init()
        }, 0);
        () => {
          remove()
          clearTimeout(timer)
        }
      // eslint-disable-next-line react-hooks/exhaustive-deps
      }, [])
    
      return (
        <div className={styles['load-more']} >
          <div id="load-more-Container" className={styles['load-more-Container']} style={{ height: height ? height : _height }}>
            {children}
          </div>
          {show && <p className={styles['load-more-text']} style={textStyle}>{text}</p>}
        </div>
      )
    }
    
    

    2、useTouchHandle.ts

    import { useCallback, useRef } from 'react'
    import { geiComtainer, getScrollIsBottom } from './utils'
    
    let _startPos: any = 0,
      _startX: any = 0,
      _transitionWidth: any = 0,
      _transitionHeight: any = 0
    
    export const useTouchHandle = ({ 
      totalRef, 
      pageSizeRef, 
      pageRef,
      setText, 
      setShow, 
      loadMoreHandle, 
      docIsBottom,
      damping,
      distanceToRefresh 
    }: any) => {
      const flageRef = useRef<boolean>(false) // 执行加载标志
    
      // 手势起点,获取初始位置,初始化加载标志
      const touchstart = useCallback((e: any) => {
        // eslint-disable-next-line react-hooks/exhaustive-deps
        _startPos = e.touches[0].pageY
        // eslint-disable-next-line react-hooks/exhaustive-deps
        _startX = e.touches[0].pageX
        flageRef.current = false
      }, [])
      
      // 手势移动,计算滚动条位置、移动距离,判断在滚动条到达底部切移动距离大于30,设置加载标志为true,显示提示信息
      const touchmove = useCallback((e: any) => {
        if (_transitionWidth === 0) { // 阻止其频繁变动,保证能进入【上拉加载】就能继续【释放加载更多】
          // eslint-disable-next-line react-hooks/exhaustive-deps
          _transitionWidth = Math.abs(e.touches[0].pageX - _startX) // 防止横向滑动
        }
        // eslint-disable-next-line react-hooks/exhaustive-deps
        _transitionHeight = _startPos - e.touches[0].pageY
    
        if (getScrollIsBottom(docIsBottom) && _transitionWidth < 10 && _transitionHeight > 0 && _transitionHeight < damping) {
          const container: any = geiComtainer()
          container.style.transition = 'transform 0s'
          container.style.transform = `translateY(-${_transitionHeight}px)`
          if (totalRef.current < pageSizeRef.current * pageRef.current) {
            setText('没有更多数据了')
            setShow(true)
            flageRef.current = false
          } else {
            setText('上拉加载')
            setShow(true)
            if (_transitionHeight > distanceToRefresh) {
              flageRef.current = true
              setText('释放加载数据')
              _transitionHeight = 0
            }
          }
        }
      }, [pageSizeRef, pageRef, setShow, setText, totalRef, docIsBottom, distanceToRefresh, damping])
    
      // 判断加载标志flage,执行加载函数
      const touchend = useCallback(() => {
        const container: any = geiComtainer()
        container.style.transition = 'transform 0.5s'
        container.style.transform = 'translateY(0)'
    
        if (flageRef.current) {
          setText('加载中...')
          loadMoreHandle()
        } else {
          setShow(false)
          setText('')
        }
      }, [loadMoreHandle, setShow, setText])
    
      return [touchstart, touchmove, touchend]
    }
    

    3、utils.ts

    export const geiComtainer = () => {
      return document.getElementById('load-more-Container')
    }
    
    const isBottom = (dom: any, min = 5) => {
      return (dom?.scrollHeight || 0) - (dom?.scrollTop || 0) - (dom?.clientHeight || 0) < min // 滚动条到底部位置
    }
    
    export const getScrollIsBottom = (docIsBottom?: boolean) => { 
      return docIsBottom ? (isBottom(document.documentElement, 100) || isBottom(document.body, 100)) && isBottom(geiComtainer()) : isBottom(geiComtainer())
    }
    

    4、useCalculateHeight.ts

    /**
     * 计算元素高度
     */
    import { useCallback, useEffect, useState } from 'react'
    import { geiComtainer } from './utils'
    
    interface Props {
      fixedHeight?: boolean // 是否不改变滚动元素高度,不改变的话 height = 窗口高度,不传 height = 窗口高度 - 列表距离顶部高度
    }
    export const useCalculateHeight = ({ fixedHeight }: Props) => {
      const [height, setHeight] = useState<string | number>(0)
    
      // 计算滚动元素初始高度
      const calculateHeight = useCallback(() => {
        if (fixedHeight) {
          setHeight('100vh')
          return 
        }
        const initHeight = Math.max(document.body.clientHeight, document.documentElement.clientHeight)
        // eslint-disable-next-line react/no-find-dom-node
        const offsetTop = geiComtainer()?.offsetTop || 0
        let _height = initHeight - offsetTop
        if (_height < initHeight * 0.66) _height = initHeight * 0.66 // 最小三分之二
        setHeight(_height)
      }, [fixedHeight])
    
      // 计算滚动元素最终高度,在数据少的时候根据子级元素高度,设置滚动元素高度
      useEffect(() => {
        const timer = setTimeout(() => {
          calculateHeight()
        }, 0);
        () => clearTimeout(timer) 
      }, [calculateHeight])
    
      return [height]
    }
    

    5、index.module.less

    .load-more {
      overflow: hidden;
    
      .load-more-Container {
        overflow-y: auto;
        position: relative;
      }
      
      .load-more-text {
        position: fixed;
        bottom: 2rem;
        z-index: 10;
        width: 100%;
        padding-bottom: 0.2rem;
        font-size: 0.32rem;
        text-align: center;
      }
    }
    

    相关文章

      网友评论

          本文标题:H5上拉加载

          本文链接:https://www.haomeiwen.com/subject/pvmfjrtx.html