背景:首页多场考试【分页请求的数据】,点击进入开始考试页。产品想要的效果:在开始考试页点击返回定位到首页上次浏览的位置。项目框架中未使用第三方keepAlive缓存路由(缓存路由会损失部分数据实时性,要根据项目实际情况酌情使用),另外考试是分页的数据,使用scrollTop定位需要多次定位。
接下来是一步步,由简单逐渐优化到最终代码(核心代码)的过程,github可以获取全部代码:
一、前置知识
(1)scrollTop介绍:
MDN描述:Element.scrollTop
属性可以获取或设置一个元素的内容垂直滚动的像素数。
// 设置滚动的距离
element.scrollTop = intValue;
(2) es6的export共用数据
通过es6的exort导出的函数或数据可以供外部import引入使用。当多次import同一文件时,只会在第一次import时执行文件中的代码,后续import不会再次执行,可以直接使用。
export 相当于闭包,export function外部的数据是共享的。
// share.js
let shareData = "我是供export function 公共使用的数据";
export function setShareData(newValue) {
console.log("shareData:", shareData);
shareData = newValue;
}
// first.js
import { setShareData } from "share.js";
setShareData("first文件中修改了公共数据shareData");// 输出:我是供export function 公共使用的数据
// second.js
import { setShareData } from "share.js";
setShareData("second文件中修改了公共数据shareData"); // 输出:first文件中修改了公共数据shareData
二、返回定位
切换到另一个页面需要保留当前页面的位置信息,数据就需要保存到全局【store】或不随页面销毁的位置【闭包】,接下来使用闭包的方式实现:
返回 -> 元素是否可滚动 -> 请求数据 -> 渲染页面 -> 元素是否可滚动 -> 设置scrollTop -> 请求分页数据
//index.tsx
import React, { useRef, useEffect, useCallback, ReactElement, useImperativeHandle, forwardRef } from "react";
// 全局保存定位信息
let allScrollTop = {};
interface IPropScrollWrapper {
scrollKey: string,
children?: ReactElement,
outerClass?: string,
outerHandleScroll?: Function
}
function ScrollWrapper(props: IPropScrollWrapper) {
const scroll = useRef<HTMLDivElement>(null);
// 渲染时是否被定位
const isSetPosition = useRef(false);
// 实时保存滚动位置
const inTimeScrollTop = useRef(0);
// 定位
const setPosition = useCallback((lastScrollTop) => {
if (scroll.current) {
scroll.current.scrollTop = lastScrollTop;
}
}, []);
// 每次渲染都会执行
useEffect(() => {
if (allScrollTop[props.scrollKey] === undefined) {
allScrollTop[props.scrollKey] = 0;
}
// 当scroll.current关联元素可滚动时,设置scrollTop到指定位置
if (!isSetPosition.current
&& scroll.current && isScrollElement(scroll.current)) {
isSetPosition.current = true;
setPosition(allScrollTop[props.scrollKey]);
}
});
// 销毁时保存位置信息到全局
useEffect(() => {
return () => {
allScrollTop[props.scrollKey] = inTimeScrollTop.current;
}
}, []);
// 滚动时触发 - 实时记录位置信息和调用外部传入滚动函数
const handleScroll = useCallback(() => {
let { scrollTop } = scroll.current;
inTimeScrollTop.current = scrollTop;
props.outerHandleScroll?.();
}, [props.outerHandleScroll]);
// 工具方法
// 判断元素是否可滚动
const isScrollElement = useCallback((element: HTMLElement): boolean => {
return element.scrollHeight > element.clientHeight
}, []);
return (
<div className={ styles['scroll-wrapper'] + " " + props.outerClass } ref={ scroll } onScroll={ handleScroll }>
{ props.children }
</div>
)
}
export default forwardRef(ScrollWrapper);
三、增加分页定位
首页考试列表数据是通过多次分页数据获取的,滚动时通过antd-mobile的 InfiniteScroll
监听并获取分页数据,当缓存位置在第二页时,上面的方法只定位一次到第一页的尾部。
返回 -> 元素是否可滚动 -> 请求数据 -> 渲染页面 -> 元素是否可滚动 -> setTimeout 16ms设置一次scrollTop -> 请求分页数据
//index.tsx
// 增加是否到达之前位置,没有,设置定位并通过setTimeout 16ms再次执行定位
const setPosition = useCallback((lastScrollTop) => {
if (scroll.current && !isRollFinish(scroll.current, lastScrollTop)) {
if(scroll.current.scrollTop == lastScrollTop) {
return;
}
scroll.current.scrollTop = lastScrollTop;
setTimeout(() => setPosition(lastScrollTop), 16);
}
}, []);
四、遇到的问题
当不同tab下的container包裹在同一个滚动组件中时,在tab1中container滚动区域最大1200,tab2下container滚动区域最大是800。
当缓存tab1为1000时,打开tab2,container中的内容会一直再最底部持续滚动。当然这是自己代码中写的setTimeout(func, 16),不到缓存的位置会一直设置定位。。。所以应该可以让父组件清除缓存信息。
//index.tsx
// 销毁时全局是否需要保存位置信息
const isRecordScrollPosition = useRef(true);
// 销毁时判断是否保存位置信息
useEffect(() => {
return () => {
if (!isRecordScrollPosition.current) {
delete allScrollTop[props.scrollKey];
return;
}
allScrollTop[props.scrollKey] = inTimeScrollTop.current;
}
}, []);
// ------供外部调用 start-------
// 销毁时清除定位信息
const clearThisPosition = useCallback(() => {
isRecordScrollPosition.current = false;
}, []);
// 下次定位到顶部(切换tab时展示内容定位顶部)
const setThisPositionToTop = useCallback(() => {
allScrollTop[props.scrollKey] = 0;
}, []);
// 获取组件内部滚动区域ref
const getScrollRef = useCallback(():null | HTMLDivElement => {
return scroll.current;
}, []);
useImperativeHandle(ref,()=>{
const handleRefs = {
clearThisPosition,
setThisPositionToTop,
getScrollRef
}
return handleRefs
},[]);
// ------供外部调用 end-------
五、实际使用
{/* 分页 key:router+序号,防止重名 */}
<ScrollWrapper ref={scrollRef} scrollKey={ props.history.location.pathname + "0" }>
{
!!mockExamList?.length &&
<>
{
mockExamList.map(item => (
<MockExamCard key ={ item.examNumber } { ...item } />
))
}
<InfiniteScroll loadMore={loadMore} hasMore={hasMore} threshold={ 50 }>
<>
{hasMore ? (
<>
<span>Loading</span><DotLoading />
</>
) : (
<span>没有更多考试啦~</span>
)}
</>
</InfiniteScroll>
<>
}
</ScrollWrapper>
最终代码
需要的可以私聊获取