页面在获取数据之前通常会有一段空白期,用户面对纯白界面很容易产生卡顿或加载失败的错觉。骨架屏的作用就是在真实内容到达前,先渲染出与最终布局结构相似的灰色占位块,并配合微妙的闪烁动画,让用户感知到页面正在加载。借助JavaScript,我们可以灵活控制骨架屏的显示时机、生成方式和销毁逻辑,从而在不同规模的项目中优化加载体验。

一、纯CSS配合JS控制显隐的骨架屏
最基础的方案是利用CSS画出占位块,再用JavaScript控制一个容器在加载前后切换内容。这种方式不依赖任何框架,适合结构固定的静态页或活动页。我们通常把骨架结构的HTML写成普通节点,默认显示,当数据渲染完成后将其隐藏。
核心思路是:后端或前端模板先输出骨架DOM,JS监听数据请求状态,成功后再替换或隐藏骨架节点。下面是一段简单的实现,用setTimeout模拟请求,展示如何通过class控制显隐。
<div id="app">
<div class="skeleton" id="skeleton">
<div class="sk-line"></div>
<div class="sk-block"></div>
</div>
<div class="real-content" id="content" style="display:none;">
<h3>文章标题</h3>
<p>这是真实加载完成的内容</p>
</div>
</div>
// 模拟数据请求
function fetchData() {
return new Promise(function(resolve) {
setTimeout(function() {
resolve({ title: '文章标题', text: '这是真实加载完成的内容' });
}, 1500);
});
}
fetchData().then(function(data) {
var sk = document.getElementById('skeleton');
var ct = document.getElementById('content');
ct.querySelector('h3').innerText = data.title;
ct.querySelector('p').innerText = data.text;
sk.style.display = 'none';
ct.style.display = 'block';
});
对应的CSS只需要定义灰色背景与闪烁动画,代码量极小,也不会影响首屏渲染速度。这种方案的优点是零依赖、易理解;缺点也很明显,当页面结构复杂或需要多处复用时,手写大量骨架DOM会变得繁琐,且难以与真实布局保持完全一致。
二、组件化骨架屏方案
在Vue或React项目中,更推荐将骨架屏封装为组件,通过props控制显示状态,并结合路由或接口状态自动渲染。组件化让骨架结构可以跟随业务布局变化,也方便在多个页面之间复用。
以Vue为例,我们可以定义一个Skeleton组件,接收loading参数。父组件在created或onMounted阶段发起请求,将loading置为false时切换为真实内容。这样骨架屏与业务代码解耦,维护成本更低。
<template>
<div>
<Skeleton v-if="loading" :rows="3" />
<div v-else>
<h3>{{ article.title }}</h3>
<p>{{ article.text }}</p>
</div>
</div>
</template>
import Skeleton from './Skeleton.vue';
export default {
components: { Skeleton },
data() {
return { loading: true, article: {} };
},
mounted() {
setTimeout(() => {
this.article = { title: '组件化骨架屏', text: '内容加载完毕' };
this.loading = false;
}, 1200);
}
};
组件内部可以用循环生成占位行,并支持不同形状,例如圆形头像、矩形卡片等。相比纯CSS写法,组件化方案在多人协作的项目中优势突出:设计师调整布局后,只需同步修改Skeleton组件,不必在每个页面重复改动。
不过要注意,组件化骨架屏仍会在JS加载完成后才渲染,如果首屏JS包过大,用户依然会先看到白屏。因此常配合服务端渲染或预渲染,将骨架HTML直出到页面中,进一步缩短白屏时间。
三、请求拦截式自动骨架屏
对于加载时序复杂、接口众多的中后台系统,手动在每个页面写骨架状态很费力。请求拦截式方案是在全局HTTP层做文章:当某个页面的接口处于pending状态时,自动展示该页面对应的骨架屏,所有请求完成后自动消失。
实现上可以利用axios拦截器或fetch包装函数,维护一个正在请求的接口计数。配合路由钩子,在切换页面时根据接口状态决定显隐。下面用axios示意如何标记加载状态。
let pendingCount = 0;
const showSkeleton = () => {
document.getElementById('global-skeleton').style.display = 'block';
};
const hideSkeleton = () => {
if (pendingCount === 0) {
document.getElementById('global-skeleton').style.display = 'none';
}
};
axios.interceptors.request.use(config => {
pendingCount++;
showSkeleton();
return config;
});
axios.interceptors.response.use(res => {
pendingCount--;
hideSkeleton();
return res;
});
这种方案对业务侵入最小,不必在每个组件里写loading变量。但它要求骨架结构相对通用,难以针对每个页面精细定制。通常做法是为不同路由配置不同的全局骨架模板,在路由切换时替换对应DOM。
从性能角度看,拦截式骨架屏避免了重复的逻辑判断,也减少了组件级状态管理负担。在接口响应慢且串行请求多的场景下,它能稳定保证用户始终看到占位界面,而不是中途闪现空白。
四、三种方案的选择建议
小型营销页或一次性活动页,结构固定、上线周期短,用纯CSS加JS显隐最经济;团队使用Vue或React且页面复用度高,组件化骨架屏在可维护性上表现最好;如果系统接口繁杂、页面众多且希望统一加载体验,请求拦截式更适合。
实际项目中也可以组合使用,例如首屏用服务端直出的CSS骨架保证极速感知,进入路由后再由组件接管局部加载态。关键在于衡量开发成本与用户体验收益,避免为了骨架屏而引入过重逻辑。
| 方案 | 侵入性 | 复用性 | 适用场景 |
|---|---|---|---|
| 纯CSS+JS | 高(需手写DOM) | 低 | 静态页、活动页 |
| 组件化 | 中(组件解耦) | 高 | 框架项目中后台 |
| 请求拦截式 | 低(全局处理) | 中 | 多接口复杂系统 |
无论选择哪种方式,骨架屏的目标都是缩短用户感知上的等待时间。配合合理的动画时长与占位精度,才能真的让加载过程变得平滑而不突兀。
skeleton_screenJavaScriptloading_optimization修改时间:2026-08-04 22:09:41