17 行 CSS 实现骨架屏 loading

3,152 阅读4分钟

前言

现代 UI 设计中,会使用各式各样的加载样式来减少用户的 loading 状态焦虑,最常见的如 转圈的小菊花虚假的进度条以及 炫酷的小动画。除了这三种最常见的 web 端加载,还有一种加载动画叫做骨架屏(Skeleton Screen),骨架屏的概念最早在 2014 年就被提出,最初在 Twitter 的工程团队的工程博客中提到过类似的技术,用来优化其移动端体验。

目前,这种加载动画已经遍地开花,几乎每个手机应用都在使用,我们对其似乎已经司空见惯,但是在 web 端却很少见到。

原因是在原生环境中,有很便捷的方式实现,如安卓端的 ShimmerSkeletonView可以快速高效的实现骨架屏。

在 web 端,以 element-plus (官方文档)为例 ,我们需要显示的写额外的标签和样式代码来实现。

image.png

image.png

微信小程序则提供了一键生成的功能,它会复制出一个完整的页面元素,并修改元素样式作为骨架屏。

这明显对代码有非常严重的侵入性或者冗余大量的加载样式代码。

那么有没有一种方法可以利用原本就有的元素实现呢,很遗憾目前没有专门的插件来做这样的事。

线索

以小程序生成骨架屏的方式为例,我发现只要将一些特定的元素的样式修改,就可以达到差不多的效果。

那么我只需为常用标签添加一个渐变背景色并通过背景大小和背景位置模拟加载的动画效果并将他的子元素全部设置为不可见并为它加一个最小高度岂不是就可以实现一个最简单的骨架屏了

ok,理论成型开始实践。

纯 CSS 实现

*[loading='true'] > div:not([loading='true']) {
  background-image: linear-gradient(90deg, #f0f2f5 25%, #e6e8eb 37%, #f0f2f5 63%) !important;
  background-size: 400% 100% !important;
  animation: skeleton-loading 1.4s infinite ease !important;
  border: none !important;
  min-height: 30px;
}
*[loading='true'] > div:not([loading='true']) > * {
  display: none !important;
}
@keyframes skeleton-loading {
  0% {
    background-position: 100% 50%;
  }
  100% {
    background-position: 0 50%;
  }
}

什么你问我为什么上来就贴了几行代码?

因为这就是全部的实现!

使用时你只需要

    <div class="recommendGrid" :loading="true">
        <div class="recommendItem" v-for="item in recommendList">
             <!--...-->
        </div>
    </div>

.recommendItem 即会变成一个在加载的灰框,效果如下图

GIF 2024-12-25 15-09-08.gif

可见以这种方式实现的骨架屏并看不出有什么不妥,非常好用,在 Vue 中,loading 值可以使用响应式变量填充,以对应真实的加载状态。

    <div class="recommendGrid" :loading="isLoading">
        <div class="recommendItem" v-for="item in recommendList">
             <!--...-->
        </div>
    </div>

那么效果将是这样

GIF 2024-12-25 15-15-34.gif

这样写出来的骨架屏要比 element-plus 的方案更合理,因为它会首先继承你原有的元素样式,在加载结束后会最大程度的贴合你的实际元素。

虽然 element-plus 也提供了自定义样式选项,但是看起来就复杂很多了,我们需要对每个地方单独做加载样式。

image.png

好了,我们看到了这短短 17 行代码带来的惊喜效果。

可能还有不懂的同学,我这里简单讲解一下上面的 css 都在做什么。

  • *[loading='true']

此选择器(attribute 选择器)选择了 全部含有 loading 属性且值为 true 的元素,如<div loading='true'></div>

  • div:not([loading='true'])

此选择器(非选择器)选择了子元素中所有 loading 为 true 的元素,这主要是为了让骨架屏更有层级感:这样它会回到第一个选择器生效(*[loading='true']),在子元素上写 loading='true' 时会将子元素的子元素精细化的展示出来!

*[loading='true'] > div:not([loading='true']) {
  background-image: linear-gradient(90deg, #f0f2f5 25%, #e6e8eb 37%, #f0f2f5 63%) !important;
  background-size: 400% 100% !important;
  animation: skeleton-loading 1.4s infinite ease !important;
  border: none !important;
  min-height: 30px;
}

这里为其设置了一个宽度 400% 的背景颜色,并为中间部分添加了一抹不一样的颜色

*[loading='true'] > div:not([loading='true']) > * {
  display: none !important;
}

将其子元素设置为不可见,否则子元素会覆盖在背景上。

@keyframes skeleton-loading {
  0% {
    background-position: 100% 50%;
  }
  100% {
    background-position: 0 50%;
  }
}

最后使用关键帧动画移动他的背景位置,达到 扫光的效果

结语

很多高大上的效果看起来是件麻烦事,但是真的动手去做了,动脑去想了,才发现也不过如此。谨以此篇简单小文与君共勉。

番外

image.png

作者德莱厄斯正在参加 2024 年度创作者排行榜,需要你的助力!

activity.juejin.cn/rank/2024/w…

点击链接👆立即投票