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

骨架屏(Skeleton Screen)设计与实现

引言

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

一、骨架屏基础认知

1.1 什么是骨架屏

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

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

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

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

1.2 与传统loading的区别

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

二、骨架屏实现方案对比

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

2.1 手写 HTML + CSS 方案

这是最基础且直观的实现方式,通过手动编写骨架屏的结构和样式。
我们来提供是个小小的示例,大家可以复制到

https://play.vuejs.org/

这个网站上自己试试。
以下是使用骨架屏的一个效果。可以看到过渡的非常丝滑。

在这里插入图片描述
以下是源代码,大家可以复制下来看看,到上面提到的网站上看看效果。

<!-- 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>

在这里我们发现想要做一个骨架屏,太累了,重复工作多,代码量大,那有没有更加简便的方法也能够实现效果,或者说我能够去复用呢,当然有。

2.2 组件化方案

具体思路:写一个包含多种类型骨架的基础组件,然后使用该基础组件去构建我们的业务组件。

基础组件: 封装常用骨架类型(例如,头像,文本,标题,按钮等等)
业务组件:使用用基础组件来构建业务组件(比如商品列表,文章列表之类的去复用基础组件)
这样的话大幅度缩减了我们的代码量。

接下来我们同样使用组件化的思想来实现上述骨架屏的效果。
这里的话由于代码量过多,大家可以直接前往下方地址查看源码。

https://play.vuejs.org/#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

这里我给大家提供基础骨架组件的代码。

<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>

2.3 现成的组件库

像element-plus,t-design等骨架屏也可以使用。以下是t-design的骨架屏的效果。
在这里插入图片描述

2.4 骨架屏预渲染方案

  1. page-skeleton-webpack-plugin
npm install --save-dev page-skeleton-webpack-plugin

webpack 配置示例:

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 插件。

npm install vue-skeleton-webpack-plugin

配置示例:

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功底更扎实
    • 组件化思维更清晰
    • 代码更有掌控感

实践中的收获

从最简单的开始:

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

到封装成组件:

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

最后变成业务组件:

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

这个过程会让你:

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

值得还是不值得?

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

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

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

最后的建议

  1. 循序渐进

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

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

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值