还在用菊花图加载?让骨架屏提升你的用户体验

引言

你可能已经注意到,当打开淘宝、知乎等主流应用时,页面并非一片空白,而是立即展现出内容的轮廓。 这种让用户感知"页面正在加载"的设计技巧,正是今天要介绍的主角 —— 骨架屏。

骨架屏基础认知

什么是骨架屏

骨架屏(Skeleton Screen)是在页面数据加载完成前,先呈现出页面的大致结构框架。这种加载占位图以简单的线条和色块勾勒出页面的大致轮廓,当真实内容加载完成后,再无缝替换掉占位图。

想象一下,当你打开淘宝App时,页面并不是一片空白或转圈的loading,而是立即展现出类似下面这样的界面:商品图片区域显示灰色方块,标题显示几条灰色线条,这就是典型的骨架屏应用。

相比传统的加载方式,骨架屏有以下特点:

  1. 渐进式加载:不是等所有内容都准备好才显示,而是先展示框架,再填充内容
  2. 结构预知性:用户可以预先了解页面的布局结构
  3. 视觉连续性:避免了页面从空白到内容的突然跳变
  4. 减少焦虑感:给用户"正在加载"的明确反馈

与传统loading的区别

特性传统Loading骨架屏
视觉反馈局部动画图标整体页面轮廓
空间占用通常居中显示,不占用实际内容空间与实际内容结构一致
用户体验完全等待状态,不知道等待什么可预知内容结构,减少等待焦虑
过渡效果内容加载完成时会有明显跳变可以实现平滑过渡
开发成本相对简单需要额外的开发工作

骨架屏实现方案对比

在前端开发中,骨架屏的实现方案主要有三大类:手写代码、组件化,自动生成和预渲染。每种方案都有其特点和适用场景,让我们深入分析各个方案。

手写 HTML + CSS 方案

这是最基础且直观的实现方式,通过手动编写骨架屏的结构和样式。 我们来提供是个小小的示例,大家可以复制到(play.vuejs.org/)这个网站上自己试试。 以下是使用骨架屏的一个效果。可以看到过渡的非常丝滑。

20250117100048_rec_.gif

 

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的骨架屏的效果。具体如何使用,还请参照他们的官方文档。文档当中有详细的介绍。这里的话我就不做过多的赘述啦。

image.png

2.4 骨架屏预渲染方案

  1. 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, // 渲染延迟时间 }) ] }

  1. 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实现骨架屏不仅能提升技术功底,更能帮你建立完整的组件化思维。

看似多余,实则必要

坦白说,在现在这个组件库横行的年代,手写骨架屏确实显得有点"多此一举"。但就像练武要从最基础的马步开始,手写骨架屏的过程,能让我们:

  1. 真正理解组件设计

    • 知道为什么要这么设计
    • 明白参数该怎么定义
    • 学会如何做到通用性
  2. 应对特殊需求

    • 组件库不够用?自己造
    • 需求有变化?随时改
    • 性能有问题?直接优化
  3. 提升开发功力

    • CSS功底更扎实
    • 组件化思维更清晰
    • 代码更有掌控感

实践中的收获

从最简单的开始:

 

html

代码解读

复制代码

<div class="skeleton-line"></div>

到封装成组件:

 

html

代码解读

复制代码

<skeleton-base width="200px" height="20px" />

最后变成业务组件:

 

html

代码解读

复制代码

<article-skeleton :loading="true" />

这个过程会能让你学到一些东西:

  • 明白组件是怎么一步步抽象的
  • 知道什么时候该复用,什么时候该重写
  • 学会如何设计一个好用的组件

值得还是不值得?

如果你问我值不值得花时间去手写,我的建议是:

  • 新手必须写:打好基础很重要
  • 老手可以写:温故知新也不错
  • 实在没时间:至少要理解原理

记住,会用组件库很简单,但理解背后的原理才是进阶的关键。就像开车,会开很简单,但懂原理的司机才能应对各种路况。

最后的建议

  1. 循序渐进

    • 先用原生写写看
    • 再试试组件封装
    • 最后做业务整合
  2. 学以致用

    • 理解了就要实践
    • 可以在项目中尝试
    • 逐步形成自己的组件库
  3. 持续改进

    • 多看看主流组件库的实现
    • 思考还有什么可以优化
    • 保持学习的心态

与其纠结要不要手写,不如动手试试看。毕竟,"会用"和"懂得"是有很大区别的。当你真正理解了骨架屏的实现原理,再去用组件库时,就能更得心应手,遇到问题也能迎刃而解。

https://juejin.cn/post/7460530181805916212
链接:https://juejin.cn/post/7460530181805916212

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

烟火漫天

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值