根据宽度变化计算要展示的div的个数

14 阅读2分钟

需求:参数仅支持一行展示,参数params是个数组,要求每个参数 完整展示,展示不全不展示不做截断。

UI: image.png

难点: 1、行宽度是动态的 2、item 的宽度也是动态的,随字数改变

基本思路: 1、使用 ref 获取到真实的行元素,通过 offsetWidth 拿到行宽度 2、创建临时 div 元素测量文本宽度,首先遍历 params 数组,设置元素的 textContent ,然后通过offsetWidth 拿到临时 div 的宽度 3、设置一个 count 用于计数,当临时 div 的宽度小于等于行宽度时,则 count++ ,当临时 div 的宽度等于行宽度时,则 count 停止累加,最后,得到 count 4、params slice 出 count 长度的数据, map 得到最终的展示

遇到的问题: 1、获取行宽度打印出来是0

原因:考虑到可能是因为在 useEffect 执行时,DOM 元素还没有完全渲染或布局完成,导致通过 ref 获取 offsetWidth 为 0。

解决: 1、创建 ResizeObserver 监听元素大小变化; 2、使用 getBoundingClientRect 更精确的获取尺寸,因为它的精度是小数,并且他是在任何时机都可以返回的

结合 offsetWidth 和 getBoundingClientRect().width 提高获取元素尺寸的可靠性。 适用于动态内容、动画过程中的元素测量,或在不同浏览器环境下的兼容性处理。

完整代码:

<div ref={rightRef} className={styles.right}>
    <div className={styles.productParams}>
        {displayItems[0]?.bindProps
          ?.slice(0, visibleParamCount)
          .map((item, index, array) => (
            <>
              <div key={item?.propertyText}>{item?.valueText}</div>
              {index < array.length - 1 && <div />}
            </>
          ))
         }
    </div>
<div>
.right {
    width: calc(100vw - 154px);
    max-width: 600px;
    flex: auto;
    display: flex;
    flex-direction: column;
    justify-content: center;
    gap: 3px;
  }
const rightRef = useRef<HTMLDivElement>(null);
const [visibleParamCount, setVisibleParamCount] = useState<number>(1);

  // 计算可显示参数数量
  useEffect(() => {
    if (!rightRef.current || !displayItems[0]?.bindProps) return;

    const { bindProps = [] } = displayItems[0] || {};
    if (bindProps.length === 0) return;

    let isCancelled = false;

    const calculateParams = () => {
      if (isCancelled || !rightRef.current) return;

      // 创建临时元素来测量文本宽度
      const tempContainer = document.createElement('div');
      tempContainer.style.position = 'absolute';
      tempContainer.style.visibility = 'hidden';
      tempContainer.style.whiteSpace = 'nowrap';
      tempContainer.style.fontSize = '12px';
      tempContainer.style.fontFamily = 'Arial, sans-serif';
      document.body.appendChild(tempContainer);

      try {
        // 获取可用宽度
        const availableWidth = rightRef.current.offsetWidth;

        // 如果宽度仍然为0,尝试使用 getBoundingClientRect
        const rectWidth = rightRef.current.getBoundingClientRect().width;
        const finalWidth = availableWidth > 0 ? availableWidth : rectWidth;
        
        if (finalWidth <= 0) {
          // 如果仍然为0,稍后重试
          setTimeout(() => {
            if (!isCancelled) {
              calculateParams();
            }
          }, 100);
          return;
        }

        console.log('获取可用宽度', finalWidth);

        let currentWidth = 0;
        let count = 0;

        for (let i = 0; i < bindProps.length; i++) {
          const currentBindProp = bindProps[i];
          // 安全访问属性
          const valueText = currentBindProp?.valueText || '';
          tempContainer.textContent = valueText;
          const paramWidth = tempContainer.offsetWidth;

          console.log('测量每一个参数宽度==', paramWidth);

          // 测量分隔符宽度(如果不是最后一个元素)
          let separatorWidth = 0;
          if (i < bindProps.length - 1) {
            separatorWidth = 13;
          }

          // 检查加上当前参数和分隔符后是否会超出可用宽度
          if (currentWidth + paramWidth + separatorWidth <= finalWidth || i === 0) {
            currentWidth += paramWidth + separatorWidth;
            count++;
          } else {
            // 超出宽度,停止添加
            break;
          }
        }
        console.log('可以显示的参数个数', Math.max(1, count));

        // 至少显示一个参数
        if (!isCancelled) {
          setVisibleParamCount(Math.max(1, count));
        }
      } finally {
        document.body.removeChild(tempContainer);
      }
    };

    // 立即尝试计算
    calculateParams();

    // 创建 ResizeObserver 监听元素大小变化
    const resizeObserver = new ResizeObserver(() => {
      if (rightRef.current && rightRef.current.offsetWidth > 0) {
        calculateParams();
      }
    });

    if (rightRef.current) {
      resizeObserver.observe(rightRef.current);
    }

    return () => {
      isCancelled = true;
      resizeObserver.disconnect();
    };
  }, [displayItems[0]?.bindProps, displayItems[0]?.spuTitle]);