Element 骨架屏 Skeleton 组件实战指南:从占位渲染、动画到防抖动切换的完整解析
【免费下载链接】elementA Vue.js 2.0 UI Toolkit for Web项目地址: https://gitcode.com/gh_mirrors/eleme/element
骨架屏(Skeleton)是页面在等待数据加载时展示的一组灰色占位块,用于模拟真实内容的轮廓,让用户感知页面正在"成型",相比整屏 Loading 转圈视觉体验更平滑、更接近最终界面。本文以 Element 组件库(Vue.js 2.0 UI Toolkit)官方文档为核心,结合仓库中 Skeleton 组件源码 与 SkeletonItem 组件源码,系统讲解el-skeleton与el-skeleton-item的全部用法:基础占位、段落行数、动画、自定义模板、Loading 切换、列表批量渲染与节流防抖,并深入到源码层面解析其渲染与状态切换原理。读完本文,你将能在自己的 Vue 2 项目中直接落地一套可复用的骨架屏方案,并理解每个参数背后的实现逻辑。
组件适用场景
在需要等待加载内容的位置放置骨架屏,是当前主流前端应用的首选加载方案。与el-loading(Loading 组件)相比,骨架屏展示的是与最终内容结构一致的灰色占位,而不是覆盖层的旋转指示器,因此在页面首屏、列表加载、卡片流式展示等场景下视觉效果更好、用户等待焦虑感更低。Element 将骨架屏拆分为两个组件:
el-skeleton:整体容器,负责占位与真实内容的切换;el-skeleton-item:单个占位单元,负责具体形状(段落、标题、图片、圆形、按钮等)。
两者在仓库中分别注册于 packages/skeleton/index.js 与 packages/skeleton-item/index.js,并统一由 src/index.js 中Skeleton、SkeletonItem导出供全局安装。
基础用法
最简单的骨架屏只需一行代码,默认渲染 4 行段落占位:
<template> <el-skeleton /> </template>从源码 index.vue 可以看出,未提供任何自定义模板时,组件会基于rows属性循环渲染el-skeleton-item,每一行的variant均为p(段落),并对第一段与最后一段附加特殊类名is-first/is-last,用于控制首段宽度为 33%、末段宽度为 61%(见 skeleton-item.scss),形成错落有致的段落轮廓。
更多参数:控制段落数量
可以通过rows属性配置骨架屏段落数量,使其更接近真实内容的高度:
<el-skeleton :rows="6" />rows默认值为4(源码 index.vue),接受正整数。与默认渲染对应的样式规则定义在 skeleton.scss:每个段落高度 16px、上边距 16px、背景色为$--skeleton-color(默认#f2f2f2,见 var.scss)。单元测试 skeleton.spec.js 中should render x rows用例验证了rows: 5时页面渲染出 5 个.el-skeleton__p元素。
动画效果
为骨架屏添加流光扫过的动画,可以让用户感知"加载正在进行":
<el-skeleton :rows="6" animated />animated默认为false。开启后,容器会附加is-animated类(index.vue),样式层通过 skeleton.scss 中的skeleton-colormixin 为所有占位单元应用一段 90 度渐变的背景(浅灰#f2f2f2与更深灰#e6e6e6交替),配合el-skeleton-loading关键帧动画(1.4 秒、ease 缓动、无限循环)实现背景位置从右到左的平移扫光效果。测试用例should render with animation会断言容器是否包含is-animated类。
自定义样式模板
Element 提供的默认段落排版有时无法满足需求(如需要图片、头像、标题等混合布局),此时可通过具名插槽template自定义占位模板。每个占位单元使用el-skeleton-item并通过variant指定形状:
<template> <el-skeleton style="width: 240px"> <template slot="template"> <el-skeleton-item variant="image" style="width: 240px; height: 240px;" /> <div style="padding: 14px;"> <el-skeleton-item variant="p" style="width: 50%" /> <div style="display: flex; align-items: center; justify-items: space-between;" > <el-skeleton-item variant="text" style="margin-right: 16px;" /> <el-skeleton-item variant="text" style="width: 30%;" /> </div> </div> </template> </el-skeleton> </template>variant的可选值包括p / h1 / h3 / text / caption / button / image / circle / rect,默认text(源码 item.vue)。从实现看,item.vue 会为每个占位单元生成el-skeleton__item el-skeleton__{variant}类名,各形状的尺寸规则集中在 skeleton-item.scss:
text:高度为$--font-size-small,宽度 100%;caption:高度$--font-size-extra-small;h1/h3/h5:分别对应$--font-size-extra-large/$--font-size-large/$--font-size-medium的高度;button:高 40px、宽 64px、圆角 4px;circle:圆形,直径默认取$--avatar-medium-size,支持lg/md尺寸变体;image:不设固定宽高(由行内样式控制),内部居中渲染一个内置 SVG 占位图(见 img-placeholder.vue)。
官方建议:描述模板时尽量贴近真实 DOM 结构,避免因占位与真实内容高度差异过大而导致的页面抖动。
Loading 状态切换
数据加载结束后需要展示真实 UI,通过loading控制:为true时渲染骨架屏,为false时渲染默认插槽(default slot)中的真实内容。
<template> <div style="width: 240px"> <p> <label style="margin-right: 16px;">切换 Loading</label> <el-switch v-model="loading" /> </p> <el-skeleton style="width: 240px" :loading="loading" animated> <template slot="template"> <el-skeleton-item variant="image" style="width: 240px; height: 240px;" /> <div style="padding: 14px;"> <el-skeleton-item variant="h3" style="width: 50%;" /> <div style="display: flex; align-items: center; justify-items: space-between; margin-top: 16px; height: 16px;" > <el-skeleton-item variant="text" style="margin-right: 16px;" /> <el-skeleton-item variant="text" style="width: 30%;" /> </div> </div> </template> <template> <el-card :body-style="{ padding: '0px', marginBottom: '1px' }"> <img src="https://shadow.elemecdn.com/app/element/hamburger.9cf7b091-55e9-11e9-a976-7f4d0b07eef6.png" class="image" /> <div style="padding: 14px;"> <span>好吃的汉堡</span> <div class="bottom card-header"> <span class="time">{{ currentDate }}</span> <el-button type="text" class="button">操作按钮</el-button> </div> </div> </el-card> </template> </el-skeleton> </div> </template> <script> export default { data () { return { loading: true, currentDate: '2021-06-01' } }, } </script>loading默认值为true(index.vue)。注意组件的默认插槽(default)与template插槽是两个独立的出口:骨架阶段渲染template,真实阶段渲染 default,二者互不干扰。
渲染多条数据
列表场景下,用count属性控制一次性渲染多少条假数据:
<template> <div style="width: 400px"> <p> <el-button @click="setLoading">点我重新加载</el-button> </p> <el-skeleton style="width:400px" :loading="loading" animated :count="3"> <template slot="template"> <el-skeleton-item variant="image" style="width: 400px; height: 267px;" /> <div style="padding: 14px;"> <el-skeleton-item variant="h3" style="width: 50%;" /> <div style="display: flex; align-items: center; justify-items: space-between; margin-top: 16px; height: 16px;" > <el-skeleton-item variant="text" style="margin-right: 16px;" /> <el-skeleton-item variant="text" style="width: 30%;" /> </div> </div> </template> <template> <el-card :body-style="{ padding: '0px', marginBottom: '1px' }" v-for="item in lists" :key="item.name" > <img :src="item.imgUrl" class="image multi-content" /> <div style="padding: 14px;"> <span>{{ item.name }}</span> <div class="bottom card-header"> <span class="time">{{ currentDate }}</span> <el-button type="text" class="button">操作按钮</el-button> </div> </div> </el-card> </template> </el-skeleton> </div> </template> <script> export default { data() { return { loading: true, currentDate: '2021-06-01', lists: [], } }, mounted() { this.loading = false this.lists = [ { imgUrl: 'https://fuss10.elemecdn.com/a/3f/3302e58f9a181d2509f3dc0fa68b0jpeg.jpeg', name: '鹿', }, { imgUrl: 'https://fuss10.elemecdn.com/1/34/19aa98b1fcb2781c4fba33d850549jpeg.jpeg', name: '马', }, { imgUrl: 'https://fuss10.elemecdn.com/0/6f/e35ff375812e6b0020b6b4e8f9583jpeg.jpeg', name: '山狮', }, ] }, methods: { setLoading() { this.loading = true setTimeout(() => (this.loading = false), 2000) }, }, } </script>源码中count默认值为1(index.vue),渲染逻辑通过v-for="i in count"嵌套循环把整个模板重复 N 份(index.vue)。官方在此特别提示:请尽可能将count保持在最小状态——即使是假的 UI,DOM 元素过多同样会引起性能问题,并且骨架屏销毁时的耗时也会相应变长。测试用例should render x times验证了count从 1 增加到 2 时,段落数由 4 变为 8。
防止渲染抖动
当接口响应非常快、骨架屏刚刚渲染真实数据就已返回时,界面会突然"闪"一下。此时通过throttle延迟占位 DOM 的渲染,从根源上避免这种抖动:
<template> <div style="width: 240px"> <p> <label style="margin-right: 16px;">切换 Loading</label> <el-switch v-model="loading" /> </p> <el-skeleton style="width: 240px" :loading="loading" animated :throttle="500" > <template slot="template"> <el-skeleton-item variant="image" style="width: 240px; height: 240px;" /> <div style="padding: 14px;"> <el-skeleton-item variant="h3" style="width: 50%;" /> <div style="display: flex; align-items: center; justify-items: space-between; margin-top: 16px; height: 16px;" > <el-skeleton-item variant="text" style="margin-right: 16px;" /> <el-skeleton-item variant="text" style="width: 30%;" /> </div> </div> </template> <template> <el-card :body-style="{ padding: '0px', marginBottom: '1px'}"> <img src="https://shadow.elemecdn.com/app/element/hamburger.9cf7b091-55e9-11e9-a976-7f4d0b07eef6.png" class="image" /> <div style="padding: 14px;"> <span>好吃的汉堡</span> <div class="bottom card-header"> <span class="time">{{ currentDate }}</span> <el-button type="text" class="button">操作按钮</el-button> </div> </div> </el-card> </template> </el-skeleton> </div> </template> <script> export default { data() { return { loading: false, currentDate: '2021-06-01' } }, } </script>throttle的语义是"延迟占位 DOM 渲染的毫秒数",默认0。若接口在throttle时间内就返回了数据,骨架屏根本不会出现在页面上,自然也就不会产生闪烁。
源码解析:throttle 与状态切换的实现原理
理解throttle的行为,需要看 index.vue 中的实现。组件内部维护了一个独立的uiLoading状态(真正决定渲染骨架还是真实内容),它与外部loading属性的同步逻辑如下:
throttle <= 0时,uiLoading与loading完全同步,加载中即渲染骨架;throttle > 0时,初始uiLoading为false(index.vue),即先渲染真实内容区域;- 监听
loading变化:当loading变为true(进入加载态),先clearTimeout清掉未执行的定时器,再通过setTimeout延迟throttle毫秒后才把uiLoading置为true(渲染骨架);当loading变为false(加载完成),则立即把uiLoading置为false切回真实 UI。
也就是说,节流只作用于"骨架屏出现"这一侧,不作用于"骨架屏消失"——加载结束后真实内容会立即呈现,避免延迟空白。单元测试should throttle rendering(skeleton.spec.js)精确验证了这一时序:throttle: 500时初始uiLoading为false,等待 600ms 后变为true。
渲染骨架时,组件外层容器为el-skeleton(可附加is-animated),内部通过v-for="i in count"循环模板;若未提供template插槽,则按rows渲染默认段落(index.vue)。这一分支逻辑与测试中的render test(默认渲染 4 个段落)、should render template slots(自定义模板内容原样渲染)等用例一一对应。
样式与主题定制
骨架屏的视觉完全由主题变量驱动,可以在自定义主题时覆盖 var.scss 中两个变量来改变占位色:
$--skeleton-color: #f2f2f2:占位块基础色;$--skeleton-to-color: #e6e6e6:动画渐变的目标色。
动画由 skeleton.scss 中的el-skeleton-loading关键帧驱动:背景位置从100% 50%平移到0 50%,配合background-size: 400% 100%的 90 度线性渐变(25% 基础色 → 37% 渐变目标色 → 63% 基础色),形成从左向右的扫光效果。各variant的具体尺寸、圆角规则均在 skeleton-item.scss 中,如段落首段 33% 宽、末段 61% 宽、button高 40px 宽 64px、circle使用头像尺寸变量等。
TypeScript 类型声明
若在 TypeScript 项目中使用,types/skeleton.d.ts 提供了完整的类型约束:animated: boolean、count: number、loading: boolean、rows: number、throttle: number(毫秒),并声明了default(真实渲染 DOM)与template(自定义骨架模板)两个插槽的VNode[]类型;rows的注释也明确指出"仅在未提供 template 插槽时生效",与源码行为完全一致。
API 汇总
Skeleton Attributes
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|---|---|---|---|---|
| animated | 是否使用动画 | boolean | true / false | false |
| count | 渲染多少个 template, 建议使用尽可能小的数字 | number | integer | 1 |
| loading | 是否显示 skeleton 骨架屏 | boolean | true / false | true |
| rows | 骨架屏段落数量 | number | 正整数 | 4 |
| throttle | 延迟占位 DOM 渲染的时间, 单位是毫秒 | number | 正整数 | 0 |
Skeleton Item Attributes
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|---|---|---|---|---|
| variant | 当前显示的占位元素的样式 | Enum(string) | p / h1 / h3 / text / caption / button / image / circle / rect | text |
Skeleton Slots
| name | description |
|---|---|
| default | 用来展示真实 UI |
| template | 用来展示自定义占位符 |
最佳实践小结
结合官方文档提示与源码实现,给出几点实战建议:
- 优先使用默认段落:简单场景直接
:rows="n",配合animated即可获得与真实排版接近的效果; - 自定义模板贴近真实 DOM:使用
template插槽与variant组合时,尽量复刻真实卡片/列表的结构与尺寸,减少高度差造成的抖动; count宁小勿大:列表骨架按可视区域数量估算,避免一次性渲染过多占位 DOM 造成性能与销毁开销;- 合理设置
throttle:对本地缓存或接口较快的场景,设置 300~500ms 的节流可显著减少"闪一下"的体验问题;需注意节流只延迟骨架出现、不延迟真实内容展示; - 主题色定制:通过覆盖
$--skeleton-color与$--skeleton-to-color两个 SCSS 变量即可整体调整占位配色,与项目主题保持一致。
相关文件索引:组件实现、占位单元、图片占位、样式定义、单元测试、类型声明。
【免费下载链接】elementA Vue.js 2.0 UI Toolkit for Web项目地址: https://gitcode.com/gh_mirrors/eleme/element
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考