「原生练手」✨如何实现骨架屏效果?

2,721 阅读4分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第18天,点击查看活动详情

今天我们来用原生js实现一个骨架屏的效果,效果如下:

骨架屏.gif

首先思考如何实现

思考实现方式

骨架屏的原理是在数据没加载出来的时候,使用滚动的背景颜色去替代,等到加载完毕后则显示对应内容

那么我们的核心就是实现一个.skeleton的样式,当这个样式出现的时候,就通过animation去开启一个背景色无限滚动的动画,数据加载完毕后则将这个类名去除即可

思路还是比较简单的,我们先搭建一个整体结构,将数据都写死看看效果单的,我们先搭建一个整体结构,将数据都写死看看效果e单的,我们先搭建一个整体结构,将数据都写死看看效果单的,我们先搭建一个整体结构,将数据都写死看看效果e先

静态结构

<div class="card">
  <!-- 存放图片 -->
  <header>
    <img
      src="https://unsplash.com/photos/EaB4Ml7C7fE/download?ixid=MnwxMjA3fDB8MXxzZWFyY2h8MTN8fGNvZGV8ZW58MHx8fHwxNjYwMzQwOTQ4&force=true"
      alt=""
    />
  </header>
  <main>
    <h3>I love coding!</h3>
    <p>Talk is cheap. Show me the code.</p>
    <section class="author">
      <div class="author-avatar">
        <img src="https://pixabay.com/get/g9ac59ef391ebfbe7b3303ed0278986a31783bedd580d5e097980ed990d9bf646b5dc44932b18fd633df1813686e4f809.png?attachment=" alt="" />
      </div>
      <div class="author-info">
        <!-- 名字 -->
        <strong>Plasticine</strong>
        <!-- 日期 -->
        <small>Aug 13, 2022</small>
      </div>
    </section>
  </main>
</div>

再写一些基础样式

img {
  height: 100%;
  width: 100%;
  object-fit: cover;
}

.card {
  width: 350px;
  border-radius: 10px;
  background-color: white;
  overflow: hidden;
  box-shadow: 5px 5px 10px 10px rgba(255, 255, 255, 0.2);
}

.card header {
  height: 200px;
}

.card main {
  padding: 30px;
}

.card main h3 {
  margin: 0;
}

.card main p {
  color: gray;
}

.author {
  display: flex;
  align-items: center;
  gap: 10px;
}

.author .author-avatar {
  height: 40px;
  width: 40px;
  border-radius: 50%;
  overflow: hidden;
}

.author .author-info {
  display: flex;
  flex-direction: column;
  gap: 5px;
  width: 100px;
}

.author .author-info small {
  color: gray;
}

现在的效果如下:

静态结构效果

骨架屏特效

现在就可以尝试添加骨架屏特效了,骨架屏特效本身就只是一个背景色向右流动的效果,所以我们需要一个渐变色背景,然后设置一个动画让背景色的background-position不断向右移动,就可以实现骨架屏的效果

对应的css代码如下:

.skeleton {
  background: linear-gradient(
    to right,
    #f6f7f8 0%,
    #edeef1 10%,
    #f6f7f8 20%,
    #f6f7f8 100%
  );
  background-size: 200% 100%;
  animation: flow 1s linear infinite;
}

@keyframes flow {
  0% {
    background-position: 50% 0;
  }

  100% {
    background-position: -150% 0;
  }
}

那么有了这个骨架屏特效的代码,我们还需要看看其效果是否真的和我们预期中一样呢?

可以先把html中的内容注释掉,只保留框架部分,模拟一下数据还没加载时候的效果,然后再在需要应用骨架屏特效的地方加上.skeleton类名

我们要应用骨架屏特效的地方有背景图片、卡片标题、卡片内容、作者头像、作者姓名、留言日期,所以在这些地方加上.skeleton类名

<div class="card">
  <!-- 存放图片 -->
  <header class="skeleton">
    <!-- <img
      src="https://unsplash.com/photos/EaB4Ml7C7fE/download?ixid=MnwxMjA3fDB8MXxzZWFyY2h8MTN8fGNvZGV8ZW58MHx8fHwxNjYwMzQwOTQ4&force=true"
      alt=""
    /> -->
  </header>
  <main>
    <h3 class="skeleton">
      <!-- I love coding! -->
    </h3>
    <p class="skeleton">
      <!-- Talk is cheap. Show me the code. -->
    </p>
    <section class="author">
      <div class="author-avatar skeleton">
        <!-- <img
          src="https://pixabay.com/get/g9ac59ef391ebfbe7b3303ed0278986a31783bedd580d5e097980ed990d9bf646b5dc44932b18fd633df1813686e4f809.png?attachment="
          alt=""
        /> -->
      </div>
      <div class="author-info">
        <!-- 名字 -->
        <strong class="skeleton">
          <!-- Plasticine -->
        </strong>
        <!-- 日期 -->
        <small class="skeleton">
          <!-- Aug 13, 2022 -->
        </small>
      </div>
    </section>
  </main>
</div>

再来看看效果:

骨架屏bug.gif

设置文本元素占位符作为骨架屏填充

咦?生效是生效了,但是只有头部背景图和作者头像有效果,而文字部分全都没效果了,这是为啥呢?

这是因为文本元素中没有文本的时候,它没有自己的宽高,那设置background属性自然也是不会生效的,所以我们需要给它添加一个用于占位的元素,只要有一个字符,就能够充满当前行了,这里我们就填充一个&nbsp;空格占位符吧

<div class="card">
  <!-- 存放图片 -->
  <header class="skeleton">
    <!-- <img
      src="https://unsplash.com/photos/EaB4Ml7C7fE/download?ixid=MnwxMjA3fDB8MXxzZWFyY2h8MTN8fGNvZGV8ZW58MHx8fHwxNjYwMzQwOTQ4&force=true"
      alt=""
    /> -->
  </header>
  <main>
    <h3 class="skeleton">
      &nbsp;
      <!-- I love coding! -->
    </h3>
    <p class="skeleton">
      &nbsp;
      <!-- Talk is cheap. Show me the code. -->
    </p>
    <section class="author">
      <div class="author-avatar skeleton">
        <!-- <img
          src="https://pixabay.com/get/g9ac59ef391ebfbe7b3303ed0278986a31783bedd580d5e097980ed990d9bf646b5dc44932b18fd633df1813686e4f809.png?attachment="
          alt=""
        /> -->
      </div>
      <div class="author-info">
        <!-- 名字 -->
        <strong class="skeleton">
          &nbsp;
          <!-- Plasticine -->
        </strong>
        <!-- 日期 -->
        <small class="skeleton">
          &nbsp;
          <!-- Aug 13, 2022 -->
        </small>
      </div>
    </section>
  </main>
</div>

现在的效果如下:

骨架屏.gif

可以看到这样就行了,那么接下来我们就通过js去模拟数据加载,加载完成后,将数据插入到对应元素中,并将.skeleton样式去除

js模拟数据加载效果

为了方便js获取对应元素,我们给应用了骨架屏特效的元素起一个语义化的id

<div class="card">
  <!-- 头部图片 -->
  <header class="skeleton" id="header-img-container">
    <!-- <img
      src="https://unsplash.com/photos/EaB4Ml7C7fE/download?ixid=MnwxMjA3fDB8MXxzZWFyY2h8MTN8fGNvZGV8ZW58MHx8fHwxNjYwMzQwOTQ4&force=true"
      alt=""
    /> -->
  </header>
  <main>
    <h3 class="skeleton" id="card-title">
      &nbsp;
      <!-- I love coding! -->
    </h3>
    <p class="skeleton" id="card-content">
      &nbsp;
      <!-- Talk is cheap. Show me the code. -->
    </p>
    <section class="author">
      <div class="author-avatar skeleton" id="card-author-avatar-container">
        <!-- <img
          src="https://pixabay.com/get/g9ac59ef391ebfbe7b3303ed0278986a31783bedd580d5e097980ed990d9bf646b5dc44932b18fd633df1813686e4f809.png?attachment="
          alt=""
        /> -->
      </div>
      <div class="author-info">
        <!-- 名字 -->
        <strong class="skeleton" id="card-author-name">
          &nbsp;
          <!-- Plasticine -->
        </strong>
        <!-- 日期 -->
        <small class="skeleton" id="card-author-date">
          &nbsp;
          <!-- Aug 13, 2022 -->
        </small>
      </div>
    </section>
  </main>
</div>

现在就可以用js去模拟数据加载效果啦

(() => {
  const skeletonEls = {
    oHeaderImgContainer: document.getElementById("header-img-container"),
    oCardTitle: document.getElementById("card-title"),
    oCardContent: document.getElementById("card-content"),
    oCardAuthorAvatarContainer: document.getElementById(
      "card-author-avatar-container"
    ),
    oCardAuthorName: document.getElementById("card-author-name"),
    oCardAuthorDate: document.getElementById("card-author-date"),
  };

  const init = () => {
    const fetchData = () => {
      setTimeout(() => {
        const data = {
          headerImg: `
            <img
              src="https://unsplash.com/photos/EaB4Ml7C7fE/download?ixid=MnwxMjA3fDB8MXxzZWFyY2h8MTN8fGNvZGV8ZW58MHx8fHwxNjYwMzQwOTQ4&force=true"
              alt=""
            />
          `,
          cardTitle: "I love coding!",
          cardContent: "Talk is cheap. Show me the code.",
          cardAuthorAvatar: `
            <img
              src="https://pixabay.com/get/g9ac59ef391ebfbe7b3303ed0278986a31783bedd580d5e097980ed990d9bf646b5dc44932b18fd633df1813686e4f809.png?attachment="
              alt=""
            />
          `,
          cardAuthorName: "Plasticine",
          cardAuthorDate: "Aug 13, 2022",
        };

        // 插入加载到的数据
        skeletonEls.oHeaderImgContainer.innerHTML = data.headerImg.trim();
        skeletonEls.oCardTitle.innerHTML = data.cardTitle;
        skeletonEls.oCardContent.innerHTML = data.oCardContent;
        skeletonEls.oCardAuthorAvatarContainer.innerHTML =
          data.cardAuthorAvatar.trim();
        skeletonEls.oCardAuthorName.innerHTML = data.cardAuthorName;
        skeletonEls.oCardAuthorDate.innerHTML = data.cardAuthorDate;

        // 移除 `.skeleton` 类名从而 移除骨架屏特效
        for (const el of Object.values(skeletonEls)) {
          el.classList.remove("skeleton");
        }
      }, 3000);
    };

    fetchData();
  };

  init();
})();

最终效果就像开头中的那样,大功告成!