上划、下拉加载接口 (两种方式)
项目中有上划、下拉请求接口的操作,所以封装了统一的功能使用。两种方式实现滚动加载 一种是包含异步请求的 ,一种是纯组件。
以大写 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>
}