Back

使用 boneyard 自动生成骨架屏

使用 boneyard 自动生成骨架屏

骨架加载器(Skeleton loader)是那种看起来简单、但真正动手实现时却会让人头疼的 UI 模式。你需要测量 DOM 元素、硬编码高度、接入条件渲染——然后设计师改了卡片布局,你又得从头来过。维护成本就这样悄悄累积。

boneyard-js 采用了一种不同的思路:它不需要你手写骨架 UI,而是在开发阶段直接从已渲染的组件中提取布局数据,自动生成占位符定义。

核心要点

  • boneyard-js 通过捕获真实组件的布局数据来自动生成骨架加载器,避免了为同一 UI 维护两套并行表示的问题。
  • DOM 扫描在开发阶段通过 Playwright 完成,产出静态的 .bones.json 文件。生产环境中不存在任何运行时 DOM 遍历。
  • 默认在三个视口宽度下进行捕获(375、768、1280px),水平方向数值以百分比形式存储以支持响应式行为。
  • fixture prop 和 --wait 标志用于处理那些在捕获时依赖异步数据的组件。
  • Vite 插件会在每次 HMR 更新时自动同步骨架数据,无需单独的 CLI 步骤。
  • 已为 React、Vue、Svelte 5、Angular、Preact 和 React Native 提供适配器,所有适配器共享同一套核心格式。

手写骨架加载器的问题

大多数骨架加载器实现都与真实 UI 脱节。你先构建实际组件,然后再单独构建一个近似其形状的骨架。当组件发生变化时,骨架就会逐渐偏离。久而久之,加载状态与实际内容不再匹配,导致布局偏移和视觉上的不一致。

react-loading-skeleton 这样的库虽然减少了样板代码,但仍然需要你手动描述结构。本质上你还是在维护同一组件的两套表示。

boneyard-js 如何自动生成骨架屏

boneyard-js 颠倒了这一工作流程。你只需用 <Skeleton> 标签包裹真实组件,运行一条 CLI 命令,工具就会替你完成布局捕获。

在 React 中是这样的:

import { Skeleton } from 'boneyard-js/react'

function ActivityPanel() {
  const { data, isLoading } = useFetch('/api/activity')

  return (
    <Skeleton name="activity" loading={isLoading}>
      {data && <ActivityContent data={data} />}
    </Skeleton>
  )
}

然后,在开发服务器运行的情况下:

npx boneyard-js build

CLI 会通过 Playwright 启动一个无头浏览器,访问你的应用,找到所有 <Skeleton name="..."> 元素,并遍历其内部的 DOM 树。它会对叶子节点(文本元素、图片、按钮、表单输入)调用 getBoundingClientRect(),记录它们相对于骨架根节点的位置和尺寸。水平方向数值以百分比存储以支持响应式,垂直方向数值则以像素存储。圆角会被自动检测。

此过程默认在三个视口宽度下运行(375、768、1280px),为每个组件生成一份 .bones.json 文件:

{
  "breakpoints": {
    "375": {
      "bones": [
        { "x": 0, "y": 32, "w": 43.59, "h": 34, "r": 8 },
        { "x": 43.59, "y": 39, "w": 23.76, "h": 33, "r": 999 }
      ]
    }
  }
}

同时还会生成一个注册表文件(registry.jsregistry.ts)。在应用入口处导入一次,所有骨架定义就会全局可用:

import './bones/registry'

在运行时,<Skeleton> 组件会根据其 name 读取已注册的骨架数据,将匹配的布局渲染为绝对定位的矩形,并在 loading 变为 false 时替换为真实内容。

构建时捕获,而非运行时扫描

一个重要的区别是:DOM 扫描发生在开发阶段,而非生产环境。.bones.json 文件是静态产物,与源代码一同提交。在运行时,boneyard-js 仅读取这些预先生成的定义。浏览器中不存在实时 DOM 遍历。

对于 React Native,捕获机制有所不同。<Skeleton> 组件在开发模式下使用 UIManager 扫描 fiber 树,测量原生视图,并将数据发送给 CLI。在生产构建中,这部分扫描代码会被完全剔除。

处理动态内容与 fixture prop

如果你的组件依赖某个在 CLI 捕获期间不可用的 API,由于内容区域为空,骨架可能会生成错误。两种方案可以解决:

使用 --wait 在页面加载后延迟捕获:

npx boneyard-js build --wait 2000

使用 fixture prop 专门为捕获过程提供静态模拟数据:

<Skeleton
  name="activity"
  loading={isLoading}
  fixture={<ActivityContent data={mockData} />}
>
  {data && <ActivityContent data={data} />}
</Skeleton>

fixture 内容仅在 CLI 捕获期间渲染,对生产环境没有任何影响。

注意: 如果你的组件在 data 为 undefined 时不渲染任何内容,包裹元素会塌缩到零高度,骨架将无法显示。在 <Skeleton> 上设置 minHeight 可避免此问题。

Vite 插件实现更紧密的集成

对于基于 Vite 的项目,你可以完全省去单独的 CLI 终端:

// vite.config.ts
import { defineConfig } from 'vite'
import { boneyardPlugin } from 'boneyard-js/vite'

export default defineConfig({
  plugins: [boneyardPlugin()]
})

骨架数据会在开发服务器启动时被捕获,并在每次 HMR 更新时自动重新捕获。这样可以让构建时的骨架加载器始终与 UI 保持同步,无需任何手动干预。

框架支持

boneyard-js 以独立的包导出形式提供针对各框架的适配器:

框架导入路径
Reactboneyard-js/react
Vueboneyard-js/vue
Svelte 5boneyard-js/svelte
Angularboneyard-js/angular
Preactboneyard-js/preact
React Nativeboneyard-js/native

核心提取逻辑与 .bones.json 格式在所有适配器之间共享。

boneyard-js 值得采用吗?

boneyard-js 是一个相对较新的工具,所以 API 仍可能演进。话虽如此,其核心理念是合理的:从真实布局数据生成骨架 UI,而非靠手工维护。

实际收益在组件频繁变更的项目中最为显著。你不再需要在每次设计迭代后更新骨架占位符,只需重新运行一条命令。.bones.json 文件随之更新,你的前端加载状态也将持续保持准确。

如果你已经在花时间让骨架加载器与实际 UI 保持同步,那么这种自动化值得为它付出初期的配置成本。

结语

骨架加载器不应与其所代表的组件脱节,但手工实现几乎总是会出现这种偏离。boneyard-js 通过将骨架视为生成产物而非手写代码来解决这一问题。捕获步骤在开发阶段运行,输出是一份静态 JSON 文件,运行时开销极小。对于快速迭代 UI 的团队来说,这一工作流不仅能节省大量时间,还能让加载状态在视觉上忠实于底层组件。

常见问题

生产环境中不会发生 DOM 扫描。CLI 或 Vite 插件会在开发阶段生成静态的 .bones.json 文件。在生产环境中,Skeleton 组件只读取这些定义,并渲染绝对定位的矩形,仅增加极小的运行时开销。

它默认在三个视口宽度下捕获骨架数据:375、768 和 1280 像素。水平位置和宽度以百分比形式存储,因此骨架可以在断点之间平滑缩放,而垂直数值则保持像素单位以保证间距的可预期性。运行时会根据当前视口宽度选择最接近的匹配断点。

你有两种选择。--wait 标志可在页面加载后延迟捕获,给异步请求留出解析时间。或者,fixture prop 接受一个填充了静态数据的组件模拟版本,仅在 CLI 捕获期间渲染。两种方法都能确保骨架反映出已填充的布局,而不是一个空容器。

可以,而且应该这么做。.bones.json 文件和 registry.js 是描述骨架布局的静态产物。提交它们能让团队保持一致,使构建在 CI 中无需运行捕获步骤即可重现,还能让代码审查在审视组件改动的同时发现意外的布局变化。

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay