为什么 (Why)
在7月份的某一天,我打算为一个项目卡片列表优化体验,项目数据从远程加载。为了提升用户感知性能,我决定为卡片列表添加骨架屏。
简单的,我需要一个 loading 状态变量和一个项目卡片的骨架屏组件。等数据加载完成后,切换 loading 状态变量,显示真实的项目卡片。
“没什么难度”,我想。
当我开始编写骨架屏组件时,我发现这确实不难,但有些繁琐。
项目卡片组件包含图片、标题、描述、标签部分。为了让骨架屏看起来更像真实内容,我需要为每个部分设计对应的占位符。 这意味着我需要手动编写大量的骨架屏代码,重复的工作令人疲惫。
更令人难过的是,随着需求变化,组件会不断迭代,添加或修改内容。每次修改后,我都需要手动更新骨架屏组件,确保它与真实内容保持一致。
现在我碰到了问题,我需要一个能随着组件变化而自动更新的骨架屏生成方案。就像往常一样,我在网上搜索解决方案。
令人惊讶的是,相关的结果并不多。大多数现有的骨架屏方案都专注于首屏加载,或者需要在构建时生成静态骨架屏代码。
而我需要的是一个客户端生成的骨架,我需要的是一个小的能够放在下拉菜单里,或者放在局部区域的骨架。而不是一个全屏的骨架。
所以,为了满足我的需求,我决定自己动手。
先让我展示一下成果😊!

为什么我选择用”占位骨架”而非”骨架屏”?
骨架屏(Skeleton Screen)概念最初由 Luke Wroblewski 在 2013 年的文章 Mobile Design Details: Avoid The Spinner 中提出,主要应用场景是移动端应用 Polar。
受限于移动设备的屏幕尺寸,移动端应用往往需要将内容铺满整个可视区域来最大化信息展示,因此早期的骨架屏设计通常覆盖全屏。
随着 Web 应用和桌面应用的发展,“骨架屏”的应用场景已经大大扩展,不再局限于全屏展示。在现代应用中,我们经常在弹窗、下拉菜单、卡片组件等局部区域看到类似的加载占位效果。
考虑到本文主要专注于生成局部区域的加载占位效果,而非传统意义上的全屏骨架屏,我更倾向于使用”占位骨架”这个术语,以更准确地描述其应用范围和功能特点。
实现 (How)
根据上述的原因,这个项目的核心功能如下:
- 自动化生成
- 避免手动编写骨架代码,交给工具自动生成。
- 这也便于在页面修改时,骨架屏能够自动适配。
- 客户端渲染
- 客户端生成,避免设备尺寸适配问题
- 部分生成,细粒度控制
自动化生成的挑战性
自动化生成的挑战性在于,如何处理并兼容众多的 UI 库。这绝不是一件容易的事情,因为不同的 UI 库有不同的组件结构和样式体系。
我的决定做一个折中的方案,提供一些配置选项,允许用户根据自己的 UI 库进行调整。这种方式不能创建完美的占位骨架,但一个看起来差不多的占位骨架也不错。

查看 fuzzy 在线示例 体验。
客户端渲染的挑战性
客户端渲染的难点在于,当页面需要接收到远端数据才能渲染时,如何在数据加载前生成合适的骨架屏。
一个例子是渲染一个文章卡片组件列表,只有当数据从远端获取到之后,才能渲染出文章卡片。那么在数据加载前,如何根据文章卡片的 DOM 结构生成一个合适的占位骨架?
这看起来是一个悖论,因为在数据未加载前,我们并不知道文章卡片的具体结构和内容。
我的做法是,
- 在开发阶段收集一部分远端数据作为样本。将这些样本数据传递给骨架屏生成工具,用于生成占位骨架。
- 然后在构建阶段,将这些样本数据内嵌到构建代码中,在生产环境中使用内嵌的样本数据来生成占位骨架。
这要求骨架屏生成工具能够在构建时访问到这些样本数据,并根据样本数据的结构生成对应的占位骨架。这也是为什么需要开发构建工具插件的原因。
外围功能设计
核心功能完成后,还需要外围辅助功能来提升易用性和适配性:
- 全局配置: 提供配置选项,允许用户根据自身需求调整全局的占位骨架生成逻辑。当然,也可以针对特定页面或组件进行覆盖配置。
- 构建工具集成:通过 unplugin 实现对 Nuxt、Vite、Webpack 等多构建工具的支持
- 多种框架兼容: 受限于个人能力,目前只支持 Vue 3,但架构设计允许方便的扩展到 React、Svelte 等其他框架。
- 文档和示例: 提供友好的文档和示例,帮助用户快速上手和理解使用方法。
上述 1,2,3 点都是常见做法,略过不谈。对多框架兼容的文档工具改造感兴趣的话,可以查看我的另一篇文章 基于 Astro Starlight 的多框架文档。
结果 (What)
经过一段时间的开发,我完成了这个项目,并将其命名为 Gueleton。
Gueleton 的名字由 中文 ”骨(Gu)“ 和英文 “Skeleton” 组合而成。
目前,Gueleton 支持 Vue 3,基于 unplugin 架构,捆绑器/构建工具上支持 Nuxt、Vite、Webpack、Vue-CLI。