skeletonizer
skeletonizer
在线 demo:https://icodejoo.github.io/codejoo/skeletonizer/(各效果、档位、深色、fit 等可交互切换)
English docs: README.md。
Web 版骨架屏方案:照 Flutter skeletonizer 的路子,真实 DOM + mock 数据 + 纯 CSS,不用手画占位形状。
受 Flutter 的 skeletonizer 启发,解决 vue-skeletor / react-content-loader 里手写占位的痛点 —— 改个 UI 就得重新设计骨架,特别折腾。这套方案照常渲染真实组件,喂进 mock 数据,你用 skz-text / skz-bone / skz-leaf 标出哪些元素是骨头,CSS 按实际布局把它们画出来。
包提供两个构建,二选一(各自自包含,同一页面不要同时引):
- 完整版(默认):
import "skeletonizer"一行搞定(样式 + 运行时 + 全局skz)。功能齐全:继承防火墙(长列表)、SVG 方案与老浏览器降级、四种文字模式、懒渲染,规模上限高得多。 - lite:
import "skeletonizer/lite"。包最小、规则最简单,面向现代浏览器里的中小骨架(单个根约 2000 元素以内)。
两者怎么选,看下面的对比表。0.3.0 起入口就只有这两个加框架适配层;从 0.2.x 升级见升级到 0.3.0。
骨头由你自己标(0.2.0 起的默认):没标的内容原样显示。给无法改标记的第三方组件,可以开一块 skz-auto 区域,区域内退回按标签推导。从 0.1.x 升级见升级到 0.2.0。
目录
- 怎么工作
- 完整版还是 lite:并列对比表
- 快速开始:完整版和 lite 各一行引入,外加怎么标记骨头
- 选择入口:各文件的内容与体积
- 升级到 0.3.0 · 升级到 0.2.0
- 功能总表:效果 / 入口 / 选项 / 自动机制 / 标记 / 主题变量 / API 的作用、用法、限制,每项标明属于 lite 还是完整版
- 标记与属性参考:元素标记 / 根属性 / 运行时属性,以及它们在各文字模式、入口、动画效果、浏览器档位下的表现
- 类型:
EnableOptions、各联合类型、类型怎么随版本变宽、适配层类型、SkzBox类 - 方案怎么选:按场景的建议
- 方案对比:性能、颜色、兼容性实测(含 lite 的规模上限)
- 各方案详解:fade / solid、global、svg、标记与
skz-auto、懒渲染 - Bone 工具 · 主题
- 浏览器支持与降级 · 已知限制与使用约束
- 在框架里怎么用 · 给 AI 编码助手 · 开发与实验存档
怎么工作
三个部分组成:
- 真实 DOM 渲染:用你平常的 JSX / Vue 模板写组件,只是喂进 mock 数据而不是真实数据。
- Mock 数据:用
Bone工具生成文字、图片等占位内容,都是确定性的(SSR 友好,不用Math.random)。 - 你标记,CSS 来画:用裸属性
skz-text(文字)、skz-bone(图片、头像、按钮、色块)、skz-leaf(把一棵子树合成一块骨头)、skz-ignore(保持原样)说明哪些元素是骨头,没标的内容原样显示。无法改标记的第三方组件放进skz-auto区域,区域内才按标签名和内容推导(见标记骨头)。
骨架样式是纯 CSS,不用 UA 嗅探,也不用 JS 闸门:CSS 可以完全脱离 JS 运行,只靠根上的 skz 属性开关(JS 只负责切开关、写根属性)。两种用法的渐进方式不同:
- lite:目标线是 Chrome 119+ / Safari 16.4+ / Firefox 128+(
@property、相对颜色、:has()、text-decoration-thickness、background-clip: text都具备)。过线的浏览器走“现代档”(一个@supports:skz-text文字条用 round / clip 画法,另有控件整块、图标识别和skz-auto区域里的其他推导),pulse/shimmer由根上的一个 CSS 动画驱动。低于线的浏览器只有第 0 档(按标签铺静态色块,仅skz-auto区域)、skz-bone/skz-leaf标记和 fade,skz-text文字条需要达到现代档;没有逐档渐进。 - 完整版:三层渐进(第 0 档 → 第 1 档 → 第 2 档),从 Chrome 88 到最新的都有对应效果,外加老浏览器的 SVG 动画降级。各档的推导只在
skz-auto区域里生效,skz-text标记由第 1 / 2 档负责。
完整版还是 lite
| 维度 | 完整版(默认) | lite |
|---|---|---|
| 适用场景 | 长列表 / 信息流 / 大表单;要兼容老浏览器的动画;要多种文字模式、懒渲染等;要精细调性能;"什么都能用" | 现代浏览器里的中小骨架:卡片、详情页、几十条以内的列表;想要"包最小" |
| 浏览器门槛 | 最佳效果同一条线;往下有第 1 / 2 档渐进,pulse / shimmer 在老浏览器里自动降级为 SVG 动画 | Chrome 119+ / Safari 16.4+ / Firefox 128+;低于门槛有第 0 档静态色块(skz-auto 区域)、skz-bone / skz-leaf 标记和 fade,skz-text 文字条需要达到门槛 |
| 验证状态 | 仅 Chrome 实测;Firefox / Safari 按规范默认兼容,未实测 | 同左 |
| 规模建议 | 上限高得多:继承防火墙下,正常速度 2000 卡(16000 元素)仍是 60 帧;4× 降速下 4000 元素掉到 47.6~57.6 帧 | 单个根约 2000 元素以内(数据见 lite 的规模上限) |
| 效果 | fade / solid / pulse / shimmer;pulse / shimmer 可选 global / svg 两种实现 | fade(默认)/ solid / pulse / shimmer;pulse / shimmer 是纯 CSS 根驱动,光带降频到 24 次/秒 |
| 文字模式 | round(默认)/ clip / underline / leaf | round(默认)/ clip |
| 标记与推导 | 标记;skz-auto 区域里第 0 / 1 / 2 档逐档按能力取舍 |
标记(skz-text / skz-bone / skz-leaf / skz-ignore);skz-auto 区域里另有第 0 档标签白名单 + 一个 @supports 的现代档 |
| 继承防火墙 | 有(global 方案) |
无 |
| svg 方案 | 有(svg 方案,不依赖 @property) |
无 |
懒渲染(skz-paused)与 skz-cv |
有 | 无 |
| 导入方式 | import "skeletonizer" |
import "skeletonizer/lite" |
| CSS | full.mjs 开头是 import "./full.css",打包器会带上样式;SSR 首屏 / 无打包器 / CDN 时单独引 skeletonizer/full.css |
lite.mjs 开头是 import "./lite.css";上述场景单独引 skeletonizer/lite.css |
| 体积(gzip) | JS 3.86 KB + CSS 2.33 KB = 6.19 KB | JS 2.21 KB + CSS 1.99 KB = 4.20 KB |
| SSR | JS 里带 CSS 导入,要打包器(见 SSR 与 Node);全局 skz 导入即挂 |
同左 |
| 类型 | EnableOptions 多出 text / shimmerMode / engine / fallback,适配层同步变宽 |
EnableOptions 只有 effect / fit,SkzEffect 四个值 |
体积口径:pnpm build 产物按 gzip -9 -n 逐文件压缩(1 KB = 1024 字节);每个 JS 文件都自包含,所以"JS"就是那一个文件。明细在选择入口。
一句话挑:默认就是完整版;包体最敏感、骨架又小,换 skeletonizer/lite。留在(或换到)完整版的理由:
- 单个根超过约 2000 元素,还要 pulse / shimmer(lite 没有继承防火墙;桌面 CPU 上实测到约 4000 元素还是 60 帧,慢设备要少得多,数据见 lite 的规模上限);
- 浏览器门槛以下的用户也要有动画(完整版有 SVG 降级);
- 需要 underline / leaf 文字、懒渲染;
- 没有列表结构的大骨架(大表单、长详情页)要 shimmer:
svg方案。
skeletonizer 和 skeletonizer/lite 不要在同一页面同时引:各自带一份 core,扩展注册表和全局 skz 会分家(后加载的占全局 skz)。
快速开始
1. 安装
pnpm add skeletonizer
2. 完整版(默认):一行引入
import "skeletonizer"; // 样式(full.css)+ 运行时 + 全局 skz,一行搞定
之后在任何地方都可以直接用全局 skz(不用再 import):
const off = skz.enable(document.querySelector("#card")!, { effect: "shimmer" });
const user = { name: skz.bone.text(8), bio: skz.bone.lines(2) };
// 数据到了:
off();
也可以具名导入,类型提示更完整(skz 的成员见全局变量 skz):
import { enable, Bone } from "skeletonizer";
import skz from "skeletonizer"拿到的就是全局上的那个skz对象(skeletonizer与skeletonizer/lite成员相同);全局名字被别人占用时,默认导入照样可用。
完整版已经内置 global、svg 两个方案,没有别的要引、要挑的;方案在运行时用 engine 选项选。
单独引 skeletonizer/full.css 的场景:入口在打包器里自带样式,下面几种情况要自己引:
- SSR 首屏:服务端渲染的 HTML 里直接写
<link rel="stylesheet" href="…/skeletonizer/full.css">,根上写skz,首屏就是骨架,JS 起来后enable()接管(幂等)。服务端跑 JS 的注意事项见 SSR 与 Node。 - 无打包器:
<link>引full.css+ import map,见无打包器。 - CDN:同上,URL 换成 CDN 地址。
3. lite:更小的构建
import "skeletonizer/lite"; // 样式(lite.css)+ core 运行时 + 全局 skz
第 2 步里的用法(skz、具名导入、import skz from "skeletonizer/lite"、单独引 skeletonizer/lite.css)全部同样适用。lite 没有的东西见完整版还是 lite。skeletonizer 和 skeletonizer/lite 不要同时引。
4. 开启骨架
import { enable, Bone } from "skeletonizer"; // lite 写 "skeletonizer/lite"
const off = enable(document.querySelector("#card")!, { effect: "shimmer" });
// 数据到了:
off();
enable() 的选项("版本"列:两者 = lite 和完整版都有;仅完整版 = lite 里传了会被忽略):
| 选项 | 版本 | 取值 | 说明 |
|---|---|---|---|
effect |
两者 | fade(默认)/ solid / pulse / shimmer |
动画效果。lite 的 pulse / shimmer 开箱即用(根驱动 CSS 在 lite.css 里);完整版里 pulse / shimmer 的实现可选 global / svg,都没引入变体时按 global 的纯 CSS 根驱动 |
text |
仅完整版 | round(默认)/ clip / underline / leaf |
文字骨头画法,写到根的 skz-text-mode,见文字骨头模式;lite 只有 round / clip,不认这个选项(不写 = round,与纯 CSS 一致) |
engine |
仅完整版 | global / svg |
pulse / shimmer 的实现。不传时按已引入的变体决定:有 global 用 global,只有 svg 用 svg;指定了没引入的方案时的表现见功能总表的 engine 一行,开发模式会警告一次 |
fallback |
仅完整版 | svg(默认)/ fade |
global 方案在不支持 @property 的浏览器里的降级:svg 由 JS 挂 SVG 动画,fade 保持基底 fade;JS 执行前老浏览器显示 fade |
fit |
两者 | 布尔,默认 false |
骨架自身绝不撑出滚动条:根的高度被限制在滚动祖先(没有则视口)剩余空间内,完全落在外面的列表项 display: none,尺寸变化时自动重算。<skz-box fit>、React / Vue / Svelte 均可透传 |
fit 的 CSS 已包含在 lite.css / full.css 里,无需另引。
重复调用 enable() 是安全的:按本次参数重新同步属性(没传的会被清掉),并重新扫描列表项、忽略区和主题变量。
5. 标记骨头
没标记的内容原样显示。在根里用裸属性标出哪些元素是骨头:
<!-- 根是你调用 enable() 的元素 / 包住内容的 <SkzBox>,由它们写上 skz -->
<div class="card">
<img skz-bone src="https://raw.githubusercontent.com/icodejoo/codejoo/HEAD/…" width="48" height="48" />
<h3><span skz-text>昵称</span></h3>
<p><span skz-text>一段会折成好几行的简介文字。</span></p>
<div skz-leaf class="stats"><b>128</b> 篇 · <b>3.2k</b> 关注</div>
<button skz-bone>关注</button>
<a skz-ignore href="https://github.com/icodejoo/codejoo/blob/HEAD/cancel">取消</a>
</div>
| 属性 | 写在哪 | 效果 |
|---|---|---|
skz-text |
文字元素(根的后代) | 整个子树的文字按真实文字行出条(最后一行较短),价格:<b>¥5</b> 这类混合内容也覆盖。推荐打在块元素内层的 <span> 上:默认的 round 下块元素会变成整块圆角矩形,行内 <span> 才是每行一条圆角条 |
skz-bone |
图片、头像、按钮、色块 | 本元素整块当一块骨头,子树隐藏;媒体内容被推出盒子 |
skz-leaf |
容器 | 把整棵子树合成一块骨头 |
skz-host |
Web Component 宿主(Shadow DOM) | 宿主被藏起来,由 ::before 铺一整块骨头,shadow 里的内容保持隐藏,见 Web Component 宿主 |
skz-ignore |
任意元素 | 保持原样、可点击、可聚焦。放在 skz-text / skz-auto 内时它的文字会显示在骨头条之上;想要干净的忽略区,写成兄弟元素:<span skz-text>价格</span> <span skz-ignore>实时</span> |
skz-auto |
根自身,或根内任意后代 | 只对该子树开启按标签的自动推导,给无法改标记的第三方组件用(见下) |
完整的属性清单(元素标记、根属性、库自己写的运行时属性)以及它们在各文字模式 / 入口 / 动画效果 / 档位下的表现,见标记与属性参考。
skz-text / skz-bone / skz-leaf 在根下任何位置都生效,不管在不在 skz-auto 区域里;写在根自身上不会被读取。文字条的外观跟随根上的 skz-text-mode(即 text 选项):默认 round,见文字骨头模式。
skz-auto 区域:在根或根内任意元素上写裸属性 skz-auto,按标签的自动推导只作用于这棵子树:文字标签(p span h1~h6 a li 等)、媒体与控件(img video canvas svg input button 等)、空的 <i> 和类名带 icon 的空元素。局限:规则基于标签白名单,CSS 又选不到文本节点,所以 直接写在 div、自定义元素、code 里的裸文字不在范围内(round / clip 下不可见),要包进 p / span 或自己标 skz-text;块级文字标签在 round 下是整块圆角矩形;用 div 做的头像 / 色块(空元素)不会被识别,要加 skz-bone。
6. 喂 mock 数据,照常渲染
const user = loading ? { name: Bone.text(8), bio: Bone.lines(2), avatar: Bone.image(48, 48) } : data;
框架适配
适配层走子路径导出,框架本身是可选 peer 依赖。适配层只是薄薄一层胶水(每个 gzip 约 0.5–0.9 KB),不含 core 代码:它们通过私有全局键 globalThis[Symbol.for("skeletonizer")] 找到 enable / disable,这个键由 skeletonizer(完整版)或 skeletonizer/lite 在导入时挂上。所以应用入口要导入且只导入其中一个、一次(样式也随它带上);适配层本身不带样式、不挂全局。两个都没导入时,开发模式下 console.warn 一次("No implementation found"),适配层的调用什么也不做。适配层的选项类型以 skeletonizer/lite 为底,项目里导入了 skeletonizer 就跟着变宽(见类型):lite 下写完整版选项是类型错误,完整版下运行时才真正生效。
// Vue 3:指令 / 组件 / 组合式 / 插件
import { vSkeleton, SkzBox, useSkeleton, SkzPlugin } from "skeletonizer/vue";
// <div v-skeleton="{ loading, effect: 'shimmer' }">…</div> 或 <SkzBox :loading="loading" effect="shimmer">…</SkzBox>
// React:Hook / 组件
import { SkzBox, useSkeleton } from "skeletonizer/react";
// <SkzBox loading={loading} effect="shimmer">…</SkzBox>
// Svelte:action
import { skeleton } from "skeletonizer/svelte";
// <div use:skeleton={{ loading, effect: 'shimmer' }}>…</div>
// Solid:指令 / 组件(适配层不用 JSX,无需编译插件)
import { skeleton, SkzBox } from "skeletonizer/solid";
// <div use:skeleton={{ loading: loading(), effect: 'shimmer' }}>…</div> or <SkzBox loading={loading()} effect="shimmer">…</SkzBox>
// skeleton 只出现在 `use:` 里时,TypeScript 会把这个 import 当成未使用删掉:加一行 `void skeleton;` 保住它
// Angular:没有适配层,自己写个十来行的指令调用 enable / disable,或用 <skz-box>,见下文「在框架里怎么用 → Angular」
<skz-box> 自定义元素 / 纯 HTML
import "skeletonizer"; // 或 "skeletonizer/lite":提供 enable / disable,<skz-box> 只是转发给它
import { defineSkzBox } from "skeletonizer/element"; // 导入没有副作用,需要手动注册
defineSkzBox();
<skz-box loading effect="shimmer"><div class="card" skz>…</div></skz-box>
<!-- 宿主上的 loading 是开关;skz 落在它的【第一个元素子节点】(骨架根)上,宿主自己不带 skz;JS 里用 el.loading = true/false 切换 -->
<!-- SSR / 无 JS:直接在子根上写 skz,如 <skz-box loading><div skz>…</div></skz-box>;CSS(标记,以及 `skz-auto` 里的推导)不用 JS 就生效,JS 起来后 enable 是幂等的 -->
<!-- 不用 JS:直接写根属性(lite:拿不到 inert 交互锁;完整版另拿不到防火墙和运行时 SVG) -->
<div skz skz-effect="shimmer">…</div>
<skz-box> 在独立入口 skeletonizer/element(只有胶水,不含 core 代码、不带 CSS;和适配层一样经私有全局键取 enable / disable,所以应用入口要导入一次 skeletonizer 或 skeletonizer/lite,否则开发模式警告一次)。它只认宿主自己的 loading(以及 effect / text / fallback / engine / fit)属性;lite 下 text / engine / fallback 被忽略。它不带 MutationObserver:框架把第一个子节点替换掉之后,由你自己处理,比如把 loading 关一下再开,新子根才会生效、旧子根被解除。宿主被移动到别的父节点后加载态会自然保持。
编辑器代码提示(skz-* 属性)
手写的属性(skz、skz-text、skz-bone、skz-leaf、skz-host、skz-ignore、skz-auto、skz-effect、skz-text-mode、skz-shimmer-mode、skz-cv)有补全、取值提示和文档链接:
-
WebStorm / JetBrains 系:不用配。包里声明了
"web-types": "./web-types.json",node_modules里有skeletonizer就自动识别。 -
VS Code:在
.vscode/settings.json加一行:{ "html.customData": ["./node_modules/skeletonizer/html-custom-data.json"] } -
JSX / 模板里的 TypeScript:导入任一适配层(
skeletonizer/react、/solid、/svelte、/vue)就生效,零配置。适配层把共享类型SkzHtmlAttributes(从skeletonizer/lite导出)并进框架的HTMLAttributes,所以skz-effect="nope"会报类型错。裸属性在 React / Vue 里类型是""(React 对未知属性传true会丢掉属性),在 Solid / Svelte 里是""或true(它们的裸写法传的是true);不收false,因为 Vue / Solid 会把它渲染成="false",照样命中[skz-bone]。TSX 对没声明的带连字符属性不做检查,所以只导入 lite 时skz-cv/skz-shimmer-mode只是没有补全,不会报错。
数据都来自同一份清单 src/attributes.ts;web-types.json 与 html-custom-data.json 由 pnpm build(或 pnpm gen:editor)生成并提交入库,测试保证清单、生成物、TS 类型与 README 属性表四处一致。
SSR 与 Node
0.3.0 起每个 JS 入口开头都带 CSS 导入(import "./full.css" / import "./lite.css"),所以 skeletonizer 和 skeletonizer/lite 在没有打包器的纯 Node 里不能直接 import(Node 不认识 .css)。SSR 框架的服务端代码都经过打包器,CSS 导入由它处理,但很多框架默认把 node_modules 里的包外置、直接交给 Node:要让它们把本包打进去(Vite / Nuxt / SvelteKit:ssr.noExternal: ["skeletonizer"];Next.js:transpilePackages: ["skeletonizer"])。全局 skz 在服务端同样会挂。完全没有打包器时,给 Node 配一个把 .css 当空模块的加载钩子,或者不要把骨架代码带进服务端包。框架适配层(skeletonizer/vue / /react / /svelte / /solid)没有 CSS 导入,Node 里 import 没问题,只是没导入过实现时开发模式会警告。
全局变量 skz
导入 skeletonizer 或 skeletonizer/lite,就会把 skz 挂到 globalThis 上,免 import 直接用。同一个 API 对象还总是会挂在私有键 globalThis[Symbol.for("skeletonizer")] 上(适配层读的就是它;公开名字 skz 被别人占用时它照样可用):
| 成员 | 版本 | 是什么 |
|---|---|---|
skz.enable / skz.disable |
两者 | 同 enable / disable |
skz.bone |
两者 | 就是 Bone 类,skz.bone.text(8)、skz.bone.lines(2) |
- SSR 里也会挂:Node 里 import 同样往
globalThis上写;不用它的话没副作用。 - 已经存在的
skz不覆盖:别人已经占用了全局名skz(不是我们挂的)时,保持原样,开发模式下console.warn一次,提示改用 import;我们自己挂的会直接刷新(多份拷贝、热更新都不报警)。生产构建里警告连同文案会被你的打包器折叠掉(见开发模式警告)。 - 不冻结对象,完整版要往上补属性;适配层(
/vue/react/svelte)不挂。 - 类型:导入任一入口后,全局
skz带类型(SkzGlobal),不需要自己声明。
无打包器:浏览器直接用(import map)
不用打包器时,JS 入口不能直接 import(开头是 CSS 导入):CSS 另用 <link> 引,模块用一份去掉那一行的拷贝,或者让你自己的构建脚本去掉它。最稳的做法是加一个很小的构建步骤;临时页面也可以把文件取下来、删掉第一条 import "./full.css"; 语句再自己托管。exports 指向的文件:
| 模块名 | 文件 | 说明 |
|---|---|---|
skeletonizer |
dist/full.mjs |
完整版;开头是 import "./full.css";full.css 另引 |
skeletonizer/lite |
dist/lite.mjs |
lite;开头是 import "./lite.css";lite.css 另引 |
CSS 文件:dist/full.css、dist/lite.css。每个 .mjs 都自包含(没有共享块),要托管的就这一个文件。
<link rel="stylesheet" href="/vendor/skeletonizer/full.css" />
<script type="importmap">
{ "imports": { "skeletonizer": "/vendor/skeletonizer/full.mjs" } }
</script>
<script type="module">
import { enable, Bone } from "skeletonizer"; // 去掉第一行(CSS 导入)的 full.mjs;导入时也会挂全局 skz
const off = enable(document.querySelector("#card"), { effect: "shimmer" });
// 数据到了:off();
</script>
- import map 要放在第一个 module 脚本之前;
full.mjs和lite.mjs千万不要同时引。 - 浏览器会因为第一行去把
./full.css当模块加载而报错,所以这种用法必须去掉那一行(打包器则会把它变成样式表链接)。
选择入口
整个包就是下面这些文件(dist/)。体积是 pnpm build 产物的 gzip -9 -n(1 KB = 1024 字节);每个 JS 文件都自包含,没有共享块,也没有带哈希的文件。
| 导入 | 文件 | 内容 | 体积(gzip) |
|---|---|---|---|
skeletonizer(完整版,默认) |
full.mjs + full.css |
core + 完整版扩展(文字模式、方案调度、懒渲染)+ global / svg 两个方案 + registerEngine + 全局 skz;开头是 import "./full.css" |
3.87 + 2.31 = 6.19 KB |
skeletonizer/lite |
lite.mjs + lite.css |
core 运行时(enable / disable / Bone / registerExtension)+ 全局 skz;开头是 import "./lite.css" |
2.22 + 1.97 = 4.20 KB |
skeletonizer/full.css |
full.css |
只要完整版样式(SSR 首屏 / 无打包器 / CDN):主题、交互锁、标记、忽略区、第 0 / 1 / 2 档、fade / solid / 减少动态效果、根驱动、fit、懒渲染暂停、skz-cv、防火墙与 svg 降级规则 |
2.33 KB |
skeletonizer/lite.css |
lite.css |
只要 lite 样式:主题、交互锁、标记、第 0 档 + 现代档、fade / solid / 减少动态效果、根驱动、fit |
1.99 KB |
skeletonizer/vue、/react、/svelte、/solid |
vue.mjs / react.mjs / svelte.mjs / solid.mjs |
框架适配层:只有胶水,不含 core 代码、不带样式;从 globalThis[Symbol.for("skeletonizer")] 取 API |
vue 0.79 / react 0.57 / svelte 0.42 / solid 0.64 KB |
skeletonizer/element |
element.mjs |
<skz-box> 自定义元素(defineSkzBox / SkzBox):只有胶水,不含 core 代码、不带样式(CSS 随 lite / full);导入没有副作用,调用 defineSkzBox() 才注册 |
0.77 KB |
每个 .mjs 旁边有对应的 .d.mts(full.d.mts、lite.d.mts ……)。lite.d.mts 是类型底座;full.d.mts 从它再导出并用 declare module "skeletonizer/lite" 把它变宽;适配层的类型从 skeletonizer/lite 引。
// 完整版(默认):全部功能,一行
import "skeletonizer";
// lite:更小的构建,两者同一页面只能引一个
import "skeletonizer/lite";
// 配适配层:先导入一个实现(一次),再用适配层
import "skeletonizer";
import { SkzBox } from "skeletonizer/vue";
- 完整版里,方案(
global防火墙 /svg)在运行时用enable(el, { engine })选,不再有分方案的入口。 explicit没有了:lite.css已经包含它的全部内容(见升级到 0.3.0)。
升级到 0.3.0
0.3.0 把 dist/ 里的 37 个文件合并成两个自包含的构建加适配层。破坏性改动如下(完整的变更记录在 CHANGELOG):
| 0.2.x | 0.3.0 |
|---|---|
import "skeletonizer"(core,core.css) |
import "skeletonizer/lite"(lite.css) |
skeletonizer/full、/all、/global、/svg(及 /all/js、/global/js、/svg/js) |
import "skeletonizer"(新的默认,= 原来的 all) |
skeletonizer/explicit、explicit.css |
删除;用 skeletonizer/lite("只认标记"本来就是默认) |
skeletonizer/core.css |
skeletonizer/lite.css |
base.css、global.css、svg.css、all.css、style.css |
skeletonizer/full.css(= 基底 + 全部变体) |
import "skeletonizer/base.css" + import "skeletonizer/global" + import { enable } from "skeletonizer/full" |
import { enable } from "skeletonizer" |
- 默认入口现在是完整版。
import "skeletonizer"带来完整版运行时和full.css;原来的 core 是skeletonizer/lite。 - 所有旧子路径删除:
./full、./explicit、./global、./svg、./all(及各自的/js)和 CSS 子路径./core.css、./base.css、./explicit.css、./global.css、./svg.css、./sweep.css、./all.css、./style.css。CSS 子路径只剩./full.css和./lite.css。 - 每个 JS 文件自包含,开头带自己的 CSS 导入;
node条件和纯 JS 的*-js构建都没有了。没有打包器的纯 Node 不能再直接 importskeletonizer/skeletonizer/lite(见 SSR 与 Node)。 skeletonizer和skeletonizer/lite不要在同一页面同时引,各自带一份 core。- 适配层不再打包 core。 它们从
globalThis[Symbol.for("skeletonizer")]取 API;应用入口要导入一次skeletonizer或skeletonizer/lite,否则开发模式警告、适配层什么也不做。适配层的选项类型以skeletonizer/lite为底,项目里导入skeletonizer后变宽。 explicit删除。 只认标记本来就是默认行为(推导要skz-auto),explicit.css是lite.css的子集;它带的懒渲染规则只对完整版 JS 有意义,完整版里本来就有。- 类型:增强目标从
declare module "skeletonizer"改成declare module "skeletonizer/lite";自己对"skeletonizer"做过类型增强的代码,改成增强"skeletonizer/lite"。
升级到 0.2.0
本节里的入口和文件名是 0.2.x 的旧名字,与今天名字的对应关系见升级到 0.3.0。
0.2.0 把默认从"按标签推导骨头"改成"你自己标"。破坏性改动如下(完整的变更记录在 CHANGELOG):
- 默认改为显式标记。 没有任何标记、也不在
skz-auto区域里的内容原样显示。文字用skz-text(块元素里包一层<span>再打),图片 / 头像 / 按钮 / 色块用skz-bone,要合并的容器用skz-leaf。无法改标记的第三方组件,在根或根内任意元素上写裸属性skz-auto:原来的推导规则(第 0 档、图标、控件、leaf 模式)现在只作用于该子树。见标记骨头。 - 根上的
skz-text属性改名为skz-text-mode(取值round/clip/underline/leaf)。text选项不变,只是写到新属性;手写根属性的要同步改名。skz-text现在是元素级标记。 - 新的默认文字模式
round。 文字条变成带圆角的背景盒(见文字骨头模式)。要回到原来的外观,根上写skz-text-mode="clip"(完整版text: "clip")。lite 与explicit.css手写round/clip也认。 - 移除
skz-has-ignore,忽略区只靠skz-ignore、纯 CSS 生效,enable()不再为它写任何属性。「含忽略区的祖先关下划线」的修正规则也一并去掉:忽略区放在skz-text元素或skz-auto区域内时,它的文字显示在骨头条之上;想要干净的忽略区,写成兄弟元素:<span skz-text>价格</span> <span skz-ignore>实时</span>。 explicit.css/skeletonizer/explicit的含义变成「不含skz-auto推导规则」,同样支持skz-text(round / clip)、skz-bone、skz-leaf、skz-ignore。lite.css与base.css= 标记 + 门控在skz-auto上的推导。- 浏览器门槛:推导和标记规则用了
:is()/:where()(Chrome 88+ / Safari 14+ / Firefox 84+;标记规则里的:not()用了选择器列表)。CSS 按这条线降级(chrome88 / firefox84 / safari14),JS 产物是 ES2015,不带 polyfill。
CSS 不再依赖 JS:手写根属性 skz 就能得到正确渲染(交互锁、懒渲染暂停、防火墙、运行时 SVG 属于性能 / 降级路径,不影响正确性)。
从 0.1.0 之前的布局迁移
0.1.0 之前的预览布局(一个入口包含全部)早已不存在,迁移说明保留在 CHANGELOG(0.1.0)里。那里提到的入口名(skeletonizer/full 等)后来又改过,当前名称以升级到 0.3.0为准。
功能总表
所有选项、属性、变量、入口和 API 的作用 / 最短用法 / 限制都在这张表里,「版本」列标明每项属于 lite 还是完整版(两者 = 都有;仅完整版 = lite 里没有;仅 lite = 只有 lite 才有的东西);总表放不下的细节见各方案详解和已知限制与使用约束;标记和属性的完整枚举(含各模式 / 入口 / 档位下的表现)见标记与属性参考。表里的性能数字(帧率 / 每帧样式重算 / 开启耗时,"4000 / 16000 元素")统一取文字模式 clip(0.2.0 之前的默认)下的数据(出自方案对比的 clip 补测与落地复测),新默认 round 没有逐表重测(原型实测 GPU 约为 clip 的一半);只有原矩阵测过、没为 clip 重测的,数字旁标了"underline 时测得"。桌面 Chrome、单次正向测量,差距在 2 倍以内的别当真。
| 项 | 版本 | 作用 | 用法 | 受限 |
|---|---|---|---|---|
| 效果 | ||||
fade(默认) |
两者 | 根整体 opacity 脉冲(1 → --skz-fade-min,默认 0.55),跑在合成器线程,几乎零成本;不写 skz-effect 就是它 |
enable(el)enable(el, { effect: "fade" })<div skz> |
根里有 skz-ignore 时隐式默认不开,要显式写 effect: "fade";会把根里的一切(含忽略区)一起淡出;不支持 @property 的老浏览器里 pulse / shimmer 也退回它(完整版的 global 会由 JS 改挂 SVG 动画)4000 / 16000 元素都是 60 帧、样式重算 0.14 / 0.33 ms,开启 24 / 106 ms lite 实测(clip 默认,Chrome 154,每格新鲜加载):2000 / 4000 元素 fade 都是 60.4 帧、样式重算 0.12 / 0.14 ms,solid 60.4 帧、样式重算 0;lite 的 fade 在 4× CPU 降速下没单独测,见 lite 的规模上限 |
solid |
两者 | 静止的骨头色,没有动画 | enable(el, { effect: "solid" })skz-effect="solid" |
基底自带,无额外限制 60 帧、样式重算 0.00 ms,开启 22 / 109 ms |
pulse |
两者 | 骨头颜色在 --skz-color 与 --skz-highlight 之间来回脉冲(根驱动:根上一个动画驱动,lite 和完整版都有;完整版另可选 svg:共享 SVG 动画图) |
enable(el, { effect: "pulse" })(lite 不用另引;完整版想要防火墙再 import "skeletonizer/global") |
根驱动需 @property(Chrome 119+ / Safari 16.4+ / Firefox 128+):低于门槛 lite 退回 fade,完整版的 global 由 JS 改挂 SVG 动画;纯 CSS 大骨架很慢,lite 没有继承防火墙(规模上限见 lite 的规模上限)lite(无防火墙,正常速度):1000 / 2000 / 4000 元素 60.4 / 60.4 / 59.2 帧,样式重算 3.05 / 6.18 / 11.56 ms;6000 元素 46.4 帧、16000 元素 18.4 帧;4× CPU 降速下 504 元素 40.8 帧、2000 元素 21.2 帧(均为 clip,数据与口径见 lite 的规模上限) 完整版 2000 卡(16000 元素) global pulse(落地后复测):clip 60.4 帧 / 样式重算 6.95 ms,underline 60.4 / 9.66;落地前 clip 因防火墙漏钉 --skz-tbg 曾掉到 22.8 帧 / 33.7 ms(已修复;现在 clip 根上不再转发逐帧变化的 --skz-tbg) |
shimmer |
两者 | 高光光带在每个骨头自己的盒子里扫(光带固定 300px,进度由根统一驱动,各骨头同时起步、同时走完),所有平台一样 | enable(el, { effect: "shimmer" })(lite 不用另引;完整版想要防火墙再 import "skeletonizer/global") |
同 pulse;round / clip 文字条里也有光带,显式 text: "underline" 的文字没法渐变(global 改为颜色脉冲、svg 保持静止)光带按骨头自身盒子扫之后的性能数据尚未重测,下面是改前的实测(原理上滚动时不再因 fixed 背景走主线程重绘,应不差于原来) lite(无防火墙,正常速度,默认光带降频):1000 / 2000 / 4000 元素都是 60.4 帧,样式重算 1.27 / 2.61 / 5.05 ms;6000 元素 58 帧、16000 元素 16.8 帧;4× CPU 降速下 504 元素 52 帧、2000 元素 17.2 帧(clip,见 lite 的规模上限) 完整版 global + enable()(2000 卡 = 16000 元素,落地后复测):60.4 帧 / 样式重算 3.19 ms,开启 85 ms(clip 补测批);svg:58.8 帧 / 0.05 ms,开启 140 ms(clip 补测;落地后复测批同场景 50.4 帧,批间漂移,见方案对比);纯 CSS 无 JS:20.8 帧 / 41.3 ms(underline 时测得,clip 未重测);explicit.css + global:2.60 ms、开启 50 ms(underline 时测得)无列表结构页(双栏表单,clip 补测): global 11.2 帧 / 75.0 ms,svg 52.8 帧 / 0.04 ms默认降频到 24 次/秒( --skz-shimmer-timing,只对根驱动:lite 与 global):2000 卡 clip 的 GPU 10.44 → 3.05 ms、underline 5.35 → 1.63 ms(落地后复测);4× CPU 降速下默认档没收益(clip 18.4 帧,与落地前持平),steps(18) 下 clip 到 40 帧,underline 仍 14~15 帧 |
| 入口 / 变体 | ||||
full.css |
仅完整版 | 完整版样式:主题变量、交互锁、显式标记、忽略区、skz-auto 区域里的三档推导、fade / solid、根驱动(纯 CSS pulse / shimmer)、懒渲染暂停、减少动态效果、skz-cv 与 fit 的 CSS、防火墙与 svg 降级规则 |
import "skeletonizer/full.css"(入口 skeletonizer 已经带上它) |
2.33 KB;pulse / shimmer 开箱即用(根驱动,防火墙由 enable() 在运行时启动);推导选择器(只在 skz-auto 区域里匹配)让每帧样式重算比 lite 贵 |
skeletonizer(完整版,默认入口) |
仅完整版 | 完整版:core 运行时 + 完整版扩展(文字模式、方案调度、懒渲染)+ global / svg 两个方案 + registerEngine + 全局 skz;文件开头是 import "./full.css" |
import "skeletonizer" |
JS 3.86 KB + CSS 2.33 KB;要打包器(见 SSR 与 Node);不要和 skeletonizer/lite 一起引 |
lite.css |
仅 lite | lite 样式:主题与交互锁 + 显式标记 + 第 0 档(skz-auto 区域)+ 现代档(一个 @supports 同时要 text-decoration-thickness 与 :has():skz-text 文字条,以及 skz-auto 区域的推导)+ fade / solid / 减少动态效果 + fit + 根驱动(pulse / shimmer) |
import "skeletonizer/lite.css" |
1.99 KB;低于门槛只有第 0 档静态色块(仅 skz-auto 区域)、skz-bone / skz-leaf 标记和 fade;文字条规则只匹配没写 skz-text-mode 或写了 round / clip 的根;没有 underline / leaf、防火墙、SVG 降级、懒渲染、skz-cv;SSR 首屏 / 无打包器 / CDN 时手动引;不要和 full.css 同时引 |
skeletonizer/lite |
仅 lite | lite:core 运行时(enable / disable / Bone / registerExtension)+ 全局 skz;文件开头是 import "./lite.css" |
import "skeletonizer/lite" |
JS 2.21 KB + CSS 1.99 KB;要打包器;不要和 skeletonizer 一起引 |
global 方案 |
仅完整版 | 继承防火墙 + 老浏览器 JS 挂 SVG 降级(pulse / shimmer 的根驱动在基底里) | 内置在 skeletonizer 里;engine: "global"(默认)选用 |
不再有单独入口 |
svg 方案 |
仅完整版 | pulse / shimmer 用一张共享 SVG 动画背景,按主题运行时生成精确颜色,不依赖 @property |
内置在 skeletonizer 里;enable(el, { engine: "svg" }) |
不再有单独入口 |
| 框架适配层 | 两者 | skeletonizer/vue / /react / /svelte / /solid:只有胶水,不含 core 代码、不带样式;从 globalThis[Symbol.for("skeletonizer")] 取 API |
先导入一次 skeletonizer 或 skeletonizer/lite,再用适配层 |
每个 0.5–0.9 KB;没导入实现时开发模式警告一次,适配层什么也不做 |
skeletonizer/element(<skz-box>) |
两者 | defineSkzBox / SkzBox:独立入口里的自定义元素,只有胶水,不含 core 代码、不带样式;从 globalThis[Symbol.for(\"skeletonizer\")] 取 API |
先导入一次 skeletonizer 或 skeletonizer/lite,再 defineSkzBox() |
0.77 KB gzip;导入没有副作用;没导入实现时开发模式警告一次,元素什么也不做 |
| 纯 CSS 子路径 | 两者 | skeletonizer/full.css 和 skeletonizer/lite.css(只有这两个) |
<link> 引 |
见“无打包器”和“SSR 首屏” |
| JS 里的 CSS 导入 | 两者 | full.mjs 和 lite.mjs 开头是 import "./full.css" / import "./lite.css",打包器一并打进样式 |
自动 | 没有打包器的纯 Node 不能直接 import,见 SSR 与 Node |
enable() 选项 |
||||
effect |
两者 | 动画效果,写到根的 skz-effect |
enable(el, { effect: "shimmer" }) |
取值 fade(默认)/ solid / pulse / shimmer;pulse / shimmer 开箱即用(根驱动在基底里),低于 @property 门槛时 lite 退回 fade、完整版的 global 由 JS 改挂 SVG 动画 |
text |
仅完整版 | 文字骨头画法,写到根的 skz-text-mode |
enable(el, { text: "clip" }) |
仅完整版,lite 里传了会被忽略(lite 只有 round / clip,手写 skz-text-mode="round" / "clip" 也认)。round(默认,不写即是;需 box-decoration-break,不认的浏览器退回 clip):文字透明 + 带圆角的背景盒,行内元素折行后每行一条圆角条,块元素整块一个圆角矩形,所以 skz-text 要打在内层 <span> 上;光带、svg 引擎下与 clip 一样,GPU 约为 clip 的一半(原型实测),局限见文字骨头模式。clip(显式写;需 background-clip: text,不认这个属性的浏览器由 @supports not 撤回、退回 underline;规范要求装饰线参与裁剪,仅 Chrome 实测过,Firefox / Safari 未验证,是已知风险):下划线当形状、background-clip: text 当填充,文字条跟字形走,svg 引擎下也能动;比 underline 贵(见方案对比的 clip / underline 对照);skz-auto 区域里不在文字标签清单里的裸文字(如 div 里直接写的文本)会不可见;underline(显式写 text: "underline",需 text-decoration-thickness):最便宜,不能渐变、没有圆角,shimmer 时文字只是颜色脉冲、svg 下静止,低端机 / 追求最省时选它;leaf(需 :has()):skz-text 元素整块变一块骨头,skz-auto 区域里多行块级文字变成一整块矩形,混合文字里的裸文字节点不出骨头,详见文字骨头模式;explicit.css 只认 round / clip |
engine |
仅完整版 | pulse / shimmer 的实现:global / svg |
enable(el, { effect: "shimmer", engine: "svg" }) |
只对 pulse / shimmer 有效;不传时按已引入的变体决定:有 global 用 global,只有 svg 用 svg,都没引入按 global(纯 CSS 根驱动) 指定了没引入的方案(Chrome 154 实测):只引 global、传 engine: "svg",不会退回 fade,照常是 global 的根驱动 shimmer(CSS 不看 engine),只是没有继承防火墙(120 项的列表里 skz-fw 为 0,对照默认 110)和老浏览器 SVG 降级;只引 svg、传 engine: "global",退回基底 fade(根动画是 skz-fade、没有光带)显式传了没注册的方案时,开发模式( process.env.NODE_ENV 不是 production)会 console.warn 一次,生产构建里摘掉 |
Related Packages
Pixel-perfect skeleton loading screens. Wrap your component in and boneyard snap...
A lightweight TypeScript library to generate skeleton loading UIs with customiza...
Skeleton elements (aka UI Skeletons, Skeleton Screens, Ghost Elements) - UI for...
Version History
| Version | Released | Vue | License |
|---|---|---|---|
| 0.3.0 | >=3.0.0 | MIT | |
| 0.2.0 | >=3.0.0 | MIT | |
| 0.1.0 | >=3.0.0 | MIT |
Pre-releases (1)
| Version | Released | License |
|---|---|---|
| 0.0.0-stage |