引言
你可能已经注意到,当打开淘宝、知乎等主流应用时,页面并非一片空白,而是立即展现出内容的轮廓。 这种让用户感知"页面正在加载"的设计技巧,正是今天要介绍的主角 —— 骨架屏。
骨架屏基础认知
什么是骨架屏
骨架屏(Skeleton Screen)是在页面数据加载完成前,先呈现出页面的大致结构框架。这种加载占位图以简单的线条和色块勾勒出页面的大致轮廓,当真实内容加载完成后,再无缝替换掉占位图。
想象一下,当你打开淘宝App时,页面并不是一片空白或转圈的loading,而是立即展现出类似下面这样的界面:商品图片区域显示灰色方块,标题显示几条灰色线条,这就是典型的骨架屏应用。
相比传统的加载方式,骨架屏有以下特点:
- 渐进式加载:不是等所有内容都准备好才显示,而是先展示框架,再填充内容
- 结构预知性:用户可以预先了解页面的布局结构
- 视觉连续性:避免了页面从空白到内容的突然跳变
- 减少焦虑感:给用户"正在加载"的明确反馈
与传统loading的区别
特性 | 传统Loading | 骨架屏 |
---|---|---|
视觉反馈 | 局部动画图标 | 整体页面轮廓 |
空间占用 | 通常居中显示,不占用实际内容空间 | 与实际内容结构一致 |
用户体验 | 完全等待状态,不知道等待什么 | 可预知内容结构,减少等待焦虑 |
过渡效果 | 内容加载完成时会有明显跳变 | 可以实现平滑过渡 |
开发成本 | 相对简单 | 需要额外的开发工作 |
骨架屏实现方案对比
在前端开发中,骨架屏的实现方案主要有三大类:手写代码、组件化,自动生成和预渲染。每种方案都有其特点和适用场景,让我们深入分析各个方案。
手写 HTML + CSS 方案
这是最基础且直观的实现方式,通过手动编写骨架屏的结构和样式。 我们来提供是个小小的示例,大家可以复制到(play.vuejs.org/)这个网站上自己试试。 以下是使用骨架屏的一个效果。可以看到过渡的非常丝滑。
html
代码解读
复制代码
<!-- App.vue --> <template> <div class="container"> <button class="toggle-btn" @click="toggleLoading"> {
{ isLoading ? '显示内容' : '显示骨架屏' }} </button> <!-- 文章列表 --> <div v-if="!isLoading" class="article-list"> <article v-for="article in articles" :key="article.id" class="article-card"> <div class="article-header"> <img :src="article.avatar" :alt="article.author" class="avatar"> <div class="author-info"> <h3 class="author-name">{
{ article.author }}</h3> <time class="post-time">{
{ article.date }}</time> </div> </div> <h2 class="article-title">{
{ article.title }}</h2> <p class="article-content">{
{ article.content }}</p> <div class="article-stats"> <span>{
{ article.views }} 阅读</span> <span>{
{ article.likes }} 点赞</span> <span>{
{ article.comments }} 评论</span> </div> </article> </div> <!-- 骨架屏 --> <div v-else class="article-list"> <div v-for="n in 3" :key="n" class="article-card"> <div class="article-header"> <div class="skeleton avatar-skeleton"></div> <div class="author-info"> <div class="skeleton title-skeleton"></div> <div class="skeleton date-skeleton"></div> </div> </div> <div class="skeleton-content"> <div class="skeleton heading-skeleton"></div> <div class="skeleton text-skeleton"></div> <div class="skeleton text-skeleton"></div> <div class="skeleton text-skeleton"></div> </div> <div class="article-stats"> <div class="skeleton stats-skeleton"></div> <div class="skeleton stats-skeleton"></div> <div class="skeleton stats-skeleton"></div> </div> </div> </div> </div> </template> <script setup> import { ref } from 'vue' const isLoading = ref(true) const articles = ref([ { id: 1, author: '张三', avatar: 'https://placekitten.com/100/100', date: '2024-01-16', title: 'Vue3 组件开发最佳实践', content: 'Vue3的组件开发带来了全新的可能性。Composition API不仅提供了更好的代码组织方式,还能够提供更好的类型推导...', views: '1.2k', likes: 328, comments: 46 }, { id: 2, author: '李四', avatar: 'https://placekitten.com/101/101', date: '2024-01-15', title: '深入理解响应式原理', content: '响应式系统是Vue的核心特性之一。通过Proxy的实现,Vue3的响应式系统变得更加强大和高效...', views: '2.3k', likes: 892, comments: 125 }, { id: 3, author: '王五', avatar: 'https://placekitten.com/102/102', date: '2024-01-14', title: 'Vite构建工具的实践分享', content: 'Vite作为新一代的前端构建工具,其基于ES modules的开发服务器让开发体验得到极大提升...', views: '1.8k', likes: 567, comments: 89 } ]) const toggleLoading = () => { isLoading.value = !isLoading.value if (isLoading.value) { setTimeout(() => { isLoading.value = false }, 2000) } } </script> <style scoped> .container { max-width: 800px; margin: 0 auto; padding: 20px; } .toggle-btn { background-color: #4a90e2; color: white; border: none; padding: 10px 20px; border-radius: 4px; margin-bottom: 20px; cursor: pointer; transition: background-color 0.3s; } .toggle-btn:hover { background-color: #357abd; } .article-list { display: flex; flex-direction: column; gap: 20px; } .article-card { background: white; border-radius: 8px; padding: 20px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } .article-header { display: flex; align-items: center; margin-bottom: 16px; } .avatar { width: 48px; height: 48px; border-radius: 50%; object-fit: cover; } .author-info { margin-left: 12px; } .author-name { font-weight: 600; color: #333; margin: 0; } .post-time { font-size: 14px; color: #666; } .article-title { font-size: 20px; font-weight: bold; color: #333; margin-bottom: 12px; } .article-content { color: #666; line-height: 1.6; margin-bottom: 16px; } .article-stats { display: flex; justify-content: space-between; color: #666; font-size: 14px; } /* 骨架屏样式 */ .skeleton { background: #f0f0f0; border-radius: 4px; position: relative; overflow: hidden; } .skeleton::after { content: ''; position: absolute; top: 0; right: 0; bottom: 0; left: 0; background: linear-gradient( 90deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.6) 50%, rgba(255, 255, 255, 0) 100% ); animation: shimmer 1.5s infinite; } @keyframes shimmer { 0% { transform: translateX(-100%); } 100% { transform: translateX(100%); } } .avatar-skeleton { width: 48px; height: 48px; border-radius: 50%; } .title-skeleton { height: 16px; width: 120px; margin-bottom: 8px; } .date-skeleton { height: 14px; width: 80px; } .heading-skeleton { height: 24px; width: 80%; margin-bottom: 16px; } .text-skeleton { height: 16px; width: 100%; margin-bottom: 8px; } .text-skeleton:last-child { width: 60%; } .stats-skeleton { height: 14px; width: 60px; } .skeleton-content { margin-bottom: 16px; } /* 响应式设计 */ @media (max-width: 768px) { .container { padding: 16px; } .article-card { padding: 16px; } .article-title { font-size: 18px; } .avatar { width: 40px; height: 40px; } .author-info { margin-left: 8px; } } </style>
在这里我们发现想要做一个骨架屏,太累了,那有没有更加简便的方法也能够实现效果,或者说我能够去复用呢,当然有。
组件化方案
具体思路:写一个包含多种类型骨架的基础组件,然后使用该基础组件去构建我们的业务组件。
基础组件: 封装常用骨架类型(例如,头像,文本,标题,按钮等等)
业务组件:复用基础组件(比如商品列表,文章列表之类的去复用基础组件)
这样的话大幅度缩减了我们的代码量。
接下来我们同样使用组件化的思想来实现上述骨架屏的效果。 这里的话由于代码量过多,大家可以直接前往下方地址查看源码。
html
代码解读
复制代码
https://play.vuejs.org/#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
这里我给大家提供基础骨架组件的代码。
html
代码解读
复制代码
<template> <div :class="[ 'skeleton-item', `skeleton-${type}`, animated && 'skeleton-animated' ]" :style="computedStyle" > <slot></slot> </div> </template> <script setup> import { computed } from 'vue'; const props = defineProps({ type: { type: String, default: 'text', // text, avatar, image, button,这里是这个组件包含的几种类型,如果还需要添加的话,只需在css部分添加样式就好了。 }, width: { type: [String, Number], default: '100%' }, height: { type: [String, Number], default: null }, animated: { type: Boolean, default: true }, round: { type: Boolean, default: false } }); const computedStyle = computed(() => { const style = {}; if (props.width) { style.width = isNaN(props.width) ? props.width : `${props.width}px`; } if (props.height) { style.height = isNaN(props.height) ? props.height : `${props.height}px`; } if (props.round) { style.borderRadius = '50%'; } return style; }); </script> <style scoped> .skeleton-item { background: #f2f2f2; overflow: hidden; } .skeleton-text { height: 16px; margin-bottom: 8px; border-radius: 4px; } .skeleton-avatar { width: 40px; height: 40px; border-radius: 50%; } .skeleton-image { border-radius: 4px; } .skeleton-button { height: 36px; border-radius: 4px; } .skeleton-animated { position: relative; overflow: hidden; z-index: 1; } .skeleton-animated::after { content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: linear-gradient(90deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.5) 50%, rgba(255, 255, 255, 0) 100%); animation: skeleton-loading 1.4s infinite; } @keyframes skeleton-loading { 0% { transform: translateX(-100%); } 100% { transform: translateX(100%); } } </style>
这样写还是有点麻烦,我还得自己去写基础组件,然后写业务组件,那还有没有更加简便的方法呢,当然还是有的,就是通过现成的组件库去实现骨架屏效果。
现成的组件库
像element-plus,t-design等组件库也提供了有关骨架屏方案。以下是t-design的骨架屏的效果。具体如何使用,还请参照他们的官方文档。文档当中有详细的介绍。这里的话我就不做过多的赘述啦。
2.4 骨架屏预渲染方案
- page-skeleton-webpack-plugin
bash
代码解读
复制代码
npm install --save-dev page-skeleton-webpack-plugin
webpack 配置示例:
javascript
代码解读
复制代码
const PageSkeletonWebpackPlugin = require('page-skeleton-webpack-plugin') module.exports = { plugins: [ new PageSkeletonWebpackPlugin({ pathname: path.resolve(__dirname, `./shell`), // 生成骨架屏文件存放地址 staticDir: path.resolve(__dirname, './dist'), // 静态资源路径 routes: ['/', '/about'], // 需要生成骨架屏的路由 excludes: ['.van-nav-bar'], // 需要忽略的元素 defer: 5000, // 渲染延迟时间 }) ] }
- vue-skeleton-webpack-plugin
这是 Vue 官方维护的骨架屏 webpack 插件。
bash
代码解读
复制代码
npm install vue-skeleton-webpack-plugin
配置示例:
javascript
代码解读
复制代码
const SkeletonWebpackPlugin = require('vue-skeleton-webpack-plugin') module.exports = { plugins: [ new SkeletonWebpackPlugin({ webpackConfig: { entry: { app: resolve('./src/skeleton.js') } }, quiet: true, minimize: true, router: { mode: 'history', routes: [ { path: '/', skeletonId: 'skeleton1' }, { path: '/about', skeletonId: 'skeleton2' } ] } }) ] }
总结
手写骨架屏:看似多余实则重要的实践
现在各大组件库都提供了骨架屏组件,为什么我们还要手写?本文会告诉你,从0到1实现骨架屏不仅能提升技术功底,更能帮你建立完整的组件化思维。
看似多余,实则必要
坦白说,在现在这个组件库横行的年代,手写骨架屏确实显得有点"多此一举"。但就像练武要从最基础的马步开始,手写骨架屏的过程,能让我们:
-
真正理解组件设计
- 知道为什么要这么设计
- 明白参数该怎么定义
- 学会如何做到通用性
-
应对特殊需求
- 组件库不够用?自己造
- 需求有变化?随时改
- 性能有问题?直接优化
-
提升开发功力
- CSS功底更扎实
- 组件化思维更清晰
- 代码更有掌控感
实践中的收获
从最简单的开始:
html
代码解读
复制代码
<div class="skeleton-line"></div>
到封装成组件:
html
代码解读
复制代码
<skeleton-base width="200px" height="20px" />
最后变成业务组件:
html
代码解读
复制代码
<article-skeleton :loading="true" />
这个过程会能让你学到一些东西:
- 明白组件是怎么一步步抽象的
- 知道什么时候该复用,什么时候该重写
- 学会如何设计一个好用的组件
值得还是不值得?
如果你问我值不值得花时间去手写,我的建议是:
- 新手必须写:打好基础很重要
- 老手可以写:温故知新也不错
- 实在没时间:至少要理解原理
记住,会用组件库很简单,但理解背后的原理才是进阶的关键。就像开车,会开很简单,但懂原理的司机才能应对各种路况。
最后的建议
-
循序渐进
- 先用原生写写看
- 再试试组件封装
- 最后做业务整合
-
学以致用
- 理解了就要实践
- 可以在项目中尝试
- 逐步形成自己的组件库
-
持续改进
- 多看看主流组件库的实现
- 思考还有什么可以优化
- 保持学习的心态
与其纠结要不要手写,不如动手试试看。毕竟,"会用"和"懂得"是有很大区别的。当你真正理解了骨架屏的实现原理,再去用组件库时,就能更得心应手,遇到问题也能迎刃而解。
https://juejin.cn/post/7460530181805916212
链接:https://juejin.cn/post/7460530181805916212