Element UI Skeleton 骨架屏组件完全指南:从基础用法到源码级防闪烁优化
【免费下载链接】elementA Vue.js 2.0 UI Toolkit for Web项目地址: https://gitcode.com/gh_mirrors/eleme/element
Skeleton(骨架屏)是 Element UI 提供的加载占位组件,用于在数据请求尚未返回时,以接近真实界面的灰色占位结构替代空白页面,显著改善用户的等待体验。本文基于 Element UI 官方文档 examples/docs/en-US/skeleton.md 展开,并结合组件源码与样式实现,系统讲解el-skeleton与el-skeleton-item的完整用法、属性语义、插槽机制以及throttle防闪烁的底层原理,帮助你构建真正可用、性能友好且不抖动的加载骨架。
为什么需要骨架屏:Skeleton 的适用场景
当页面加载数据时,用户看到的往往是整片空白或单调的 spinner。Skeleton 组件在数据到达之前,用一组灰色的矩形、圆角块与图片占位符模拟真实 DOM 的轮廓,让用户预判内容结构,从而获得更丰富的视觉与交互体验。Element UI 的骨架屏由两个组件协作完成:
el-skeleton:外层容器,负责loading、count、rows、animated、throttle等全局状态;el-skeleton-item:单个骨架单元,通过variant指定矩形、圆形、按钮、图片等不同形态。
两个组件在源码中分别对应 packages/skeleton/src/index.vue 与 packages/skeleton/src/item.vue,并通过 packages/skeleton/index.js 以Vue.component的方式全局注册,组件名分别为ElSkeleton与ElSkeletonItem。
基础用法:一行代码开启骨架占位
最基本的骨架屏只需在模板中放置一个<el-skeleton />:
<template> <el-skeleton /> </template>不传任何属性时,组件使用默认值渲染:loading为true(显示骨架)、rows为4(渲染 4 行段落)、count为1(只渲染一组)。从 index.vue 的props定义可以看出,这些默认值均由源码直接声明:
props: { animated: { type: Boolean, default: false }, count: { type: Number, default: 1 }, rows: { type: Number, default: 4 }, loading: { type: Boolean, default: true }, throttle: { type: Number, default: 0 } }默认渲染的 4 行段落并非等宽:模板渲染逻辑(index.vue)会给首行添加is-first类、末行添加is-last类。对应样式 packages/theme-chalk/src/skeleton-item.scss 规定:
- 首行(
is-first)宽度为33%; - 末行(
is-last)宽度为61%; - 中间段落(
el-skeleton__paragraph)宽度为100%。
这种「首行短、末行次短、中间占满」的排布正是对真实段落文本行高参差的模拟,官方文档称之为「rendering a title row with 33% width of the others」(渲染一个宽度为其余行 33% 的标题行)。
可配置行数:用 rows 控制段落数量
当默认的 4 行段落不符合需求时,可以通过rows属性自定义:
<el-skeleton :rows="6" />rows只在未提供template插槽时生效(详见属性表),其类型为 number、默认值为4。注意rows渲染的是variant="p"的段落单元,并不影响下方将要介绍的el-skeleton-item自定义结构。
加载动画:用 animated 启用流光效果
默认骨架块是静态的灰块,而animated属性可以为其加上来回扫过的「流光」渐变动画:
<el-skeleton :rows="6" animated />当animated为true时,外层容器会追加is-animated类(见 index.vue)。动画的真正实现位于样式 packages/theme-chalk/src/skeleton.scss:通过skeleton-colormixin 生成 90 度线性渐变背景,背景尺寸放大为400% 100%,再利用el-skeleton-loading关键帧让背景位置从100% 50%平移到0 50%,以1.4s ease无限循环:
background: linear-gradient( 90deg, $--skeleton-color 25%, $--skeleton-to-color 37%, $--skeleton-color 63% ); background-size: 400% 100%; animation: #{$namespace}-skeleton-loading 1.4s ease infinite;两个渐变色变量$--skeleton-color与$--skeleton-to-color定义在 packages/theme-chalk/src/common/var.scss 中,可通过主题定制覆盖,这意味着你可以让骨架动画颜色跟随项目主题风格。
自定义模板:用 template 插槽 + variant 搭建真实结构
Element 内置的骨架模板只覆盖最常见场景,当页面结构复杂时,应使用template插槽自行拼装骨架,并结合不同variant的el-skeleton-item组合出最接近真实 UI 的占位结构:
<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>该示例用一张图片占位块(240×240)模拟封面图,再用p与text单元模拟标题和描述行,正是卡片类页面的常见骨架形态。官方文档特别强调了一个容易被忽略的实战要点:自定义骨架时应尽量让结构与真实 DOM 高度一致("structuring them as closer to the real DOM as possible"),以避免因骨架与真实内容高度差导致的 DOM 跳动(DOM bouncing)。
variant 支持的骨架单元形态
el-skeleton-item的variant属性决定了骨架单元的渲染形态,取值与实现对应关系如下(样式见 skeleton-item.scss):
| variant | 渲染形态 | 默认尺寸(源码实现) |
|---|---|---|
p | 段落块 | 高 16px,首行 33%、末行 61%、其余 100% 宽 |
h1 | 大标题 | 高$--font-size-extra-large(20px) |
h3 | 中标题 | 高$--font-size-large(18px) |
h5 | 小标题 | 高$--font-size-medium(16px) |
text | 文本行 | 高$--font-size-small(13px),宽 100% |
caption | 说明文字 | 高$--font-size-extra-small(12px) |
button | 按钮块 | 高 40px、宽 64px、圆角 4px |
image | 图片占位 | 宽高自适应,内部渲染 SVG 占位图 |
circle | 圆形占位 | 圆角 50%,尺寸对应头像中/大/小三种规格 |
rect | 矩形占位 | 默认 16px 高、100% 宽、基础圆角 |
值得说明的是:h5虽未在官方文档 API 表中列出,但源码 skeleton-item.scss 已为其定义了样式,属于从源码结构可以确认的扩展形态。circle的尺寸变量$--avatar-medium-size、$--avatar-large-size、$--avatar-small-size复用了 Avatar 组件的尺寸体系,可推断其设计意图是模拟用户头像占位。
variant="image"是一个特殊实现:它并非纯 CSS 灰块,而是通过 item.vue 中的条件渲染,挂载了 packages/skeleton/src/img-placeholder.vue 提供的内联 SVG 图片占位图(一座山峰与太阳的剪影),SVG 填充色为$--svg-monochrome-grey并占据单元 22% 的宽高,视觉上更贴近「图片未加载」的语义。
加载状态切换:用 loading + default 插槽呈现真实内容
骨架屏的最终使命是「加载完成后展示真实 UI」。通过loading属性控制骨架 DOM 与真实 DOM 的切换,真实内容放入default插槽:
<template> <div style="width: 240px"> <p> <label style="margin-right: 16px;">Switch 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>Delicious hamberger</span> <div class="bottom card-header"> <span class="time">{{ currentDate }}</span> <el-button type="text" class="button">Operation 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,页面先显示骨架;当用户拨动开关将loading置为false,组件立刻切换到default插槽中真实的卡片内容。从 index.vue 的模板结构可以看到切换的本质是v-if="uiLoading"条件渲染:uiLoading为真时渲染骨架容器(外层el-skeleton+is-animated类),为假时渲染default插槽内容。
列表数据渲染:用 count 批量生成骨架
骨架屏最常见的场景是列表加载:数据未返回时,先渲染多条骨架占位,让页面看起来「正在逐条加载」。count属性用于控制渲染几组骨架模板:
<template> <div style="width: 400px"> <p> <el-button @click="setLoading">Click me to reload</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>Delicious hamberger</span> <div class="bottom card-header"> <span class="time">{{ currentDate }}</span> <el-button type="text" class="button">Operation 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: 'Deer', }, { imgUrl: 'https://fuss10.elemecdn.com/1/34/19aa98b1fcb2781c4fba33d850549jpeg.jpeg', name: 'Horse', }, { imgUrl: 'https://fuss10.elemecdn.com/0/6f/e35ff375812e6b0020b6b4e8f9583jpeg.jpeg', name: 'Mountain Lion', }, ] }, methods: { setLoading() { this.loading = true setTimeout(() => (this.loading = false), 2000) }, }, } </script>示例中用:count="3"渲染 3 组相同的骨架,数据到达后mounted中将loading置为false并填充 3 条真实列表数据;点击按钮则通过setLoading先置true(重新显示骨架)再于 2 秒后切回真实列表,模拟一次完整的下拉刷新。源码层面,count通过 index.vue 的v-for="i in count"循环包裹template插槽实现多组渲染。
官方文档在此处给出了明确的性能提示:不建议渲染大量假 UI,因为假骨架同样占用浏览器渲染资源,且切换销毁时成本更高。请尽量让count保持最小,以换取更好的用户体验。
防闪烁优化:throttle 延迟渲染的真实原理
当接口响应极快时,骨架刚渲染到 DOM 就要立刻切换回真实内容,会造成一帧「白闪」或「闪烁」(sudden flashy)。throttle属性正是为此设计——它设置骨架渲染的延迟毫秒数,延迟期内不渲染骨架,从而平滑跳过这种瞬态:
<template> <div style="width: 240px"> <p> <label style="margin-right: 16px;">Switch 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>Delicious hamberger</span> <div class="bottom card-header"> <span class="time">{{ currentDate }}</span> <el-button type="text" class="button">operation 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并不是简单地对loading做节流,而是通过内部状态uiLoading实现「进入加载态延迟生效」的策略(index.vue):
watch: { loading: { handler(loading) { if (this.throttle <= 0) { this.uiLoading = loading; return; } if (loading) { clearTimeout(this.timeoutHandle); this.timeoutHandle = setTimeout(() => { this.uiLoading = this.loading; }, this.throttle); } else { this.uiLoading = loading; } }, immediate: true } }, data() { return { uiLoading: this.throttle <= 0 ? this.loading : false }; }其行为可以精确概括为:
- 初始状态:若
throttle > 0,uiLoading强制为false,即使loading为true也暂不渲染骨架; loading变为true(进入加载)时:不立即切换,而是启动setTimeout,等待throttle毫秒后再把uiLoading同步为true并渲染骨架;loading变为false(加载结束)时:立即把uiLoading置为false,马上展示真实内容;- 若在延迟窗口内
loading又变回false,clearTimeout会取消尚未触发的骨架渲染——这正是「接口太快时完全不闪骨架」的实现基础。
因此,throttle的语义是「骨架进入延迟」而非「内容显示延迟」:加载结束后内容永远即时呈现,只有加载过程短于throttle阈值时骨架才被跳过,从而彻底规避闪烁。
API 速查:属性、插槽与类型定义
el-skeleton 属性(Skeleton Attributes)
| 属性 | 说明 | 类型 | 可选值 | 默认值 |
|---|---|---|---|---|
animated | 是否显示加载动画 | boolean | true / false | false |
count | 渲染多少组骨架模板到 DOM | number | 整数 | 1 |
loading | 是否显示骨架屏 | boolean | true / false | true |
rows | 段落行数,仅在未提供 template 插槽时生效 | number | 整数 | 4 |
throttle | 骨架渲染延迟(毫秒) | number | 整数 | 0 |
以上属性与 packages/skeleton/src/index.vue 的props声明一一对应,同时已在 TypeScript 类型文件 types/skeleton.d.ts 中完整声明(其中rows的声明类型为 boolean,属于类型文件与源码实现不一致的历史遗留,实际运行时按 number 处理,引用时需注意)。
el-skeleton-item 属性(Skeleton Item Attributes)
| 属性 | 说明 | 类型 | 可选值 | 默认值 |
|---|---|---|---|---|
variant | 当前渲染的骨架单元类型 | Enum(string) | p / h1 / h3 / text / caption / button / image / circle / rect | text |
插槽(Skeleton Slots)
| 插槽名 | 说明 |
|---|---|
default | 真实渲染的 DOM(加载完成后的实际内容) |
template | 自定义骨架模板(加载过程中的占位结构) |
插槽的语义同样体现在模板逻辑中:当uiLoading为真时渲染template插槽(未提供时退化为内置段落),为假时渲染default插槽,两个插槽互斥切换。类型层面可参考 types/skeleton.d.ts 中的ElSkeletonSlots接口定义。
结语:一套组合拳打造无跳动的加载体验
综合来看,Element UI 的骨架屏组件设计了一套完整的加载体验方案:用variant拼装贴近真实 DOM 的自定义骨架,用animated提供轻柔的加载反馈,用count覆盖列表场景,再用loading+default插槽完成骨架与真实内容的无缝切换,最后以throttle从源码层面规避快速响应下的闪烁问题。遵循「骨架结构尽量贴近真实 DOM」与「count 尽量小」两条官方建议,配合 packages/skeleton/src/index.vue、packages/theme-chalk/src/skeleton-item.scss 等实现细节的理解,你就能在自己的项目中落地一套专业、流畅且性能友好的骨架屏方案。
【免费下载链接】elementA Vue.js 2.0 UI Toolkit for Web项目地址: https://gitcode.com/gh_mirrors/eleme/element
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考