还在为客户端渲染等待网络请求时loading状态不友好发愁; 来吧, 这一款超简洁的方案是一个不错的选择!而且,Vue、React、Angular、JQuery、原生JavaScript都能用.
1.主流方案分析
温馨提示: 需要仔细评比的可以跳转链接进去看看, 但是如果只是想找一个好的方案, 我的建议是跳过我列出的文章,嘿嘿, 当然,也可以直接跳过第一章啦.
1.1 组件库支持
现在主流的组件库, 一般都有骨架屏组件; 比如
- 骨架屏 Skeleton - Ant Design
- Skeleton 骨架屏 | Element Plus (element-plus.org)
- Skeleton 骨架屏 - Vant 4 (vant-ui.github.io)
- ......
1.2 微信小程序支持
如果是微信小程序, 微信开发者工具还支持为页面一键生成骨架图(nice!),但是主要对使用原生小程序开发的人友好, 如果你使用了第三方框架(uniapp,taro等),那想要集成到项目中, 还是有不小的适配工作的!这有一篇作此努力的文章: javascript - 正确使用uniapp搭配微信开发者工具自带的骨架屏功能,生成骨架屏 - 个人文章 - SegmentFault 思否,当然,如果你仔细搜索, 还有很多类似的文章
1.3 其他文章
也还有许多其他的骨架屏的文章啦
1.4 结论
基本上主流的组件库都提供了骨架屏组件;而且相信每一个只有一点前端基础的人, 都能够自己写一个骨架屏组件(几个灰色的框框,谁不会呢?),也能在项目中做好骨架屏适配工作!
但是,我想说的是,他们都过于复杂了, 让开发者不但需要根据需求开发, 仅仅是骨架屏的适配,就会导致一大部分的工作量, 比如:
- 需要去定义每一个骨架块的宽高,
- 需要逐个去控制骨架块的显示和隐藏
这些需要耦合在具体的业务代码中, 后期维护成本也会增加!
所以, 当产品提出要做骨架屏优化的时候, 我是无论如何都无法接受这些方案的
所以, 就有了下面提出的方案.
2.方案介绍
2.0 效果展示
2.1 原理
- 编写业务的时候, 主要的布局我们已经实现, 为什么不直接使用业务代码的布局, 而是为骨架屏再写一份新的布局呢? 所以骨架屏必须在业务代码的布局之上!
- 布局存在很多静态的内容区域, 这部分在加载中时可直接保持不变, 我们仅需要处理动态从后端获取数据的dom元素
- 页面上存在很多元素,但其实他们都共属于同一个接口, 应该由同个loading状态管理
2.2 实现和框架
基于以上考量, 开始准备实现部分, 方式也存在很多种, 比如
- React的高阶组件
- 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,相信难不倒各位
具体说明
- 在动态绑值的元素上添加骨架屏类名
skeleton|skeleton-circle|skeleton-full,这里根据布局需要选择合适的类 - 在动态绑值的元素的上级元素根据数据请求的时机动态绑定
_isloading类, 这里可以根据绑定的数据是来自于什么接口, 动态规划_isloading应该方式的位置!
2.4 总结和注意事项
- 首先全局定义骨架屏的公共样式
skeleton|skeleton-circle|skeleton-full, 包含在_isloading这个类名下, 所以我们即使在代码上添加对应的骨架类,骨架的效果默认是不会显示的 - 根据数据请求状态, 请求中时, 给对应的元素的
上级元素添加_isloading, 并在请求结束时去掉_isloading, 就完成了骨架屏的显示和隐藏. - 注意事项: 上级元素是任意的,可以根据字段对应的接口去规划, 如果上级元素(会存在很多级别,直到根节点)绑定了多个
_isloading, 只要有一个_isloading绑上了, 就会呈现为骨架屏, 所以合理规划好_isloading加在什么位置,针对某一个骨架块, 只有一个上级元素绑定_isloading即可
教程到此结束, 赶紧去试试效果