前言
之前写的react组件如下:
-
Affix组件: react组件库源码+ 单测解析(Affix 固钉组件)
-
GridLayout组件:秒杀ant design布局组件
-
Button和ButtonGroup 按钮组件: react组件库源码+ 单测解析(Button和ButtonGroup 按钮组件)
-
日历组件: react 日历组件上
-
日组件拖拽逻辑部分: react 日历组件下
-
Modal组件:实现一个比ant功能更丰富的Modal组件
-
Portal组件:react如何把组件渲染到任意另一个组件内?
组件基本样式
因为滚动到了基础锚点这个标题上,所以上方的Anchor组件中“基础锚点”字体高亮了
基本用法
<Anchor targetOffset={150}>
<AnchorItem href="#基础锚点" title="基础锚点" />
<AnchorItem href="#多级锚点" title="多级锚点" />
<AnchorItem href="#指定容器锚点" title="指定容器锚点" />
<AnchorItem href="#特定交互锚点" title="特定交互锚点" />
<AnchorItem href="#尺寸" title="尺寸"></AnchorItem>
</Anchor>
我们主要讲解思路:
- 首先,如何判断,此时有标题已经进入了可视区域(浏览器窗口),然后把对应的AnchorItem组件颜色改成蓝色,表示正在预览此区域
- 然后,如何在点击AnchorItem的时候,滚动条滑动至对应的区域
初始化组件,判断浏览器窗口是否有锚点进入
我们的要跳转到的标题,需要id名字跟
<AnchorItem href="#基础锚点" title="基础锚点" />
上的href相同,例如
所以我们可以通过document.querySelector(href),来获取到不同锚点对应的dom的标题是哪个。
接着,我们用一个intervalRef来收集所有锚点的href属性,目的是通过document.querySelector(href)来获取到锚所有锚点对应的dom标题.
所以整个架构是外层有一个context收集AnchorItem的信息,如下:
<AnchorContext.Provider
value={{
onClick: handleClick,
activeItem,
registerItem,
unregisterItem,
}}
>
{children}
</div>
</AnchorContext.Provider>
registerItem就是注册函数
AnchorItem组件注册代码如下:
useEffect(() => {
registerItem(href);
return () => unregisterItem(href);
}, [href, registerItem, unregisterItem]);
在AnchorItem组件上还绑定了onClick事件,在AnchorContext上传给子组件的,用来滚动到对应标题上,如下:
<div
>
<a
href={href}
title={titleAttr}
target={target}
onClick={(e) => handleClick(e)}
>
{title}
</a>
</div>
然后我们继续看registerItem是个什么函数:
const intervalRef = useRef<IntervalRef>({
items: [],
scrollContainer: canUseDocument ? window : null,
handleScrollLock: false,
});
/**
* 注册锚点
* @param href 链接
*/
const registerItem = (href: string): void => {
const { items } = intervalRef.current;
if (/#(\S+)$/.test(href) && items.indexOf(href) < 0) items.push(href);
};
可以看出来intervalRef.current.items负责收集所有的子节点href信息。
因为父节点的context收集了这个信息才在初始化的时候,能够用dom的api获取到href对应的dom,然后计算其是否自己距离浏览器窗口顶部
useEffect(() => {
// 这里intervalRef.current.scrollContainer就是window
const { scrollContainer } = intervalRef.current;
handleScroll();
scrollContainer.addEventListener('scroll', handleScroll);
return () => {
scrollContainer.removeEventListener('scroll', handleScroll);
};
}, [container, handleScroll]);
所以这里最关键的代码在handleScroll,最关键的在于就算每一个标题的getBoundingClientRect().top的值,就是当前dom跟浏览器顶部高度的值,然后其中小于等于0的中,最大的那个就是当前锚点应该选中的值
具体代码如下:
// 这段代码一看就是新手写的,没办法,这是源码,就硬着头皮解读一下吧
const handleScroll = useCallback(() => {
// 获取window元素
const { scrollContainer } = intervalRef.current;
// 获取到所有注册的herf
const { items } = intervalRef.current;
const filters: { top: number; href: string }[] = [];
let active = '';
// 找出所有当前 top 小于预设值
items.forEach((href) => {
const anchor = document.querySelector(href);
if (!anchor) return;
// anchor.getBoundingClientRect().top是指元素到浏览器窗口顶部的距离
// document.documentElement.clientTop是指html文档上边框的高度,一般都是0
const top = anchor.getBoundingClientRect().top - document.documentElement.clientTop;
// bounds + targetOffset可以理解为想要到浏览器顶部的空白区域
// bounds默认是5,targetOffset默认是0
if (top <= bounds + targetOffset) {
filters.push({
href,
top,
});
}
});
// 找出小于预设值集合中top最大的
if (filters.length) {
const latest = filters.reduce((prev, cur) => (prev.top > cur.top ? prev : cur));
active = latest.href;
}
// 将当前需要激活的锚点通过setActiveItem更新
if (active !== activeItem) {
onChange?.(active, activeItem);
setActiveItem(active);
}
}, [activeItem, bounds, onChange, targetOffset]);
是不是很简单啊,哈哈,就是一个getBoundingClientRect().top API的运用而已。
然后来个小插曲,就是我们知道哪个锚点被激活了,所以对应锚点上的样式就需要改变,比如颜色变为蓝色,这个咋做呢?
useEffect监听被激活的锚点属性,然后更改样式即可。
如何在点击AnchorItem的时候,滚动条滑动至对应的区域
首先我们在锚点上注册一个点击事件,点击就触发滚动
const handleClick = (item: Item, e: React.MouseEvent<HTMLDivElement>) => {
onClick?.({ e, ...item });
handleScrollTo(item.href);
};
接着我们看看handleScrollTo是如何处理的,传参注意是传的href.
这里的核心逻辑是,利用 document.documentElement.scrollTop = 锚点标题距离页面顶端的距离;
来实现定位的效果
const handleScrollTo = (link: string) => {
// 找到锚点对应的标题的dom
const anchor = document.querySelector(link);
if (!anchor) return;
onChange?.(link, activeItem);
setActiveItem(link);
const { scrollContainer } = intervalRef.current;
// 这里因为scrollContainer是window,所以scrollTop是pageXOffset,意思是滚动多少距离
// 如果是普通dom则scrollTop就是这个dom的scrollTop属性
const scrollTop = getScroll(scrollContainer);
// 因为这里是window,所以offsetTop = anchor.getBoundingClientRect().top - document.documentElement.clientTop;
const offsetTop = getOffsetTop(anchor, scrollContainer);
// 所以真正滚动的距离就是滚动条的距离 + 锚点标题到浏览器视口的距离,减去targetOffset(锚点的偏移量)
const top = scrollTop + offsetTop - targetOffset;
document.documentElement.scrollTop = top;
};
好了,讲完,回家!
之前写的react组件如下:
-
Affix组件: react组件库源码+ 单测解析(Affix 固钉组件)
-
GridLayout组件:秒杀ant design布局组件
-
Button和ButtonGroup 按钮组件: react组件库源码+ 单测解析(Button和ButtonGroup 按钮组件)
-
日历组件: react 日历组件上
-
日组件拖拽逻辑部分: react 日历组件下
-
Modal组件:实现一个比ant功能更丰富的Modal组件
-
Portal组件:react如何把组件渲染到任意另一个组件内?
-
Input组件:# 你知道Compositionstart和Compositionend事件吗,react 组件库之Input组件的坑