前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >动态监听DOM元素高度变化

动态监听DOM元素高度变化

作者头像
用户1097444
发布2022-06-29 14:11:12
4.9K0
发布2022-06-29 14:11:12
举报
文章被收录于专栏:腾讯IMWeb前端团队

1、背景

考虑这样一种情况,产品同学希望达到以下功能:

在我们的网页中有一个固定区域,这个区域会用于渲染从后端拉取的含有图片等资源的富文本字符串。

他需要在内容不超过一个最大高度的时候完全显示所有内容,超过最大内容后仅展示最大高度范围内的内容,超出部分隐藏,并通过一个按钮 “展示更多” 来给用户展示更多的选择。

在这看似简单的需求当中,其实涉及到了一个难点,那就是怎样动态的监听到内容区域的高度变化?

因为在这里面会含有图片资源,他们在渲染的时候会发起网络请求,等待图片加载完成后触发浏览器重排,该区域的高度被撑开。

因此,内容区域的高度是动态变化,且变化的时间点是未知的,那么怎样知道我们的内容区高度发生了变化呢?

为此我做了以下几种尝试:

  • MutationObserver
  • IntersectionObserver
  • ResizeObserver
  • 监听所有资源的 onload 事件
  • iframe

2、MutationObserver

MutationObserver 接口提供了监视对 DOM 树所做更改的能力。它被设计为旧的 Mutation Events 功能的替代品,该功能是 DOM3 Events 规范的一部分。

observe(target, options)

这个方法会根据传入的 options 配置,观察 DOM 树中的单个 Node 或者所有的子孙节点的变化。

他一共有七个属性,这里就不一一介绍了,可以通过 MutationObserverInit 来获取相应的介绍.

那么我们要怎么使用这个 API 来监听目标区域的高度变化呢?

  1. 首先我们要创建对该区域的 dom 根结点引用:
代码语言:javascript
复制
// useRef创建引用
const contentRef = useRef();

// 绑定ref
<div
  className="content"
  dangerouslySetInnerHTML={{ __html: details }}
  style={{ maxHeight }}
  ref={contentRef}
/>;
  1. 然后我们需要创建一个 MutationObserver 实例:
代码语言:javascript
复制
const [height, setHeight] = useState(-1);
const [observer, setObserver] = useState<MutationObserver>(null!);
useEffect(() => {
  const observer = new MutationObserver((mutationList) => {
    if (height !== contentRef.current?.clientHeight) {
      console.log("高度变化了!");
      setHeight(contentRef.current.clientHeight);
    }
  });
  setObserver(observer);
}, []);
  1. 当我们的 ref 或者 observer 发生变化的时候,对 ref 节点进行观察:
代码语言:javascript
复制
useEffect(() => {
  if (!observer || !contentRef.current) return;
  observer.observe(contentRef.current, {
    childList: true, // 子节点的变动(新增、删除或者更改)
    attributes: true, // 属性的变动
    characterData: true, // 节点内容或节点文本的变动
    subtree: true, // 是否将观察器应用于该节点的所有后代节点
  });
}, [contentRef.current, observer]);

完整代码:

代码语言:javascript
复制
const Details = () => {
    // useRef创建引用
    const contentRef = useRef();
    const [height, setHeight] = useState(-1);
    const [observer, setObserver] = useState<MutationObserver>(null!);

    useEffect(() => {
          const observer = new MutationObserver((mutationList) => {
            if (height !== contentRef.current?.clientHeight) {
                console.log('高度变化了!');
                setHeight(contentRef.current.clientHeight);
            }
          });
          setObserver(observer);
    }, []);

    useEffect(() => {
          if (!observer || !contentRef.current) return
          observer.observe(contentRef, {
            childList: true, // 子节点的变动(新增、删除或者更改)
            attributes: true, // 属性的变动
            characterData: true, // 节点内容或节点文本的变动
            subtree: true// 是否将观察器应用于该节点的所有后代节点
          });
    }, [contentRef.current, observer]);

    // 绑定ref
    return<div className="content" dangerouslySetInnerHTML={{ __html: details }} style={{ maxHeight }} ref={contentRef} />
}

经过上面的一番操作之后,发现根本达不到效果,因为我们的 css 属性根本没有发生变化(我们是通过 maxHeight 来约束容器的高度的), 但是资源加载完毕之后,浏览器重排根本没有产生 css 属性的变化,它的高度是自动计算的

因此这个方案无济于事!但是它确实可以监听到认为修改容器的高度产生的变化,比如:contentRef.current.style.height = '1000px',这个 api 是可以监听到这一操作的,但是并不符合我们的场景

此外,它的浏览器兼容性也还行:

3、IntersectionObserver

经过激情编码,最后发现 MutationObserver 根本达不到我们想要的效果之后,其实我的心态已经产生了一些变化,不过不要紧!

我们可以换一种思路,既然我们无法通过监听容器的高度变化来展示相应的 “展开更多” 操作,那么我们可不可以将这个 “展开更多” 固定到一个位置上,然后超出部分隐藏,

当我们的内容自动撑开,达到指定高度后,我们这个 “展开更多” 的操作的按钮就显示出来了,听上去不错,能达到要求!废话不多说,开撸!

因为这里只涉及到相应的 css 样式的书写,就不做展示了。

经过处理之后,确实在容器高度小于指定高度的时候,“展示更多” 按钮不会展示,超过最大值之后,会将该按钮展示出来,

但是也遇到了一个问题,操作按钮是有高度的,如果我们的内容高度介于最大高度 - 按钮高度 到 容器的最大高度之间, 按钮会产生显示一部分,同时又隐藏一部分的效果,这可不是我们想要的!

如图所示:

显然这种效果是不符合要求的,我们的 “展示更多” 按钮,只有两种状态,要么全部展示,要么不展示,没有这种部分展示的效果

因此我查阅了相关资料,了解到了 IntersectionObserver 这个 API,它可以监听一个元素是否进入用户视野,它的相关使用方法可以参考这篇文章:IntersectionObserver API 使用教程

它使用起来和 MutationObserver 几乎一样,只是名字不一样而已

它监听的值里面有一个比较重要的属性:intersectionRatio

借助这个 API,我的设计思路是这样的:

当用户滚动网页的时候(或者不滚动,此时目标区域已经出现在屏幕中),可以得到 intersectionRatio 的值,通过判断这个值是否等于 1 来决定要不要展示 “展示更多” 按钮

但经过我的编码实现后,发现滚动事件发生的时候,intersectionRatio 的变化是不可靠的,有时候完全可见了,但是它并不等于 1。经过多轮实验,结果依然如此。但是它确实可以用来判断一个元素是否进入用户视野

由于使用上结果的不可靠,我放弃这个方案(可能是我使用方式上出了问题)

它的各浏览器兼容性如下:

4、ResizeObserver

顾名思义,这个 API 就是专门监听 DOM 尺寸变化的,只不过它还处于试验阶段,各浏览器的兼容性很差,所以基本不考虑

具体使用方法可以参考这篇文章:检测 DOM 尺寸变化 JS API ResizeObserver 简介

它现阶段各浏览器的兼容性情况:

5、监听所有资源的 onload 事件

既然上述方法都不行,那么我绞尽脑汁,又想出了另外一种方法:监听所有带有 src 属性的 DOM 元素的 onload 事件,通过他的回调来判断当前容器的高度情况

这种实现方式,在思路上是完全符合目的的,具体做法参考如下:

代码语言:javascript
复制
const [height, setHeight] = useState(-1);
const [showMore, setShowMore] = useState(false);
// contentRef 的定义见 MutationObserver 一节
useEffect(() => {
  const sources = contentRef.current.querySelectorAll("[src]");
  sources.onload = () => {
    const height = contentRef?.current?.clientHeight ?? 0;
    const show = height >= parseInt(MAX_HEIGHT, 10);

    setHeight(height);    setShowMore(show);
  };
}, []);

通过这种方式可以实现对富文本中的图片进行加载后,对容器高度进行相应的判断。

但是这种方式,存在不确定性,即无法判断是否找齐了所有高度由内容撑开的资源。

6、Iframe

这是终极方案,也是在此背景中所采用的方案。

既然 window 可以监听到 resize 事件,那么我们就可以利用 iframe 来达到同样的效果,具体做法就是在容器里面嵌套一个隐藏的高度为 100% 的 iframe,通过监听他的 resize 事件,来判断当前容器的高度。

话不多说,具体实现方式如下:

代码语言:javascript
复制
const Detail: FC<{}> = () => {
  const ref = useRef<HTMLDivElement>(null);
  const ifr = useRef<HTMLIFrameElement>(null);
  const [height, setHeight] = useState(-1);
  const [showMore, setShowMore] = useState(false);
  const [maxHeight, setMaxHeight] = useState(MAX_HEIGHT);
  const introduceInfo = useAppSelect(
    (state) => state.courseInfo?.data?.introduce_info ?? {}
  );
  const details = introduceInfo.details ?? "";
  const isFolded = maxHeight === MAX_HEIGHT;
  const onresize = useCallback(() => {
    const height = ref?.current?.clientHeight ?? 0;
    const show = height >= parseInt(MAX_HEIGHT, 10);

    setHeight(height);    setShowMore(show);
    if (ifr.current && show) {
      ifr.current.remove();
    }
  }, []);

  useEffect(() => {
    if (!ref.current || !ifr.current?.contentWindow) return;
    ifr.current.contentWindow.onresize = onresize;
    onresize();
  }, [details]);

  if (!details) returnnull;

  return (
    <section className="section detail-content">
      <div className="content-wrapper">
        <div
          className="content"
          dangerouslySetInnerHTML={{ __html: details }}
          style={{ maxHeight }}
          ref={ref}
        />
        {/* 这个iframe是用来动态监听content高度的变化的 */}
        <iframe title={IFRAME_ID} id={IFRAME_ID} ref={ifr} />
      </div>
      {isFolded && showMore && (
        <>
          <div
            className="show-more"
            onClick={() => {
              setMaxHeight(isFolded ? "none" : MAX_HEIGHT);
            }}
          >
            查看全部
            <IconArrowDown className="icon" />
          </div>
          <div className="mask" />
        </>
      )}
    </section>
  );
};

这种方式实际上就是对 ResizeObserver 的一种 hack,经过多次实践,符合功能要求。

7、总结

  • 解决问题要尽可能的考虑多种情况,对比多种方案,采取最为可靠的一种方案。
  • 不断学习,多查询资料,你所遇到的问题基本上前人都已经踩过坑了。
  • 监听 DOM 元素的高度变化,可以采用内嵌 iframe 的方式来解决。

紧追技术前沿,深挖专业领域

扫码关注我们吧!

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-06-06,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 腾讯IMWeb前端团队 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1、背景
  • 2、MutationObserver
    • observe(target, options)
    • 3、IntersectionObserver
    • 4、ResizeObserver
    • 5、监听所有资源的 onload 事件
    • 6、Iframe
    • 7、总结
    相关产品与服务
    容器服务
    腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档