H5上拉加载

2022-08-23  本文已影响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;
  }
}
上一篇下一篇

猜你喜欢

热点阅读