react组件库系列:实现Anchor组件
组件基本样式
因为滚动到了基础锚点这个标题上,所以上方的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; }; 复制代码
好了,讲完,回家!
作者:孟祥_成都
链接:https://juejin.cn/post/7169176663782064135