实现上划、下拉加载接口 (react,hook,IntersectionObserver)

466 阅读4分钟

上划、下拉加载接口 (两种方式)

项目中有上划、下拉请求接口的操作,所以封装了统一的功能使用。两种方式实现滚动加载 一种是包含异步请求的 ,一种是纯组件。

以大写 I 开头的都是标注改数据的数据类型,可删除

方式一 :适用于 依赖接口结构的高度统一不需要对 list 有复杂的操作的场景

ScrollList 是完全依赖 url,param,以及分页返回的数据统一做数据渲染,需要将接口的请求 参数传递过去 type="top|bottom" 表示是向上滚动还是向下滚滚动加载,renderItem 是对每一行的数据进行内容填充

import { ScrollList  } from 'src/pages/collaboration/components/component/ScrollList';

   const scrollParam: IScrollParam = {
        url: '/list',
        params: {},
        type: 'bottom',
        renderItem: (item: IItem, index: number) => {
            return <div style={{ height: '150px' }}>"内容"+{item.value}</div>;
        },
        rootStyle: { height: '60vh' },
    };

    ...
    <ScrollList {...scrollParam} />
    ...

// ScrollList 实现
import React, { useEffect, useRef, useState } from 'react';
import { useScrollData } from '../../hooks/hooks';
import './index.less';
import { IItem } from '../../hooks'; // 类型定义 可删除

import { IScrollParam } from './type'; // 类型定义 可删除
/**
 *
 * @param param
 * @param param.url 滚动加载的url
 * @param param.params 滚动加载的 请求参数
 * @param param.renderItem 滚动加载的 每条数据的展示
 * @param param.type 向上滚动|向下滚动加载
 * @returns
 */
const ScrollList = (props: IScrollParam) => {
    const { url, params, renderItem, type = 'bottom' } = props;
    const rootRef = useRef(null);
    const topRef = useRef(null);
    const bottomRef = useRef(null);
    const target = type === 'top' ? topRef : bottomRef;
    const data = useScrollData({ url, param: params, root: rootRef, target: target });
    const [list, setList] = useState<IItem[]>([]);

    useEffect(() => {
        if (type === 'bottom') {
            setList((list) => [...list, ...data]);
        } else {
            if (data?.length > 0) {
                let lastele: any = data?.pop();
                lastele!.indexTag = 'lastTag';
                data.push(lastele);
                setList((list) => {
                    let arr = list.map((item) => {
                        if (item.indexTag) {
                            const { indexTag, ...other } = item;
                            return other;
                        } else {
                            return item;
                        }
                    });
                    return [...data, ...arr];
                });
            }
        }
    }, [data]);

    useEffect(() => {
        if (list?.length > 0 && type === 'top') {
            rootRef?.current?.querySelector('.last-tag').scrollIntoView(true);
        }
    }, [list?.length]);

    return (
        <div className={`root ${props?.rootClass}`} style={props?.rootStyle} ref={rootRef}>
            <div className={`top ${props?.topClass}`} style={props?.topStyle} ref={topRef}>
                我是有顶线的
            </div>
            {list?.map((item, index) => (
                <div
                    key={index + 'a' + item.value + item.value}
                    className={`${item.indexTag === 'lastTag' ? 'last-tag' : ''}`}
                >
                    {renderItem(item, index)}
                </div>
            ))}
            <div className={`bottom ${props?.bottomClass}`} style={props?.bottomStyle} ref={bottomRef}>
                我是有底线的
            </div>
        </div>
    );
};

export default ScrollList;


// useScrollData 实现
import { $http, smsHttp, partnerHttp } from 'src/utils/http';
import { useState, useEffect, useRef } from 'react';
import { IItem, IParams } from './index';  // 类型定义 可删除

/**
 * 例:js
 * let list = useScrollData("/url",{})
 *
 * 例:dom
 * <div className="root" style={{width:'30vw',height:"80vh",backgroundColor:'#ccc',float:"right",overflow:"auto"}} >
 *  <div className="bottom">
 *  </div>
 * </div>
 *
 * @param url 请求列表的地址
 * @param param 请求参数
 * @param root 滑动时需要规定的根节点
 * @param target 滑动时检测目标位置的节点,以此来判断是否可加载
 * @returns 当前返回的data
 */

export const useScrollData = ({ url, param, root = 'root', target = 'bottom' }: IParams) => {
    const [list, setList] = useState<IItem[]>([]);
    const observerRef = useRef<any>({});
    const initObserver = () => {
        const callback = (entries: any) => {
            for (let item of entries) {
                if (item.isIntersecting) {
                    fetchData();
                }
            }
        };
        const options = {
            root: typeof root === 'string' ? document.querySelector(`.${root}`) : root?.current,
            rootMargin: '200px',
            threshold: [0.1],
        };
        const observer = new IntersectionObserver(callback, options);
        observerRef.current.observer = observer;
    };

    const fetchData = async (page?: number) => {
        let arr: IItem[] = [];
        for (let i = 0; i < 10; i++) {
            arr.push({
                value: Math.floor(Math.random() * 1000),
            });
        }
        await setTimeout(() => {
            console.log('请求中~');
            setList(arr);
        }, 500);

        // const result = await $http.get(url, {...param,page});
        // setList(result.data)
    };

    useEffect(() => {
        initObserver();
    }, []);
    useEffect(() => {
        observerRef?.current?.observer?.disconnect();
        const eleBottom = typeof target === 'string' ? document.querySelector(`.${target}`) : target?.current;
        eleBottom && observerRef?.current?.observer?.observe(eleBottom);
    }, [target]);
    return list;
};


方式二 :适用于列表结构复杂的数据场景

ScrollListRef 是依赖 ref 将 list 滚动容器和滚动机制进行关联,所以需要应用

useScrollDataWithRef(自定义hooks) 将返回值 refs.scrollRef 和 ScrollListRef 相关联, data 就是此次滚动所加载的数据,data支持复杂的数据结构,需要使用者自行解析,将list,data传入ScrollListRef 进行整合

通过 list 传入数据展示,setList 将数据上抛(可以理解为 onChange 事件)

需要将接口的请求 参数传递过去 useScrollDataWithRef 这里,每次滚动加载的时候都会触发data ,通过它实现滚动加载返回一次分页数据,同时将 data 传递到 ScrollListRef 中

type="top|bottom" 表示是向上滚动还是向下滚滚动加载

renderItem 是对每一行的数据进行内容填充

// 调用
import { ScrollListRef  } from 'src/pages/collaboration/components/component/ScrollList';
import { useScrollDataWithRef } from '../components/hooks/hooks';

    const [list, setList] = useState([]);

    const [refs, data] = useScrollDataWithRef({
        url: '',
        param: { current: 1 },
    });

   const scrollParam2: IScrollParamRef = {
        list,
        setList,
        type: 'top',
        renderItem: (item: IItem, index: number) => {
            return (
                <div style={{ height: '150px' }}>
                    {item.value}:+
                    为了理解中间件,让我们站在框架作者的角度思考问题:如果要添加功能,你会在哪个环节添加?
                    Reducer:纯函数,只承担计算 State
                </div>
            );
        },
        data,
        rootStyle: { height: '60vh' },
        ref: refs.scrollRef,
    };

    ...
    <ScrollListRef {...scrollParam2}></ScrollListRef>
    ...

import React, { forwardRef, useEffect, useRef, useImperativeHandle } from 'react';
import './index.less';
import { IScrollParamRef } from './type';
import { IItem } from '../../hooks';

/**
 *
 * @param param
 * @param param.list 已经渲染的全部数据
 * @param param.setList 滚动加载的url
 * @param param.params 滚动加载的 请求参数
 * @param param.renderItem 滚动加载的 每条数据的展示
 * @param param.type 向上滚动|向下滚动加载
 * @param param.data 滚动加载的请求数据
 * @returns
 */
const ScrollListRef = forwardRef((props: IScrollParamRef, refparams) => {
    const { list, setList, renderItem, type = 'bottom', data } = props;
    const rootRef = useRef(null);
    const topRef = useRef(null);
    const bottomRef = useRef(null);
    const targetRef = type === 'top' ? topRef : bottomRef;

    useImperativeHandle(
        refparams,
        () => {
            return {
                getRefs: () => ({ rootRef, targetRef }),
            };
        },
        []
    );

    useEffect(() => {
        if (type === 'bottom') {
            setList && setList((list: Array<{}>) => [...list, ...data]);
        } else {
            if (data?.length > 0) {
                let lastele: IItem = data?.pop();
                lastele!.indexTag = 'lastTag';
                data.push(lastele);
                setList &&
                    setList((list: Array<IItem>) => {
                        let arr = list.map((item: IItem) => {
                            if (item.indexTag) {
                                const { indexTag, ...other } = item;
                                return other;
                            } else {
                                return item;
                            }
                        });
                        return [...data, ...arr];
                    });
            }
        }
    }, [data]);

    useEffect(() => {
        if (list?.length > 0 && type === 'top') {
            rootRef?.current?.querySelector('.last-tag').scrollIntoView(true);
        }
    }, [list?.length]);

    return (
        <div className={`root ${props?.rootClass}`} style={props?.rootStyle} ref={rootRef}>
            {type === 'top' ? (
                <div className={`top ${props?.topClass}`} style={props?.topStyle} ref={topRef}>
                    我是有顶线的
                </div>
            ) : null}
            {list?.map((item: IItem, index: number) => (
                <div key={'index' + index} className={`${item.indexTag === 'lastTag' ? 'last-tag' : ''}`}>
                    {renderItem(item, index)}
                </div>
            ))}
            {type === 'bottom' ? (
                <div className={`bottom ${props?.bottomClass}`} style={props?.bottomStyle} ref={bottomRef}>
                    我是有底线的
                </div>
            ) : null}
        </div>
    );
});
export default ScrollListRef;

// useScrollDataWithRef 实现

import { $http, smsHttp, partnerHttp } from 'src/utils/http';
import { useState, useEffect, useRef } from 'react';
import { IItem, IParams, IScrollRefs } from '.';

/**
 * 例:js
 * let list = useScrollData("/url",{})
 *
 * 例:dom 具体使用被封装在 ScrollListRef中
 * <div className="root" style={{width:'30vw',height:"80vh",backgroundColor:'#ccc',float:"right",overflow:"auto"}} >
 *  <div className="bottom">
 *  </div>
 * </div>
 *
 * @param url 请求列表的地址
 * @param param 请求参数
 * @returns 当前返回的data
 */

export const useScrollDataWithRef = ({ url, param }: IParams): [IScrollRefs, Array<{}>] => {
    const [list, setList] = useState<IItem[]>([]);
    const observerRef = useRef<any>({});

    const refs: IScrollRefs = useScrollRefs();

    const initObserver = () => {
        const callback = (entries: any) => {
            for (let item of entries) {
                if (item.isIntersecting) {
                    fetchData();
                }
            }
        };
        const options = {
            root: refs?.root?.current,
            rootMargin: '200px',
            threshold: [0.1],
        };
        const observer = new IntersectionObserver(callback, options);
        observerRef.current.observer = observer;
    };

    const fetchData = async (page?: number) => {
        let arr: IItem[] = [];
        for (let i = 0; i < 10; i++) {
            arr.push({
                value: Math.floor(Math.random() * 1000),
            });
        }
        await setTimeout(() => {
            console.log('请求中~');
            setList(arr);
        }, 500);

        // const result = await $http.get(url, {...param,page});
        // setList(result.data)
    };

    useEffect(() => {
        initObserver();
    }, []);
    useEffect(() => {
        observerRef?.current?.observer?.disconnect();
        if (typeof refs.target === 'object') {
            refs?.target && observerRef?.current?.observer?.observe(refs.target.current);
        }
        return () => observerRef?.current?.observer?.disconnect();
    }, [refs?.target]);
    return [refs, list];
};

export const useScrollRefs = (): IScrollRefs => {
    const scrollRef = useRef(null);
    const refs = scrollRef?.current?.getRefs();
    return { scrollRef, root: refs?.rootRef, target: refs?.targetRef };
};

// .type.d.ts 类型定义
export interface IScrollParam {
    url: string;
    params: any;
    renderItem: Function;
    type: 'top' | 'bottom';
    rootClass?: string;
    topClass?: string;
    bottomClass?: string;
    rootStyle?: React.CSSProperties;
    topStyle?: React.CSSProperties;
    bottomStyle?: React.CSSProperties;
}

export interface IScrollParamRef {
    list: Array,
    setList:Function,
    renderItem: Function;
    type: 'top' | 'bottom';
    data: Array,
    rootClass?: string;
    topClass?: string;
    bottomClass?: string;
    rootStyle?: React.CSSProperties;
    topStyle?: React.CSSProperties;
    bottomStyle?: React.CSSProperties;
    ref:MutableRefObject<null>
}