Skeleton动画实现的那些坑我都帮你踩过了
先看效果,再看代码
说实话,Skeleton动画这个东西现在各大厂都在用,我也在好几个项目里实现了。最直观的感受就是用户体验确实提升了,至少用户不会看到一片空白在那里傻等。今天就把我用过的几种方案都分享出来,毕竟这个技术我已经用了一年多了。
先上一个最简单的基础版本:
<!DOCTYPE html>
<html>
<head>
<style>
.skeleton {
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: loading 1.5s infinite;
}
@keyframes loading {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
</style>
</head>
<body>
<div class="skeleton" style="width: 200px; height: 20px; margin-bottom: 10px;"></div>
<div class="skeleton" style="width: 150px; height: 20px; margin-bottom: 10px;"></div>
<div class="skeleton" style="width: 180px; height: 60px;"></div>
</body>
</html>
这段代码的核心就是那个渐变背景加关键帧动画。background-position从200%移动到-200%,就形成了那种扫描的效果。我第一次做的时候就在这里纠结了好久,后来发现其实很简单,就是不断地改变背景位置。
不同场景下的应用
实际项目中,Skeleton不是只有一种形态。我在做新闻列表的时候,需要模拟出标题、摘要、图片的占位效果:
.skeleton-news-title {
width: 100%;
height: 24px;
border-radius: 4px;
}
.skeleton-news-summary {
width: 100%;
height: 16px;
margin-top: 8px;
border-radius: 3px;
}
.skeleton-news-image {
width: 80px;
height: 60px;
border-radius: 4px;
}
.skeleton-container {
display: flex;
padding: 16px;
gap: 12px;
}
.skeleton-text-container {
flex: 1;
}
<div class="skeleton-container">
<div class="skeleton skeleton-news-image"></div>
<div class="skeleton-text-container">
<div class="skeleton skeleton-news-title"></div>
<div class="skeleton skeleton-news-summary"></div>
<div class="skeleton skeleton-news-summary" style="width: 80%;"></div>
</div>
</div>
电商项目的商品卡片稍微复杂一点,要考虑价格、销量这些信息的展示:
.skeleton-product-card {
width: 150px;
padding: 12px;
box-sizing: border-box;
}
.skeleton-product-image {
width: 100%;
height: 150px;
border-radius: 8px;
}
.skeleton-product-title {
width: 100%;
height: 14px;
margin-top: 12px;
border-radius: 2px;
}
.skeleton-product-price {
width: 60%;
height: 16px;
margin-top: 8px;
border-radius: 2px;
}
.skeleton-product-sales {
width: 40%;
height: 12px;
margin-top: 6px;
border-radius: 2px;
}
<div class="skeleton-product-card">
<div class="skeleton skeleton-product-image"></div>
<div class="skeleton skeleton-product-title"></div>
<div class="skeleton skeleton-product-title" style="width: 80%;"></div>
<div class="skeleton skeleton-product-price"></div>
<div class="skeleton skeleton-product-sales"></div>
</div>
踩坑提醒:这三点一定注意
说几个我踩过坑的地方。
第一个坑是性能问题。一开始我把所有的动画都放在DOM元素上,结果页面一多就开始卡顿。后来改为CSS变量控制动画速度,性能提升了不少:
.skeleton {
--loading-speed: 1.5s;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: loading var(--loading-speed) infinite;
}
@keyframes loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
第二个坑是在移动端的表现。iOS Safari有个奇怪的问题,background-position的动画有时候会卡住。我后来加上了transform3d强制开启硬件加速才解决:
.skeleton {
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: loading 1.5s infinite;
transform: translateZ(0); /* 强制硬件加速 */
}
@keyframes loading {
0% {
background-position: 200% 0;
transform: translateZ(0);
}
100% {
background-position: -200% 0;
transform: translateZ(0);
}
}
第三个坑是颜色选择。很多人随便选个灰色就用了,但实际上要考虑整体设计风格。我一般会取主色调的浅色版本,这样看起来不会突兀。比如主题色是#1890ff,我就用#e6f7ff作为骨架屏的背景色之一。
React组件封装实践
实际项目中肯定不能每次都写一堆div。我封装了一个React组件,用起来比较方便:
import React from 'react';
const Skeleton = ({
rows = 1,
width = '100%',
height = '16px',
circle = false,
className = '',
...props
}) => {
const renderRows = () => {
return Array.from({ length: rows }, (_, index) => (
<div
key={index}
className={skeleton ${className}}
style={{
width: circle ? height : width,
height,
borderRadius: circle ? '50%' : undefined,
marginBottom: index < rows - 1 ? '12px' : undefined
}}
/>
));
};
return <div {...props}>{renderRows()}</div>;
};
// 使用示例
const MyComponent = () => {
const [loading, setLoading] = useState(true);
return (
<div>
{loading ? (
<div>
<Skeleton rows={3} height="20px" />
<Skeleton width="60%" height="16px" style={{ marginTop: '10px' }} />
</div>
) : (
// 真实内容
)}
</div>
);
};
Vue版本也很简单
Vue的实现大同小异,主要是利用props传递参数:
<template>
<div v-if="loading" :class="wrapperClass">
<div
v-for="n in rows"
:key="n"
:class="['skeleton', className]"
:style="getRowStyle(n)"
/>
</div>
<slot v-else />
</template>
<script>
export default {
name: 'Skeleton',
props: {
loading: {
type: Boolean,
default: true
},
rows: {
type: Number,
default: 1
},
width: {
type: String,
default: '100%'
},
height: {
type: String,
default: '16px'
},
circle: {
type: Boolean,
default: false
},
className: String,
wrapperClass: String
},
methods: {
getRowStyle(index) {
const isLastRow = index === this.rows;
return {
width: this.circle ? this.height : this.width,
height: this.height,
borderRadius: this.circle ? '50%' : '4px',
marginBottom: !isLastRow ? '12px' : '0'
};
}
}
};
</script>
动态控制显隐的最佳时机
什么时候隐藏Skeleton是最合适的?我在实践中发现,不能等到所有数据都加载完成才切换,这样反而会让用户觉得卡顿。我的做法是分批显示,先显示主要的内容区域,再逐步显示细节。
// 模拟接口请求
const fetchData = async () => {
const response = await fetch('https://jztheme.com/api/content');
const data = await response.json();
// 设置一个延迟,让Skeleton多显示一会儿
setTimeout(() => {
setContent(data);
setLoading(false);
}, 300); // 至少显示300ms,避免闪烁
};
另外还要考虑网络特别快的情况,如果数据瞬间返回,Skeleton可能只闪了一下,用户体验反而不好。所以我会设置最小显示时间,保证动画能够完整播放一次。
这个技术的拓展用法还有很多,比如结合Intersection Observer实现懒加载的Skeleton、配合服务端渲染的首屏优化等等。后续会继续分享这类博客。
以上是我踩坑后的总结,希望对你有帮助。

暂无评论