react组件库系列:实现Anchor组件

·  阅读 1592

前言

之前写的react组件如下:

组件基本样式

image.png

因为滚动到了基础锚点这个标题上,所以上方的Anchor组件中“基础锚点”字体高亮了

image.png

基本用法

<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相同,例如

image.png

所以我们可以通过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组件如下:

分类:
前端
收藏成功!
已添加到「」, 点击更改