You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何实现独立于被包裹组件的SkeletonLoader/WireframeLoader?

组件骨架屏实现方案(基于限制条件)

问题背景

我们有如下UserCard组件:

function UserCard(props) {
  const { avatarUrl, name, loading } = props
  return (
    <div className='UserCard'>
      <Avatar image={avatarUrl} />
      <Name name={name} />
    </div>
  )
}

需要为其实现线框加载效果,且满足以下限制:

  • 不能为Avatar、Name组件单独设计骨架副本(避免组件尺寸/形状修改后骨架不同步)
  • 无法修改Avatar、Name组件内部结构或CSS
  • 可以从外部包裹组件,并控制它们的间距
  • 需适配组件的透明区域、圆角,仅在可见部分显示骨架动画

理想的Loader使用方式如下:

function UserCard(props) {
  const { avatarUrl, name, loading } = props
  return (
    <div className='UserCard'>
      <Loader loading={loading}>
        <Avatar image={avatarUrl} />
      </Loader>
      <Loader loading={loading}>
        <Name name={loading ? '占位文本控制骨架尺寸' : name} />
      </Loader>
    </div>
  )
}

一、单个子组件的Loader实现方法

方法1:CSS混合模式实现适配型骨架动画

核心思路是利用混合模式,让渐变动画仅在组件的不透明区域显示,同时用底色填充组件占位空间,自动适配圆角和透明区域。

import styled, { keyframes } from 'styled-components';

// 骨架闪烁动画
const shimmer = keyframes`
  0% { transform: translateX(-100%); }
  100% { transform: translateX(100%); }
`;

const LoaderOverlay = styled.div`
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: linear-gradient(90deg, transparent, rgba(255,255,255,0.7), transparent);
  animation: ${shimmer} 1.5s infinite;
  mix-blend-mode: overlay;
  pointer-events: none;
`;

const LoaderWrapper = styled.div`
  position: relative;
  display: inline-block;
  background-color: #e0e0e0;
  opacity: ${props => props.loading ? 1 : 0};
`;

function Loader({ loading, children }) {
  if (!loading) return <>{children}</>;
  
  return (
    <LoaderWrapper loading={loading}>
      {children}
      <LoaderOverlay />
    </LoaderWrapper>
  );
}

该方案无需提取组件形状,依赖浏览器原生混合模式特性,适配性强,代码简洁。

方法2:CSS遮罩实现精准骨架动画

通过提取组件的形状生成SVG遮罩,让骨架动画仅在组件可见区域显示,适合复杂形状的组件。

import styled, { keyframes } from 'styled-components';
import { useRef, useEffect, useState } from 'react';

const shimmer = keyframes`
  0% { transform: translateX(-100%); }
  100% { transform: translateX(100%); }
`;

const LoaderWrapper = styled.div`
  position: relative;
  display: inline-block;
  
  &::after {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: linear-gradient(90deg, 
      rgba(255,255,255,0) 0%, 
      rgba(255,255,255,0.6) 50%, 
      rgba(255,255,255,0) 100%);
    animation: ${shimmer} 1.5s infinite;
    mask: url(${props => props.maskUrl});
    mask-mode: alpha;
    pointer-events: none;
    opacity: ${props => props.loading ? 1 : 0};
  }
`;

function Loader({ loading, children }) {
  if (!loading) return <>{children}</>;
  
  const childRef = useRef(null);
  const [maskUrl, setMaskUrl] = useState('');
  
  useEffect(() => {
    if (!childRef.current) return;
    
    // 提取组件尺寸和圆角,生成简易SVG遮罩
    const { offsetWidth, offsetHeight } = childRef.current;
    const borderRadius = getComputedStyle(childRef.current).borderRadius;
    const svgMask = `data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${offsetWidth} ${offsetHeight}"><rect x="0" y="0" width="${offsetWidth}" height="${offsetHeight}" rx="${borderRadius}" fill="white"/></svg>`;
    setMaskUrl(svgMask);
  }, [loading]);
  
  return (
    <LoaderWrapper loading={loading} maskUrl={maskUrl}>
      {React.cloneElement(children, { ref: childRef })}
    </LoaderWrapper>
  );
}

如果组件是复杂不规则形状,可以用dom-to-svg等库提取更精准的DOM路径生成遮罩。


二、外部包裹UserCard为子div生成骨架的处理方式

若只能从外部包裹整个UserCard,且已知其为包含多个div子元素的容器,可通过以下方式实现:

实现思路

  1. 包裹原组件,加载状态下隐藏原组件
  2. 获取原组件子div的位置、尺寸、样式信息
  3. 生成匹配的骨架块并添加动画

代码实现

import styled, { keyframes } from 'styled-components';
import { useRef, useEffect, useState } from 'react';

const shimmer = keyframes`
  0% { transform: translateX(-100%); }
  100% { transform: translateX(100%); }
`;

const SkeletonBlock = styled.div`
  position: absolute;
  top: ${props => props.top}px;
  left: ${props => props.left}px;
  width: ${props => props.width}px;
  height: ${props => props.height}px;
  border-radius: ${props => props.borderRadius};
  background-color: #e0e0e0;
  overflow: hidden;
  
  &::after {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: linear-gradient(90deg, 
      rgba(255,255,255,0) 0%, 
      rgba(255,255,255,0.6) 50%, 
      rgba(255,255,255,0) 100%);
    animation: ${shimmer} 1.5s infinite;
  }
`;

const SkeletonWrapper = styled.div`
  position: relative;
  width: ${props => props.width}px;
  height: ${props => props.height}px;
`;

function UserCardSkeleton({ loading, children }) {
  if (!loading) return <>{children}</>;
  
  const cardRef = useRef(null);
  const [skeletonBlocks, setSkeletonBlocks] = useState([]);
  
  useEffect(() => {
    if (!cardRef.current) return;
    
    const card = cardRef.current;
    const childDivs = Array.from(card.querySelectorAll('div'));
    
    const blocks = childDivs.map(child => {
      const childRect = child.getBoundingClientRect();
      const cardRect = card.getBoundingClientRect();
      return {
        top: childRect.top - cardRect.top,
        left: childRect.left - cardRect.left,
        width: childRect.width,
        height: childRect.height,
        borderRadius: getComputedStyle(child).borderRadius
      };
    });
    
    setSkeletonBlocks(blocks);
  }, [loading]);
  
  return (
    <SkeletonWrapper width={cardRef.current?.offsetWidth || 0} height={cardRef.current?.offsetHeight || 0}>
      <div ref={cardRef} style={{ opacity: 0 }}>{children}</div>
      {skeletonBlocks.map((block, index) => (
        <SkeletonBlock key={index} {...block} />
      ))}
    </SkeletonWrapper>
  );
}

// 使用示例
function App() {
  const [loading, setLoading] = useState(true);
  
  return (
    <UserCardSkeleton loading={loading}>
      <UserCard avatarUrl="..." name="..." />
    </UserCardSkeleton>
  );
}

该方案完全从外部实现,无需修改原组件代码,骨架尺寸和样式会随原组件子元素自动同步。

内容的提问来源于stack exchange,提问作者grabantot

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.25 02:54:25