结构意识骨架装载机. 一个网络组件。 每个框架.
别再用手做骨架了 将您真正的用户界面包入 <phantom-ui> 它通过在运行时测量您的实际 DOM 自动生成闪光的占位符 .
没有单独的骨架组件可以维护. 无复制打字布局. 真正的组成部分 实值 骨架模板.
为什么
传统的骨架加载器需要您构建和维护每个组件的第二版本,仅用于加载状态. 当真正的组件改变时,骨架会漂出同步.
phantom-ui 采取不同的做法。 它使您真实的构件带有隐形文字,测量出每个叶片元素的位置和大小(getBoundingClientRect),并在完全相同的坐标上覆盖出出动画闪光块. 集装箱背景和边界保持可见,在装入时给出了天然卡片大纲.
因为它是一个标准的Web组件(用Lit来建造),它在React,Vue,Svelte,Angular,Solid,Qwik,HTMX或平版HTML工作. 不需要框架适配器.
安装
bun add @aejkatappaja/phantom-ui # bun
npm install @aejkatappaja/phantom-ui # npm
pnpm add @aejkatappaja/phantom-ui # pnpm
yarn add @aejkatappaja/phantom-ui # yarn
或者在脚本标签中投放没有构建步骤:
<script src="https://cdn.jsdelivr.net/npm/@aejkatappaja/phantom-ui/dist/phantom-ui.cdn.js"></script>
销售单一文件
如果您无法从注册簿中安装, 或者您宁愿将副本保存在您的 repo 中, 有一个独立的 ES 模块, 里面装有 Lit 。 1个文件, 34 kB 被精密化, 11.5 kB gziped, 没有运行时间依赖 :
import "@aejkatappaja/phantom-ui/standalone";
或者,如果你根本无法到达一个注册,请将两个文件复制到您的项目中,然后通过路径导入:
phantom-ui.standalone.js # the component, unminified so a review can read it
phantom-ui.standalone.min.js # the same code, minified
phantom-ui.standalone.d.ts # its types, no imports of any kind
取第一相与三相为相. TypeScript 精确地将声明对一模块 文件名,所以这两个名字会一起使用,而且类型应用时没有任何配置.
import "./phantom-ui.standalone.js";
import type { PhantomUi } from "./phantom-ui.standalone.js";
这是整个安装。 元素本身在进口时登记,申报中没有提到任何外部内容,因此没有 node_modules 仍然按类型检查 strict 与 skipLibCheck: false。 。 。 .
宣布的类扩展 HTMLElement 而不是说 LitElement,这是对同一物体的更窄的视图,所以 updateComplete 公开宣布,其余的API保持不变.
这些是释放资产,是根本无法进入登记册的人的渠道。 在 npm 包内,同样的构件是 phantom-ui.standalone.js 财务报告和审定财务报表 phantom-ui.standalone.dev.js,通过导出地图而不是文件名解决。 任意一种构建都是同一种代码,两者都是捆绑输出而不是原始的TypeScript:源导入了Lit,因此无法自行编译.
图书馆是麻省理工学院,所以销售是好的. 注意你在文件附近复制的版本, 因为一个卖家复制的版本不会得到更新 .
可选设置
该组件以零设置来工作. 额外的便利通行证,运行捆绑 init 命令一次从您的工程根 :
npx @aejkatappaja/phantom-ui init # npm
bunx @aejkatappaja/phantom-ui init # bun
pnpx @aejkatappaja/phantom-ui init # pnpm
yarn dlx @aejkatappaja/phantom-ui init # yarn
它能侦测到你的项目设置 并处理两件事:
JSX 类型声明: 对于反应,固态和克威克,它生成一个 phantom-ui.d.ts 在你身边 src/ 目录是这样 <phantom-ui> jSX承认了这一点。 Vue, Svelte, 和Angular在盒子外工作,没有任何类型的声明.
SSR 水分前水分: 对于Next.js, Nuxt, SvelteKit, Remix, 和克威克, 它添加 import "@aejkatappaja/phantom-ui/ssr.css" 到您的布局文件,以便在水合之前防止内容闪烁(参见 水分前水分控制系统) (中文(简体) ).
没有运行在安装上: 库故意没有 postinstall 钩子,所以你的源 从来没有修改 除非你要求它。 宁愿不运行命令吗? 这两个步骤都很小,下面有记载,用于人工设置.
手动JSX类型的申报
反应 / Next.js / Remix
import type { PhantomUiAttributes } from "@aejkatappaja/phantom-ui";
declare module "react/jsx-runtime" {
export namespace JSX {
interface IntrinsicElements {
"phantom-ui": PhantomUiAttributes;
}
}
}
固态
import type { SolidPhantomUiAttributes } from "@aejkatappaja/phantom-ui";
declare module "solid-js" {
namespace JSX {
interface IntrinsicElements {
"phantom-ui": SolidPhantomUiAttributes;
}
}
}
齐克
import type { PhantomUiAttributes } from "@aejkatappaja/phantom-ui";
declare module "@builder.io/qwik" {
namespace QwikJSX {
interface IntrinsicElements {
"phantom-ui": PhantomUiAttributes & Record<string, unknown>;
}
}
}
手动导入 SSR CSS
将此导入添加到您的根布局文件 :
import "@aejkatappaja/phantom-ui/ssr.css";
| 框架 | 布局文件 |
|---|---|
| Next.js (App路由器) (英语) | app/layout.tsx |
| Next.js (页) | pages/_app.tsx |
| 坚果 | app.vue |
| 斯维尔特基特 | src/routes/+layout.svelte |
| 重混合 | app/root.tsx |
| 齐克 | src/root.tsx |
快开始
<phantom-ui loading>
<div class="card">
<img src="avatar.png" width="48" height="48" style="border-radius: 50%" />
<h3>Ada Lovelace</h3>
<p>First computer programmer, probably.</p>
</div>
</phantom-ui>
设定 loading 以显示发光。 删除以揭示真实内容 。 所有的儿童元素(包括深筑的图像和媒体)在加载时自动被隐藏.
数据获取
奇幻-伊与任何数据采集方法合作. 模式:在装入时提供占位符内容, 完成时提供真实内容 。 占位符文本是不可见的(CSS透明),仅用于生成骨架形状.
TanStack 查询
import { useQuery } from "@tanstack/react-query";
import "@aejkatappaja/phantom-ui";
function UserProfile({ userId }: { userId: string }) {
const { data: user, isLoading } = useQuery({
queryKey: ["user", userId],
queryFn: () => fetch(`/api/users/${userId}`).then((r) => r.json()),
});
return (
<phantom-ui loading={isLoading}>
<div className="card">
<img src={user?.avatar ?? "/placeholder.png"} width="48" height="48" />
<h3>{user?.name ?? "Placeholder Name"}</h3>
<p>{user?.bio ?? "A short bio goes here."}</p>
</div>
</phantom-ui>
);
}
虽然 isLoading 真实的,占位符文本("Placeholder Name", (中文(简体) ), "A short bio goes here.")被隐形地渲染出并被幽灵-伊能产生出与其确切位置和大小相匹配的闪光块. 当查询解决, loading 并显示真实内容.
水资源
import useSWR from "swr";
import "@aejkatappaja/phantom-ui";
function UserProfile({ userId }: { userId: string }) {
const { data: user, isLoading } = useSWR(`/api/users/${userId}`);
return (
<phantom-ui loading={isLoading}>
<div className="card">
<img src={user?.avatar ?? "/placeholder.png"} width="48" height="48" />
<h3>{user?.name ?? "Placeholder Name"}</h3>
<p>{user?.bio ?? "A short bio goes here."}</p>
</div>
</phantom-ui>
);
}
名单
对于数据尚未加载的动态列表,请使用 count 重复单一模板行:
const { data: users, isLoading } = useQuery({
queryKey: ["users"],
queryFn: () => fetch("/api/users").then((r) => r.json()),
});
return (
<phantom-ui loading={isLoading} count={5} count-gap={8}>
{isLoading ? (
<div className="row">
<img src="/placeholder.png" width="32" height="32" />
<span>Placeholder Name</span>
<span>[email protected]</span>
</div>
) : (
users?.map((u) => (
<div key={u.id} className="row">
<img src={u.avatar} width="32" height="32" />
<span>{u.name}</span>
<span>{u.email}</span>
</div>
))
)}
</phantom-ui>
);
框架实例
安全可多次导入 。 自定义内容登记要防止重复 customElements.define() {\fn黑体\fs22\bord1\shad0\3ahbe\4ah00\fscx67\fscy66\2chffffff\3ch808080}所以幽灵在 微前端、 懒惰的模块、 动态导入和 HMR / 热重装 包件最终可以不止一次被初始化的环境.
反应
import "@aejkatappaja/phantom-ui";
function ProfileCard({ user, isLoading }: Props) {
return (
<phantom-ui loading={isLoading} animation="pulse" reveal={0.3}>
<div className="card">
<img src={user?.avatar ?? "/placeholder.png"} className="avatar" />
<h3>{user?.name ?? "Placeholder Name"}</h3>
<p>{user?.bio ?? "A few words about this person go here."}</p>
</div>
</phantom-ui>
);
}
// List with repeat mode
function UserList({ users, isLoading }: Props) {
return (
<phantom-ui loading={isLoading} count={5} count-gap={8}>
<div className="row">
<img src="/placeholder.png" width="32" height="32" />
<span>Placeholder Name</span>
</div>
</phantom-ui>
);
}
阴道
<script setup lang="ts">
import "@aejkatappaja/phantom-ui";
const props = defineProps<{ loading: boolean }>();
</script>
<template>
<phantom-ui :loading="props.loading" animation="breathe" stagger="0.05">
<div class="card">
<img src="/avatar.png" class="avatar" />
<h3>Ada Lovelace</h3>
<p>First computer programmer, probably.</p>
</div>
</phantom-ui>
</template>
丝绒
<script lang="ts">
import "@aejkatappaja/phantom-ui";
export let loading = true;
</script>
<phantom-ui {loading} reveal={0.4} stagger={0.03}>
<div class="card">
<img src="/avatar.png" alt="avatar" class="avatar" />
<h3>Ada Lovelace</h3>
<p>First computer programmer, probably.</p>
</div>
</phantom-ui>
角质
import { Component, signal, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
import "@aejkatappaja/phantom-ui";
@Component({
selector: "app-profile",
schemas: [CUSTOM_ELEMENTS_SCHEMA],
template: `
<phantom-ui [attr.loading]="loading() ? '' : null" animation="pulse">
<div class="card">
<img src="/avatar.png" class="avatar" />
<h3>Ada Lovelace</h3>
<p>First computer programmer, probably.</p>
</div>
</phantom-ui>
`,
})
export class ProfileComponent {
loading = signal(true);
}
固态
import { createSignal } from "solid-js";
import "@aejkatappaja/phantom-ui";
function ProfileCard() {
const [loading, setLoading] = createSignal(true);
return (
<phantom-ui attr:loading={loading() ? "" : null} animation="shimmer" stagger={0.05}>
<div class="card">
<img src="/avatar.png" class="avatar" />
<h3>Ada Lovelace</h3>
<p>First computer programmer, probably.</p>
</div>
</phantom-ui>
);
}
安全部门改革框架(Next.js、Nuxt、SvelteKit、Remix、Quik)
组件需要浏览器API来测量 DOM. 仅导入客户端 :
// Next.js
"use client";
import { useEffect } from "react";
export default function Page() {
useEffect(() => { import("@aejkatappaja/phantom-ui"); }, []);
return <phantom-ui loading>...</phantom-ui>;
}
<!-- Nuxt -->
<script setup>
onMounted(() => import("@aejkatappaja/phantom-ui"));
</script>
<template>
<ClientOnly>
<phantom-ui loading>...</phantom-ui>
</ClientOnly>
</template>
<!-- SvelteKit -->
<script>
import { onMount } from "svelte";
onMount(() => import("@aejkatappaja/phantom-ui"));
</script>
// Qwik
import { component$, useVisibleTask$ } from "@builder.io/qwik";
export default component$(() => {
// eslint-disable-next-line qwik/no-use-visible-task
useVisibleTask$(async () => {
import("@aejkatappaja/phantom-ui");
});
return <phantom-ui loading>...</phantom-ui>;
});
这个 <phantom-ui> 标记可以在服务器的 HTML 中存在。 浏览器将它视为一个未知的元素,直到水合,然后Web组件激活并测量DOM. 内容通常会在服务器上生成,这对SEO有好处.
水分前水分控制系统
在 JavaScript 载入之前, 内容在内 <phantom-ui loading> 可短暂地以可见文本的形式闪出。 包装一个小的CSS文件, 立即隐藏这些内容
import "@aejkatappaja/phantom-ui/ssr.css";
运行 npx @aejkatappaja/phantom-ui init 检测 SSR 框架,并将此导入添加到您的布局文件(例如 . app/layout.tsx 接下来,Js, app.vue 给纳克斯特, +layout.svelte 为斯维尔特基特。 或自加行为行. 如果您使用 CDN 构建, 请直接在您的 <head>数字 :
<style>
phantom-ui[loading] * {
-webkit-text-fill-color: transparent !important;
pointer-events: none;
user-select: none;
}
phantom-ui[loading] img, phantom-ui[loading] svg,
phantom-ui[loading] video, phantom-ui[loading] canvas,
phantom-ui[loading] button, phantom-ui[loading] [role="button"] {
opacity: 0 !important;
}
</style>
属性
| 属性 | 类型 | 默认 | 说明 |
|---|---|---|---|
loading | boolean | false | 显示闪烁的覆盖或真实内容 |
animation | string | shimmer | 动画模式 : shimmer, (中文(简体) ), pulse, (中文(简体) ), breathe,或 solid |
mode | string | skeleton | skeleton (隐藏内容,显示块)或 overlay (保持内容暗淡, 扫光过它来刷新) |
shimmer-direction | string | ltr | 清扫方向: ltr, (中文(简体) ), rtl, (中文(简体) ), ttb,或 btt (只有震动模式) |
shimmer-color | string | rgba(128,128,128,0.3) | 动画渐变扫描的颜色( 仅显示闪光模式) |
background-color | string | rgba(128,128,128,0.2) | 每个闪光块的背景( 所有模式) |
duration | number | 1.5 | 动画周期( 秒) |
stagger | number | 0 | 每个块动画启动之间的秒数延迟 |
reveal | number | 0 | 装入结束时以秒计的淡出持续时间 |
count | number | 1 | 从单一模板重复的骨架行数 |
count-gap | number | 0 | 重复行之间的像素差距 |
fallback-radius | number | 4 | 文本等平面元素的边框半径( px) |
debug | boolean | false | 附有检查索引的每个测量区块大纲 |
loading-label | string | Loading | 屏幕阅读器在装入时宣布的可访问标签(设置为 aria-label(中文(简体) ) |
pierce-shadow | boolean | false | 时间档组件(Stencil, Lit 设计系统)的开阔阴影根内测量 |
使用Web组件设计系统? 见 阴影 DOM 指南 为何
pierce-shadow组件内的措施.
刷新模式( E)mode="overlay"(中文(简体) )
默认 skeleton 模式用于第一个负载:它隐藏内容并显示占位符块。 对于更新/ stale- while- revalidate 状态, 您已经得到了结果, 并且正在调试, 使用 mode="overlay"数字 :
<phantom-ui loading mode="overlay">
<div class="grid"><!-- the previous result --></div>
</phantom-ui>
现有的内容保持可见并被暗化,而一闪光闪烁地扫遍了每个元素(结构意识,和骨架一样). 在刷新期间无法点击( I)pointer-events: none),因此用户不能对已死数据采取行动,但可以读取,并保存在无障碍树上(aria-busy 宣布最新情况)。 慢慢来 --phantom-content-opacity (默认) 0.5,设置为 1 完全不透明,只有光. count 编号 : count-gap 这里不适用.
扫荡就是 shimmer 动画. 与 pulse 或者说 breathe 光是每个元素的固定面纱 取而代之的是动画, solid 保持静态,当系统要求减少运动时,使用相同的静态面纱. 每一次刷新都能看到.
精细的控制
数据属性允许您控制哪些元素得到闪光的处理,以及如何测量它们:
data-shimmer-ignore 在加载时保持一个元素及其所有后缀的可见性和交互性(从未制作) inert) (中文(简体) ). 用于标识,品牌标志,或应始终显示的活指标.
data-shimmer-no-children 将元素捕获为单一的闪光块,而不是重现其子孙. 对密集的、应作为单一占位符出现的计量组有用.
data-shimmer-width 编号 : data-shimmer-height 覆盖元素的已测量维度( 像素) 。 可用于在骨架生成时尚未有维度的动态大小元素(例如图像未明确) width编号 :height由联合来文填入的容器)。 零尺寸的元素通常被跳过。 这些属性让你强迫一个骨干块.
<phantom-ui loading>
<div class="dashboard">
<div class="logo" data-shimmer-ignore>ACME</div>
<div class="kpi-row" data-shimmer-no-children>
<span>$48.2k</span>
<span>2,847 users</span>
<span>42ms p99</span>
</div>
<img src="/hero.jpg" data-shimmer-width="600" data-shimmer-height="400" />
<div class="content">
<p>Each leaf element here gets its own shimmer block.</p>
</div>
</div>
</phantom-ui>
重复模式
在加载动态列表或表格时,您往往还没有数据来渲染N行. 这个 count 属性允许您定义一个单一模板元素,并从中生成多个骨架行:
<phantom-ui loading count="5" count-gap="8">
<div class="user-row">
<img src="avatar.png" width="32" height="32" />
<span>John Doe</span>
<span>[email protected]</span>
</div>
</phantom-ui>
组件测量模板一次,然后垂直复制每个计数的骨干块. count-gap 增加重复行之间的间隔(以像素计). 何时 loading 删除时,只显示真正的模板元素.
在数据加载前列表为空的框架循环中, 这样做是有用的 :
// React
<phantom-ui loading={!users} count={5} count-gap={8}>
<div class="row-template">
<img src="/placeholder.png" width="32" height="32" />
<span>Placeholder Name</span>
<span>[email protected]</span>
</div>
</phantom-ui>
如何运作
-
您的真实内容在DOM中呈现
color: transparent和媒体元素隐藏。 用 CSS 绘制的图标mask-image(包括::before编号 :::after)在运行时检测出并隐藏。 集装箱背景和边界保持可见,保留了天然卡/分节大纲. -
组件走 DOM 树并识别出"叶"元素:文本节点,图像,按钮,输入,以及没有子元素的任何东西. 集装箱潜水器被重复使用,而不是被捕获.
-
每个叶子元素都用
getBoundingClientRect()相对主机。 边框半径从getComputedStyle()。表格单元格获得特殊处理,以测量实际文本宽度,而不是单元格宽度. -
一个绝对位置的叠加使每个被测量的元素有一个闪光块,一个CSS梯度动画横扫每个块.
-
A 类
ResizeObserver, (中文(简体) ),MutationObserver媒体load收听器在布局变化(窗口调整大小,内容注入,DOM突变,或图像/视频完成加载)时自动重新测量. 与pierce-shadow穿孔的阴影根也被监视着. -
何时
loading被删除,覆盖被销毁,真实内容被披露.aria-busy自动设置在主机元素上,以沟通加载状态以辅助技术,占位符内容被制作inert装入时, 它会远离标签顺序和可访问性树( 标出元素)data-shimmer-ignore保持互动).
业绩
DOM测量管道快. 在克罗梅基准:
| 要件 | 叶节点 | 时间 |
|---|---|---|
| 100个 | 334 (韩语) | ~ 20 ms 时 |
| 500个 | 1 667个 | ~25ms 时 |
| 1 000个 | 3 334个 | ~31ms 时 |
即使拥有了1000个元素(远不止一个典型的骨架屏幕),全量度-使周期在一帧中完成. 不需要拆卸或虚拟化.
CSS 自定义属性
您可以使用 CSS 自定义属性而不是(或除此之外) 属性从外部对组件进行样式化 :
phantom-ui {
--shimmer-color: rgba(100, 200, 255, 0.3);
--shimmer-duration: 2s;
--shimmer-bg: rgba(100, 200, 255, 0.08);
}
对于活生生的他们(例如,一个黑暗模式的切换,没有重新投降),通过 var() 通过一个属性来引用,使其重新溶解于涂料上:
<phantom-ui shimmer-color="var(--brand-shimmer)" background-color="var(--brand-bg)">
自定义元素外观
货包船a custom-elements.json 显示,它使 IDE 自动完成,Storybook 自动docs, 和框架工具可以完整地显示属性、属性、插槽和类型.
套装大小
CDN建筑(包括了)为~34kb /~ 11.5kb gzipped.
当用同捆绑器作为ES模块时,Lit很可能已经在你的依赖树上,使组件成本降至2kb以下.
发展
bun install
bun run storybook # dev server on :6006
bun run build # tsc + custom elements manifest + CDN bundle
bun run lint # biome check
bun run lint:fix # biome auto-fix
bun run test # browser tests (Chromium)
bun run test:all # browser tests (Chromium + Firefox + WebKit)
bun run playground # local server to test the component
这个 examples/ 目录包含React,Vue,Solid,Angular,和Qwik的测试应用程序,每个软件都被接通本地包.
捐款
错误报告,特性请求,和PR欢迎. 读取 贡献指南 财务报告和审定财务报表 行为守则 在打开公关前.
关于安全弱点,见 安全政策。 。 。 .
鸣谢
DOM测量叠加技术基于前作的艺术 页面-skeleton-webpack-插件 (2018年)和 @findify/skeleton - 生成器 (~2019) (中文(中国大陆) ). 奇幻-伊再设想这个概念是一个单一的通用Web组件,而不是框架特定的适配器.
许可证
麻省理工学院