超快超简洁的[骨架屏|Skeleton]方案;骨架屏与实际内容的一致性也相当高!

339 阅读6分钟

还在为客户端渲染等待网络请求时loading状态不友好发愁; 来吧, 这一款超简洁的方案是一个不错的选择!而且,Vue、React、Angular、JQuery、原生JavaScript都能用.

1.主流方案分析

温馨提示: 需要仔细评比的可以跳转链接进去看看, 但是如果只是想找一个好的方案, 我的建议是跳过我列出的文章,嘿嘿, 当然,也可以直接跳过第一章啦.

1.1 组件库支持

现在主流的组件库, 一般都有骨架屏组件; 比如

  1. 骨架屏 Skeleton - Ant Design
  2. Skeleton 骨架屏 | Element Plus (element-plus.org)
  3. Skeleton 骨架屏 - Vant 4 (vant-ui.github.io)
  4. ......

1.2 微信小程序支持

如果是微信小程序, 微信开发者工具还支持为页面一键生成骨架图(nice!),但是主要对使用原生小程序开发的人友好, 如果你使用了第三方框架(uniapp,taro等),那想要集成到项目中, 还是有不小的适配工作的!这有一篇作此努力的文章: javascript - 正确使用uniapp搭配微信开发者工具自带的骨架屏功能,生成骨架屏 - 个人文章 - SegmentFault 思否,当然,如果你仔细搜索, 还有很多类似的文章

1.3 其他文章

也还有许多其他的骨架屏的文章啦

  1. uniapp的骨架屏生成指南我们常把骨架屏一般用于页面在请求远程数据尚未完成时,页面用灰色块预显示本来的页面结构,给用户 - 掘金 (juejin.cn)

1.4 结论

基本上主流的组件库都提供了骨架屏组件;而且相信每一个只有一点前端基础的人, 都能够自己写一个骨架屏组件(几个灰色的框框,谁不会呢?),也能在项目中做好骨架屏适配工作!

但是,我想说的是,他们都过于复杂了, 让开发者不但需要根据需求开发, 仅仅是骨架屏的适配,就会导致一大部分的工作量, 比如:

  1. 需要去定义每一个骨架块的宽高,
  2. 需要逐个去控制骨架块的显示和隐藏

这些需要耦合在具体的业务代码中, 后期维护成本也会增加!

所以, 当产品提出要做骨架屏优化的时候, 我是无论如何都无法接受这些方案的

所以, 就有了下面提出的方案.

2.方案介绍

2.0 效果展示

Stackblitz 示例

2.1 原理

  1. 编写业务的时候, 主要的布局我们已经实现, 为什么不直接使用业务代码的布局, 而是为骨架屏再写一份新的布局呢? 所以骨架屏必须在业务代码的布局之上!
  2. 布局存在很多静态的内容区域, 这部分在加载中时可直接保持不变, 我们仅需要处理动态从后端获取数据的dom元素
  3. 页面上存在很多元素,但其实他们都共属于同一个接口, 应该由同个loading状态管理

2.2 实现和框架

基于以上考量, 开始准备实现部分, 方式也存在很多种, 比如

  1. React的高阶组件
  2. Vue的插槽

最后采用了最简单的方式, 只用到了css(具体使用时需要辅以具体框架,或者原生dom的支持),做到与框架无关, 可和任意框架集成!

2.3 实现步骤

2.3.1. 定义公共样式,这里为骨架屏的样式, 需要全局引入. 实现三个基础的样式

注意: 这里的样式使用了scss写法, 没有集成的自行换成css的, 这里的像素单位rpx源自于小程序,不是小程序的自行换成px,需要num/2

  • skeleton 默认矩形骨架屏(距离容器存在12rpx边距,避免多个骨架屏连在一起)
  • skeleton-circle 圆形骨架屏
  • skeleton-full 矩形骨架屏, 铺满整个容器
  • ... 根据业务需要, 可以自行添加别样式
  :root {
    /* 骨架屏变量 */
    --skeleton-color: #e0e0e0; /* 骨架屏背景色 */
    --skeleton-highlight: rgba(255, 255, 255, 0.8); /* 高亮效果色 */
    --skeleton-animation-duration: 1.5s; /* 动画时长 */
  }

  /* 骨架屏基础类 */
  .skeleton-full {
    position: relative;
    overflow: hidden;
  }

  /* 文本遮盖解决方案 */
  ._isloading .skeleton-full::before {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 2;
    border-radius: inherit;
    background-color: var(--skeleton-color);
  }

  ._isloading .skeleton-full::after {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 3;
    transform: translateX(-100%);
    animation: skeleton-shimmer var(--skeleton-animation-duration) infinite;
    border-radius: inherit;
    background: linear-gradient(
      90deg,
      rgba(255, 255, 255, 0) 0%,
      var(--skeleton-highlight) 50%,
      rgba(255, 255, 255, 0) 100%
    );
  }

  .skeleton-full > * {
    transition: opacity 0.3s ease;
  }
  /* 隐藏实际内容 */
  ._isloading .skeleton-full > * {
    opacity: 0;
  }

  /* skeleton - 默认带边距的矩形 */
  ._isloading .skeleton.skeleton-full::before {
    top: 12px;
    bottom: 12px;
    left: 12px;
    right: 12px;
  }

  ._isloading .skeleton.skeleton-full::after {
    top: 12px;
    bottom: 12px;
    left: 12px;
    right: 12px;
  }

  /* skeleton-circle - 圆形 */
  .skeleton-circle{
    border-radius: 50%;
  }
  ._isloading .skeleton-circle.skeleton-full::after {
    border-radius: 0%;
  }

  /*  - 全宽填充 */
  /* 默认已全宽填充,无需额外样式 */

  @keyframes skeleton-shimmer {
    100% {
      transform: translateX(100%);
    }
  }

以上就是骨架屏实现的最核心的代码了

2.3.2 调用(以vue为例,React等照搬就好)

<template>
  <div class="card" :class="{ _isLoading  }" :style="skeletonVars">
    <div class="card-header">
      <!-- 圆形骨架屏 -->
      <div class="avatar-container">
        <div class="avatar skeleton-full skeleton-circle">
          <img class="avatar" :src="user.avatar"></img>
        </div>
      </div>
      <div class="user-info">
        <!-- 全宽骨架屏 -->
        <h2 class="skeleton-full ">{{ user.name }}</h2>
        <!-- 带边距骨架屏 -->
        <p class="skeleton-full">{{ user.title }}</p>
      </div>
    </div>

    <!-- 全宽骨架屏 -->
    <div class="content skeleton-full skeleton">
      <p>{{ user.bio }}</p>
    </div>

    <div class="stats">
      <div class="stat-item skeleton-full skeleton">
        <div class="number">{{ stats.posts }}</div>
        <div class="label">文章</div>
      </div>
      <div class="stat-item skeleton-full skeleton">
        <div class="number">{{ stats.followers }}</div>
        <div class="label">粉丝</div>
      </div>
      <div class="stat-item skeleton-full skeleton">
        <div class="number">{{ stats.following }}</div>
        <div class="label">关注</div>
      </div>
    </div>

    <div class="actions">
      <button class="btn btn-primary skeleton-full">关注</button>
      <button class="btn btn-outline skeleton-full">发消息</button>
    </div>
  </div>
</template>

<script setup lang="ts">
import { onMounted, ref } from 'vue'
const emits = defineEmits(['item-click'])
// 模拟用户数据
const user = ref({
  name: '张明',
  title: '前端架构师 | Vue专家',
  bio: '拥有8年前端开发经验,专注于Vue生态和性能优化。热爱开源,曾参与多个知名开源项目。目前致力于打造更高效的前端工作流和组件体系。',
  avatar: 'https://randomuser.me/api/portraits/men/32.jpg'
})

// 模拟统计数据
const stats = ref({
  posts: 128,
  followers: 2456,
  following: 342
})
const _isloading = ref(true)
onMounted(() => {
  setTimeout(() => {
    _isloading.value = false
  }, 1000)
})
</script>

loading状态管理的核心在于管理上级元素的_isloading类名, 具体怎么给某一个节点动态添加一个class,相信难不倒各位 具体说明

  1. 在动态绑值的元素上添加骨架屏类名skeleton|skeleton-circle|skeleton-full,这里根据布局需要选择合适的类
  2. 在动态绑值的元素的上级元素根据数据请求的时机动态绑定_isloading, 这里可以根据绑定的数据是来自于什么接口, 动态规划_isloading应该方式的位置!

2.4 总结和注意事项

  1. 首先全局定义骨架屏的公共样式skeleton|skeleton-circle|skeleton-full, 包含在_isloading这个类名下, 所以我们即使在代码上添加对应的骨架类,骨架的效果默认是不会显示的
  2. 根据数据请求状态, 请求中时, 给对应的元素的上级元素添加_isloading, 并在请求结束时去掉_isloading, 就完成了骨架屏的显示和隐藏.
  3. 注意事项: 上级元素是任意的,可以根据字段对应的接口去规划, 如果上级元素(会存在很多级别,直到根节点)绑定了多个_isloading, 只要有一个_isloading绑上了, 就会呈现为骨架屏, 所以合理规划好_isloading加在什么位置,针对某一个骨架块, 只有一个上级元素绑定_isloading即可

教程到此结束, 赶紧去试试效果

3.0 Demo

Stackblitz 示例