skeleton —— 骨架屏
هذا المحتوى غير متوفر بلغتك بعد.
骨架屏是「載入完成前」的佔位:用灰色塊模擬即將出現的版面配置,讓使用者對「等會兒會看到什麼」有預期。core/skeleton 計算文字行的寬度序列(末行縮短,像真實段落),並把「這是佔位、不是內容」的無障礙訊號準備好。
- 各端實作
LhSkeleton:卡片載入、列表載入、文章載入。 - 需要在任何區域表達「正在載入」的視覺時,骨架比 loading 轉圈更「貼合未來版面配置」。
- 不該用它:載入時間極短(小於 300ms)的場景——閃一下骨架反而干擾;也不該用於「資料為空」,那是
LhEmpty。
import { getSkeletonClassNames, getSkeletonItemClassNames, getSkeletonRowWidths, getSkeletonAria,} from '@lh-ui/share/core/skeleton'import type { LhSkeletonState, LhSkeletonVariant } from '@lh-ui/share/core/skeleton'getSkeletonRowWidths(rows) 的末行 60% 是一個細節但重要的設計:真實文字段落的最後一行幾乎總是不滿的。全等寬的骨架看起來像「條紋桌布」而非「文字」,這個 60% 讓佔位有「文字感」。
rows > 1 才縮短末行:只有一行時它就是完整的一行(單行縮短反而像標題被截斷)。rows <= 0 傳回空陣列,不拋錯。
getSkeletonAria() 恆傳回 { 'aria-hidden': true }——骨架內容零資訊量,讀屏軟體不應播報它。但僅隱藏不夠:原始碼註解要求 UI 層同時在外層容器掛 aria-busy="true",這樣資料就緒時讀屏軟體會重新播報真實內容。這兩個屬性是一對:aria-hidden 摘掉噪音,aria-busy 標記「稍後再讀」。
| 函式 | 簽名 | 傳回 | 作用 |
|---|---|---|---|
getSkeletonClassNames |
(state: LhSkeletonState) => string[] |
類別名陣列 | 動畫、帶頭像修飾 |
getSkeletonItemClassNames |
(variant: LhSkeletonVariant, animated: boolean) => string[] |
類別名陣列 | 佔位塊形狀與動畫 |
getSkeletonRowWidths |
(rows: number) => string[] |
寬度序列 | 生成文字行寬度 |
getSkeletonAria |
() => LhAriaAttrs |
aria 屬性包 | 恆 { 'aria-hidden': true } |
輸出範例:
getSkeletonRowWidths(3) // ['100%', '100%', '60%']getSkeletonRowWidths(1) // ['100%']getSkeletonRowWidths(0) // []getSkeletonItemClassNames('text', true)// ['lh-skeleton-item', 'lh-skeleton-item--text', 'lh-skeleton-item--animated']export type LhSkeletonVariant = 'text' | 'rect' | 'circle'
export interface LhSkeletonState { animated: boolean avatar: boolean}// UI 層的外層容器const aria = { 'aria-busy': true } // 標記「內容載入中,就緒後請重讀」// 骨架元素本身const skeletonAria = getSkeletonAria() // { 'aria-hidden': true }常見的錯誤是只做其中一件:只 aria-hidden 會讓讀屏使用者在資料就緒後錯過新內容(它以為讀過了);只 aria-busy 會讓讀屏軟體嘗試播報骨架的空白內容。
import { getSkeletonRowWidths, getSkeletonItemClassNames } from '@lh-ui/share/core/skeleton'
getSkeletonRowWidths(4)// ['100%', '100%', '100%', '60%']
getSkeletonItemClassNames('circle', false)// ['lh-skeleton-item', 'lh-skeleton-item--circle']<!-- Vue3 --><div :class="getSkeletonClassNames({ animated, avatar })" aria-busy="true"> <div v-if="avatar" class="lh-skeleton__avatar"> <div :class="getSkeletonItemClassNames('circle', animated)" /> </div> <div class="lh-skeleton__content"> <div v-for="(w, i) in getSkeletonRowWidths(rows)" :key="i" :class="getSkeletonItemClassNames('text', animated)" :style="{ width: w }" v-bind="getSkeletonAria()" /> </div></div>// Reactconst widths = getSkeletonRowWidths(rows)return ( <div className={getSkeletonClassNames({ animated, avatar }).join(' ')} aria-busy={true}> <div className="lh-skeleton__content"> {widths.map((w, i) => ( <div key={i} className={getSkeletonItemClassNames('text', animated).join(' ')} style={{ width: w }} {...getSkeletonAria()} /> ))} </div> </div>)// uniapp:動畫在非 h5 端可能不流暢,各端 UI 層可按平台降級關閉 animated| 類別名 | 語義 |
|---|---|
.lh-skeleton |
基礎類:版面配置 |
.lh-skeleton--animated |
啟用 shimmer 掃光動畫 |
.lh-skeleton--with-avatar |
頭像 + 文字的組合版面配置(頭像左側留位) |
.lh-skeleton-item |
單個佔位塊 |
.lh-skeleton-item--{text|rect|circle} |
形狀:文字行 / 矩形 / 圓形 |
.lh-skeleton-item--animated |
掃光動畫 |
.lh-skeleton__avatar / .lh-skeleton__content |
頭像區與內容區 |
佔位塊顏色用 --lh-skeleton-* 令牌(比背景略深的灰 + 掃光高光),暗色下自動調暗。
rows為 0 或負數傳回空陣列,UI 層不會渲染出行——這是合法輸入,不是錯誤。- 骨架的版面配置應當與真實內容一致(同樣的卡片結構、同樣的行數),否則載入完成會有版面配置跳動。用
LhCard的loading狀態比單獨放骨架更省心。 - shimmer 動畫對部分使用者是前庭刺激源;尊重
prefers-reduced-motion的責任在樣式層(@media裡停用動畫),core 層不感知。 aria-busy應掛在資料容器上,不是骨架自身——骨架銷毀後aria-busy要隨之移除。
@lh-ui/share/core/skeleton → getSkeletonRowWidths( )返回值
(填好参数后点运行)Copyright © 2024-2026 FengMo WenXue