【组件开发系列】骨架屏

2,192 阅读13分钟

一、前言

在过去的两个月中,我们的移动端项目重构UI组件,表单组件、Layout布局、弹框提示、导航、Card卡片等基础组件已经完成并应用到日常功能开发中。

一期的开发中,我们把基础的常用的组件基本都完成了。二期计划将一些不太常用但是能提升交互体验组件纳入开发计划,比如骨架屏,比如步骤条等。组件开发系列第一篇,让我们一起来实现一个骨架屏组件的开发吧。

注:移动端项目使用的react框架,组件开发使用的hooks函数式组件。

二、知其然

2.1 何为骨架屏

Skeleton Screen(骨架屏)就是在页面数据尚未加载前先给用户展示出页面的大致结构,直到请求数据返回后再渲染页面,补充进需要显示的数据内容。

如何页面数据没有加载完毕,会展示空白,骨架屏的主要作用的替代白屏,展示页面的大致结构,直到页面数据完全返回。

2.2 何时使用

1)首次加载数据时,可能出现白屏,可以使用骨架屏替代白屏展示;

2)数据量较大的列表中,每次数据返回前,可以使用骨架屏做临时展示;

3)某些授权中间页,一般授权中间页并没有内容,所以会出现短暂的空白,可以使用骨架屏替代空白页展示;

以上几种情况会造成页面空白,使用骨架屏代替展示,可以提升用户体验,视觉上是可以看到页面有内容的,且内容铺满了屏幕。

三、知其所以然

3.1 图解组件

骨架屏01.png

上图是我完成的骨架屏的实际效果。因为骨架屏是替代页面实际内容暂时的占位,所以一个常见骨架屏的结构和常见的列表结构很像,包括头像、标题、段落三个部分。想实现一个基本的骨架屏,也基本涵盖了这三个部分,而这三块内容的风格可以是直角也可以是圆角,为了增加用户体验,可以增加动画效果,其中头像是非必须的,可以不展示,标题和段落是必须的,但是展示长度可以变化。既然骨架屏是暂时的,那么一旦数据加载完成,实际内容需要渲染出来,骨架屏需要被隐藏,因此骨架屏展示可以有控制开关。

上面这段话,包含了基础的骨架屏开发的全部关键点,也帮我们屡清楚了开发是思路。 组件内容三部分:头像、标题、段落,组件传参props:头像对象、标题对象、段落对象、展示开关、动画效果、展示风格。

3.2 粮草先行

兵法有云:“兵马未动粮草先行”,组价开发也需要有序进行。前面,我们图解了骨架屏组件,清楚了它的结构和样式,不同样式和不同结构相互组合,能排列出不同的展示结构,而控制这些结构的关键在于组件的props传参,所以我们想开发一个组件,第一步要先定一下它的props传参,这也就是我们说的"粮草"。

PropTypes

在项目中使用自定义组件时,需要对组件的props进行类型检测。而React提供了专门的库,可以校验组件的props类型,也可以做一些特定的限制。关于PropTypes的详细介绍,可以看我另一篇文章【知识点】PropTypes提供的验证器,这里不做详细介绍。下面列出骨架屏组件的props类型校验,并做一一解读。

Skeleton.propTypes = {
  avatar: PropTypes.oneOfType([PropTypes.bool, PropTypes.Object]), // 是否显示头像占位图
  title: PropTypes.bool, // 是否显示标题占位图
  paragraph: PropTypes.oneOfType([PropTypes.bool, PropTypes.Object]), // 是否显示段落占位图
  show: PropTypes.bool, // 是否显示骨架屏,传 false 时会展示子组件内容
  active: PropTypes.bool, // 是否开启动画
  round: PropTypes.bool, // 是否将标题和段落显示为圆角风格
};

Skeleton.defaultProps = {
  avatar: false,
  title: true,
  paragraph: true,
  show: true,
  active: false,
  round: false,
};

avatar

是否显示头像占位图。

校验类型是布尔值和对象。

1)布尔值:控制图像模块是否展示,默认为false-不展示,可选true-展示。

2)对象:控制图像展示风格,包括:active-是否有动画效果,默认false-没有动画效果;shape-头像风格,默认circle-圆形,可选square-矩形;size-头像大小,默认default,可选large-较大, small-较小,number-具体值。

title

是否显示标题占位图。

校验类型是布尔值。

布尔值:控制图像模块是否展示,默认为true-展示,可选false-不展示。

paragraph

是否显示段落占位图

校验类型是布尔值和对象。

1)布尔值:控制段落模块是否展示,默认为true-展示,可选false-不展示。

2)对象:控制段落展示风格,包括:rows-设置段落的行数;width-设置段落的宽度,若为数组时则为对应段落每行宽度,反之则是最后一行的宽度。

show

骨架屏是否展示的开关。

校验类型是布尔值。

布尔值:控制骨架屏是否展示,默认为true-展示,可选false-不展示。

active

骨架屏是否开启动画效果。

校验类型是布尔值。

布尔值:控制骨架屏风格是否开启动画效果,默认为false-不开启动画效果,可选true-开启动画效果。

round

骨架屏风格是否是圆角。

校验类型是布尔值。

布尔值:控制骨架屏风格是否是圆角,默认为false-不是圆角,可选true-是圆角。

3.3 整顿兵马

props传参已设置好,接下来就可以进行下一步对“兵马”的整顿了。

正如我们前面所讲的,基础的骨架屏分为三个部分:头像、标题、段落,且实际内容加载完成之后可以选择隐藏骨架屏。所以“兵马”的布阵应该是这样的:

/** @name class前缀 */
const prefixCls = 'fly-skeleton';

/** @name 根元素class */
const rootCls = classnames(prefixCls, className, {
  [`${prefixCls}--round`]: round,
  [`${prefixCls}--active`]: active,
});

/** @name 内容class */
const contentCls = classnames(`${prefixCls}--content`);

return (
  <>
    {show ? (
      <div className={rootCls}>
        {avatarContent()}
        <div className={contentCls}>
          {titleContent()}
          {paragraphContent()}
        </div>
      </div>
    ) : (
      children
    )}
  </>
);

当参数show设置为true时,展示骨架屏内容,反之展示骨架屏组件内容包裹的子组件的内容即页面真实内容。其中头像、标题、段落又分别提炼成了方法,这样的处理,让组件的结构很清楚,易于维护。下面,我们来看这三块内容到底是怎么实现的。

头像

头像模块的功能实现如下:

/** @name 是否展示头像 */
const hasAvatar = !!avatar;

/** @name 尺寸枚举 */
const sizgClsObj = {
  large: 'lg',
  small: 'sm',
  default: null,
};

/**
 * 获取对象属性值
 * @param {void} 无
 * @return {render} 展示内容
 */
const getTypeOfObject = prop => {
  if (prop && typeof prop === 'object') {
    return prop;
  }
  return {};
};

/**
 * 头像展示
 * @param {void} 无
 * @return {render} 展示内容
 */
const avatarContent = () => {
  if (hasAvatar) {
    /** @name 头像对象数据 */
    let avatarObj = {
      active: false,
      shape: 'circle',
      size: 'default',
      ...getTypeOfObject(avatar),
    };

    /** @name 头像父容器class */
    const headCls = classnames(`${prefixCls}__header`);

    /** @name 头像class */
    const avatarCls = `${prefixCls}__avatar`;

    /** @name 尺寸class */
    const sizeCls = sizgClsObj[avatarObj.size] && classnames(`${avatarCls}--${sizgClsObj[avatarObj.size]}`);

    /** @name 形状class */
    const shapeCls = classnames(`${avatarCls}--${avatarObj.shape}`);

    /** @name 尺寸内联样式 */
    const sizeStyle =
      typeof avatarObj.size === 'number'
        ? {
            width: avatarObj.size,
            height: avatarObj.size,
            lineHeight: `${avatarObj.size}px`,
          }
        : {};

    return (
      <div className={headCls}>
        <span className={classnames(avatarCls, sizeCls, shapeCls, className)} style={{ ...sizeStyle }} />
      </div>
    );
  }
};

在上面的代码中,主要做了以下功能:

1)展示控制

通过avatar参数控制展示与否,如果骨架屏组件上的props没有avatar参数,则不展示头像模块,反之则展示;

2)头像大小

通过avatar参数中的size属性控制头像大小。

size值默认是default,可以设置固定变量值如:large-较大,small-较小,也可是设置具体数值,当设置具体数值的时候,会使用内联样式进行覆盖,将头像的宽度和高度的值设置为当前数值,单位均为像素。

3)头像风格

通过avatar参数中的shape属性控制头像风格。

shape的值默认是circle,也就是圆角风格,头像展示样式为圆形,如果shape的值设置为square,那么展示效果会为矩形。

标题

标题模块的功能实现如下:

/** @name 是否展示标题 */
const hasTitle = !!title;

/**
 * 获取对象属性值
 * @param {void} 无
 * @return {render} 展示内容
 */
const getTypeOfObject = prop => {
  if (prop && typeof prop === 'object') {
    return prop;
  }
  return {};
};

/**
 * 标题展示
 * @param {void} 无
 * @return {render} 展示内容
 */
const titleContent = () => {
  if (hasTitle) {
    /** @name 标题class */
    const titleCls = classnames(`${prefixCls}__title`);

    /** @name 标题style */
    const titleStyle = {
      width: !hasAvatar ? '35%' : '50%',
      ...getTypeOfObject(title),
    };

    return <h3 className={titleCls} style={titleStyle} />;
  }
};

标题模块的实现相较于头像会简单一些:

1)展示控制

通过title参数控制展示与否,默认展示标题,如果想隐藏标题,可以设置title的值为false;

2)标题宽度

通过title参数中的width属性控制标题宽度。

如果width变量设置了具体值,标题宽度取设置的值。不设置值的情况下,如果有头像则宽度的值默认为50%,如果没有头像则宽度的值默认为35%,当有头像的情况下,标题父容器的宽度会变小,所以对应值设置要大于没有头像的值。

段落

段落模块的功能实现如下:

/** @name 是否展示段落 */
const hasParagraph = !!paragraph;

/**
 * 获取对象属性值
 * @param {void} 无
 * @return {render} 展示内容
 */
const getTypeOfObject = prop => {
  if (prop && typeof prop === 'object') {
    return prop;
  }
  return {};
};

/**
 * 段落-获取段落宽度
 * @param {number} index 段落的索引
 * @param {object} obj 段落的数据对象
 * @return {number} 计算之后的宽度
 */
const getParagraphWidth = (index, obj) => {
  const { width, rows = 2 } = obj;
  // =>true: 如果width的值是数组时,设置每行对应宽度
  if (Array.isArray(width)) {
    return width[index];
  }
  // =>true: 如果width的值不是数组时,设置为最后一行的宽度
  if (rows - 1 === index) {
    return width;
  }
  return undefined;
};

/**
 * 段落展示
 * @param {void} 无
 * @return {render} 展示内容
 */
const paragraphContent = () => {
  if (hasParagraph) {
    /** @name 段落class */
    const paragraphCls = classnames(`${prefixCls}__paragraph`);
    let paragraphObj = {};

    // =>true: 有标题但是没有头像,默认3行,其他是2行
    if (!hasAvatar && hasTitle) {
      paragraphObj.rows = 3;
    } else {
      paragraphObj.rows = 2;
    }

    // => true: 没有标题或者没有头像,最后一个段落的宽度是61%
    if (!hasAvatar || !hasTitle) {
      paragraphObj.width = '61%';
    }

    // =>true: paragraph传参有值时,对默认值进行覆盖
    paragraphObj = {
      ...paragraphObj,
      ...getTypeOfObject(paragraph),
    };

    /** @name 段落的行数组 */
    const rowList = [...Array(paragraphObj.rows)].map((_, index) => <li key={index} style={{ width: getParagraphWidth(index, paragraphObj) }} />);

    return <ul className={paragraphCls}>{rowList}</ul>;
  }
};

段落的实现相对复杂一些,其中主要处理是针对段落行数的处理:

1)展示控制

通过paragraph参数控制展示与否,默认展示标题,如果想隐藏标题,可以设置title的值为false;

2)段落宽度

通过paragraph参数中的width属性控制段落宽度。如果width的值设置为数组,每行的宽度对应为数组中的值,如width值是具体数值或者字符串类型的值,那么段落最后一行的值会被设置。

3)段落的行数

通过paragraph参数中的rows属性控制段落的行数。如果rows属性有值,那么段落行数为rows的值,如果rows属性没有设置值,那么没有头像有标题的情况下,段落行数设置为3行,反之设置为2行。

3.4 优秀兵法

到此,”粮草兵马“皆已备齐,骨架屏组件圆满完成。接下来可以尝试在页面中使用它了,让骨架屏成为我们在日常开发中提升用户体验的优秀”兵法“。

四、组件使用

4.1 完整API

Skeleton

属性说明类型默认值
paragraph是否显示段落占位图boolean 或 SkeletonParagraphPropstrue
title是否显示标题占位图boolean 或 SkeletonTitlePropstrue
avatar是否显示头像占位图boolean 或 SkeletonAvatarPropsfalse
show是否显示骨架屏,传 false 时会展示子组件内容booleantrue
active是否开启动画booleanfalse
round是否将标题和段落显示为圆角风格booleanfalse

SkeletonParagraphProps

属性说明类型默认值
width设置段落的宽度,如果是数组则为每行的宽度,反之为最后一行的宽度number 或 string 或 Array<number 或 string>-
rows设置段落的行数number-

SkeletonTitleProps

属性说明类型默认值
width设置标题宽度number 或 string-

SkeletonAvatarProps

属性说明类型默认值
active是否开启动画效果,仅在单独使用头像骨架时生效booleanfalse
shape设置头像的形状,可选值circle 或 squarestringcircle
size设置头像的大小,可选值number或 large 或 small 或 default 或 number  stringdefault

4.2 完整代码

4.2.1 组件

skeleton.less

@textColorGrey: rgba(201, 201, 201, 0.2);
@textColorGrey2: rgba(186, 186, 186, 0.3);

.fly-skeleton {
  display: table;
  width: 100%;

  &--active {
    .fly-skeleton__avatar,
    .fly-skeleton__title,
    .fly-skeleton__paragraph > li {
      background: linear-gradient(90deg, @textColorGrey 25%, @textColorGrey2 37%, @textColorGrey 63%);
      background-size: 400% 100%;
      animation: fly-skeleton__loading 1.2s ease infinite;
    }
  }
  &--round {
    .fly-skeleton__title,
    .fly-skeleton__paragraph > li {
      border-radius: 100px;
    }
  }
  &--content {
    display: table-cell;
    width: 100%;
    vertical-align: top;
    .fly-skeleton__title {
      margin-top: 8px;
    }
  }
  &__avatar {
    flex-shrink: 0;
    width: 32px;
    height: 32px;
    margin-right: 15px;
    background-color: @textColorGrey;
    display: inline-block;
    &--circle {
      border-radius: 50%;
    }
    &--sm {
      width: 24px;
      height: 24px;
      line-height: 24px;
    }
    &--lg {
      width: 40px;
      height: 40px;
      line-height: 40px;
    }
  }

  &__title {
    height: 16px;
    background-color: @textColorGrey;
    & + .fly-skeleton__paragraph {
      margin-top: 20px;
    }
  }
  &__paragraph {
    li {
      width: 100%;
      height: 16px;
      list-style: none;
      background: @textColorGrey;
      border-radius: 4px;
      & + li {
        margin-top: 12px;
      }
    }
  }
}

@keyframes fly-skeleton__loading {
  0% {
    background-position: 100% 50%;
  }
  100% {
    background-position: 0 50%;
  }
}

Skeleton.jsx

/**
 * @description Skeleton 骨架屏
 * @author 叶一一
 */

import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import './skeleton.less';

const Skeleton = ({ ...props }) => {
  const { avatar, title, paragraph, round, active, show, children, className } = props;

  /** @name class前缀 */
  const prefixCls = 'fly-skeleton';

  /** @name 是否展示头像 */
  const hasAvatar = !!avatar;

  /** @name 是否展示标题 */
  const hasTitle = !!title;

  /** @name 是否展示段落 */
  const hasParagraph = !!paragraph;

  /** @name 尺寸枚举 */
  const sizgClsObj = {
    large: 'lg',
    small: 'sm',
    default: null,
  };

  /**
   * 获取对象属性值
   * @param {void} 无
   * @return {render} 展示内容
   */
  const getTypeOfObject = prop => {
    if (prop && typeof prop === 'object') {
      return prop;
    }
    return {};
  };

  /**
   * 头像展示
   * @param {void} 无
   * @return {render} 展示内容
   */
  const avatarContent = () => {
    if (hasAvatar) {
      /** @name 头像对象数据 */
      let avatarObj = {
        active: false,
        shape: 'circle',
        size: 'default',
        ...getTypeOfObject(avatar),
      };

      /** @name 头像父容器class */
      const headCls = classnames(`${prefixCls}__header`);

      /** @name 头像class */
      const avatarCls = `${prefixCls}__avatar`;

      /** @name 尺寸class */
      const sizeCls = sizgClsObj[avatarObj.size] && classnames(`${avatarCls}--${sizgClsObj[avatarObj.size]}`);

      /** @name 形状class */
      const shapeCls = classnames(`${avatarCls}--${avatarObj.shape}`);

      /** @name 尺寸内联样式 */
      const sizeStyle =
        typeof avatarObj.size === 'number'
          ? {
              width: avatarObj.size,
              height: avatarObj.size,
              lineHeight: `${avatarObj.size}px`,
            }
          : {};

      return (
        <div className={headCls}>
          <span className={classnames(avatarCls, sizeCls, shapeCls, className)} style={{ ...sizeStyle }} />
        </div>
      );
    }
  };

  /**
   * 标题展示
   * @param {void} 无
   * @return {render} 展示内容
   */
  const titleContent = () => {
    if (hasTitle) {
      /** @name 标题class */
      const titleCls = classnames(`${prefixCls}__title`);

      /** @name 标题style */
      const titleStyle = {
        width: !hasAvatar ? '38%' : '50%',
        ...getTypeOfObject(title),
      };

      return <h3 className={titleCls} style={titleStyle} />;
    }
  };

  /**
   * 段落-获取段落宽度
   * @param {number} index 段落的索引
   * @param {object} obj 段落的数据对象
   * @return {number} 计算之后的宽度
   */
  const getParagraphWidth = (index, obj) => {
    const { width, rows = 2 } = obj;
    // =>true: 如果width的值是数组时,设置每行对应宽度
    if (Array.isArray(width)) {
      return width[index];
    }
    // =>true: 如果width的值不是数组时,设置为最后一行的宽度
    if (rows - 1 === index) {
      return width;
    }
    return undefined;
  };

  /**
   * 段落展示
   * @param {void} 无
   * @return {render} 展示内容
   */
  const paragraphContent = () => {
    if (hasParagraph) {
      /** @name 段落class */
      const paragraphCls = classnames(`${prefixCls}__paragraph`);
      let paragraphObj = {};

      // =>true: 有标题但是没有头像,默认3行,其他是2行
      if (!hasAvatar && hasTitle) {
        paragraphObj.rows = 3;
      } else {
        paragraphObj.rows = 2;
      }

      // => true: 没有标题或者没有头像,最后一个段落的宽度是61%
      if (!hasAvatar || !hasTitle) {
        paragraphObj.width = '61%';
      }

      // =>true: paragraph传参有值时,对默认值进行覆盖
      paragraphObj = {
        ...paragraphObj,
        ...getTypeOfObject(paragraph),
      };

      /** @name 段落的行数组 */
      const rowList = [...Array(paragraphObj.rows)].map((_, index) => <li key={index} style={{ width: getParagraphWidth(index, paragraphObj) }} />);

      return <ul className={paragraphCls}>{rowList}</ul>;
    }
  };

  /** @name 根元素class */
  const rootCls = classnames(prefixCls, className, {
    [`${prefixCls}--round`]: round,
    [`${prefixCls}--active`]: active,
  });

  /** @name 内容class */
  const contentCls = classnames(`${prefixCls}--content`);

  /**
   * 骨架屏展示
   * @param {void} 无
   * @return {render} 展示内容
   */
  const skeletonContent = () => {
    if (show) {
      return (
        <div className={rootCls}>
          {avatarContent()}
          <div className={contentCls}>
            {titleContent()}
            {paragraphContent()}
          </div>
        </div>
      );
    }
    return children;
  };

  return <>{skeletonContent()}</>;
};

Skeleton.propTypes = {
  paragraph: PropTypes.oneOfType([PropTypes.bool, PropTypes.Object]), // 是否显示段落占位图
  title: PropTypes.bool, // 是否显示标题占位图
  avatar: PropTypes.oneOfType([PropTypes.bool, PropTypes.Object]), // 是否显示头像占位图
  show: PropTypes.bool, // 是否显示骨架屏,传 false 时会展示子组件内容
  active: PropTypes.bool, // 是否开启动画
  round: PropTypes.bool, // 是否将标题和段落显示为圆角风格
};

Skeleton.defaultProps = {
  paragraph: true,
  title: true,
  avatar: false,
  show: true,
  active: false,
  round: false,
};

export default Skeleton;

4.2.2 页面引用

style.less

.fly-skeleton {
  &__content {
    padding: 15px;
    background: #fff;
    min-height: 100vh;
    padding-bottom: 50px;
  }
  &__children {
    display: flex;
    align-items: flex-start;
    line-height: 20px;
    font-size: 14px;
    margin-top: 10px;
    &--avatar {
      margin-right: 15px;
      width: 30px;
      height: 30px;
      border-radius: 50%;
      overflow: hidden;
      img {
        display: block;
        width: 100%;
        height: 100%;
      }
    }
    &--title {
      margin-top: 8px;
    }
    &--desc {
      margin-top: 20px;
    }
  }
  &__button {
    text-align: center;
    width: 100px;
    padding: 0 10px;
    font-size: 14px;
    line-height: 24px;
    border-radius: 5px;
    color: #fff;
    background-color: #45b7f5;
  }
}

skeleton-test.jsx

/**
 * @description Skeleton 骨架屏 使用展示
 * @author 叶一一
 */
import React, { useEffect, useState } from 'react';
import { Skeleton, Switch } from 'fly';
import './style.less';

const SkeletonTest = () => {
  const title = 'Skeleton 骨架屏'; // 标题
  const [skeletonShow, setSkeletonShow] = useState(true);

  useEffect(() => {
    // 设置标题
    document.title = title;
  }, []);

  // 子组件内容展示开关控制方法
  const onChangeSwitch = () => {
    setSkeletonShow(!skeletonShow);
  };

  return (
    <div className='fly-skeleton__content'>
      <div className='text-xxxl mb20'>{title}</div>
      <div className='mb10'>基础用法</div>
      <Skeleton />
      <div className='mb10 mt10'>显示头像</div>
      <div className='text-sm text-darkgray mt5 mb10'>设置avatar属性,可以展示头像。</div>
      <Skeleton avatar />
      <div className='mb10 mt10'>动画效果</div>
      <div className='text-sm text-darkgray mt5 mb10'>设置active属性,可以有动画效果。</div>
      <Skeleton avatar active />
      <div className='mb10 mt10'>展示子组件内容</div>
      <div className='text-sm text-darkgray mt5 mb10'>设置show属性为false时,可以展示Skeleton的子组件。</div>
      {/* <Switch value={!skeletonShow} onChange={onChangeSwitch} /> */}
      <div onClick={onChangeSwitch} className='fly-skeleton__button'>
        控制展示
      </div>
      <Skeleton avatar active show={skeletonShow} className='mt10'>
        <div className='fly-skeleton__children'>
          <div className='fly-skeleton__children--avatar'>
            <img src='https://sf3-ttcdn-tos.pstatp.com/img/user-avatar/c6c1a335a3b48adc43e011dd21bfdc60~300x300.image' />
          </div>
          <div className='fly-skeleton__children--content'>
            <div className='fly-skeleton__children--title'>这是标题</div>
            <div className='fly-skeleton__children--desc'>
              这是段落111111111111111111111
              <br />
              这是段落222222222
            </div>
          </div>
        </div>
      </Skeleton>
      <div className='mb10 mt10'>头像样式</div>
      <div className='text-sm text-darkgray mt5 mb10'>设置avatar属性中size属性的值,可以调整头像大小,设置shape的值可以调整头像形状。</div>
      <Skeleton avatar={{ size: 50, shape: 'square' }} />
      <div className='mb10 mt10'>设置标题的宽度</div>
      <div className='text-sm text-darkgray mt5 mb10'>设置title属性中width属性的值,可以调整标题的宽度,值可以是百分比、具体像素值、具体数值,比如:70%、150px、150。</div>
      <Skeleton title={{ width: 150 }} />
      <div className='mb10 mt10'>标题和段落圆角</div>
      <div className='text-sm text-darkgray mt5 mb10'>设置round属性,可以调整段落和标题为圆角。</div>
      <Skeleton avatar round />
      <div className='mb10 mt10'>多行段落</div>
      <div className='text-sm text-darkgray mt5 mb10'>设置paragraph属性中rows属性的值为具体数值,可以调整段落的行数。</div>
      <Skeleton avatar paragraph={{ rows: 4 }} />
      <div className='mb10 mt10'>段落宽度可设置</div>
      <div className='text-sm text-darkgray mt5 mb10'>设置paragraph属性中width属性的值为数组值,可以调整每行段落的宽度。</div>
      <Skeleton avatar paragraph={{ rows: 4, width: ['90%', '90%', '90%', '60%'] }} />
    </div>
  );
};
export default SkeletonTest;

五、总结

整个流程下来,我们就实现了一个基础的骨架屏组件,对骨架屏也有了系统的了解,如果自己尝试去实现也会有思路怎么做。

但是,这只是实现了基础功能,并不完善,比如不支持头像、按钮等元素的单独使用。我在完成之后,去antd的官网看了一下它的源码,发现antd的功能做的更加完善,不愧是大厂的项目。从antd处获取的灵感,也让我完善了一下我的代码。所以,我们可以在日常的空闲时间,看一些大厂的源码,他们的功能更加强大、考虑问题更全面、实现思路也更优秀。

开头难吗?有时候挺难的,但是有了这个困难且良好的开头,后面的事就变得简单且顺利了,组件开发也会自然而然,水到渠成。所以,诸君加油。

再次感谢所有的开源项目,可以让像我一样的学习者获取技术上的进步。