backtop —— 回到頂部
This content is not available in your language yet.
長頁面捲到底部,使用者想回頂部只能瘋狂滾輪。backtop 是一個右下角的浮動按鈕,捲到一定距離才出現、點了平滑回頂。它邏輯不複雜,但捲動 API 的相容性坑特別多——捲動根是誰、平滑捲動怎麼降級、捲動結束怎麼探測,core/backtop 把這些全收了。
- 各端實作
<LhBackTop>:長清單、長文件、後臺資料表格的回頂入口。 - 頁面高度明顯超過一螢幕(通常 2~3 螢幕以上)時才值得出現。
- 不該用它:短頁面(一螢幕都不到,按鈕永遠不出現還佔程式碼);本身就是滾輪/手勢能快速回頂的行動端原生頁(行動端系統手勢已覆蓋,h5 內嵌頁視情況)。
import { LH_BACKTOP_VISIBILITY_HEIGHT_DEFAULT, LH_BACKTOP_SCROLLEND_DELAY, getBackTopClassNames, resolveScrollTarget, getScrollTop, backToTop, isBackTopVisible, supportsScrollEnd,} from '@lh-ui/share/core/backtop'捲動根的優先級:document.scrollingElement 優先於 document.documentElement。這不是潔癖——有些頁面(用了 body { overflow } 的)真正捲動的是 body,documentElement.scrollTop 恆為 0,可見性判定會徹底失效。scrollingElement 是 W3C 為統一這個混亂發明的標準屬性。
getScrollTop 用 instanceof HTMLElement 而非 instanceof Window:註解寫了「跨 realm 安全」——如果頁面有 iframe,iframe 傳入的 Window 物件不等於目前 realm 的 Window 建構函式,x instanceof Window 會誤判為 false。用 HTMLElement 判定(元素不會有這個問題)就規避了。這是很細節但真實存在的坑。
平滑捲動的兩級降級:① prefers-reduced-motion 開啟時降級為 auto(無動畫)——前庭功能障礙使用者對持續捲動會眩暈,這是無障礙硬要求;② 目標元素沒有 scrollTo 方法(舊 jsdom 等環境)時直接寫 scrollTop = 0。
supportsScrollEnd 的特性探測:scrollend 事件是 Chromium 114+ / Safari 18.2+ 才有的——用它可以在捲動真正靜止時精確重新整理可見狀態(捲動中頻繁讀 scrollTop 有效能成本)。不支援時由呼叫方退回 scroll 事件 + LH_BACKTOP_SCROLLEND_DELAY(150ms)防抖模擬。
可見性是純函式:isBackTopVisible(scrollTop, height) 只比較數值——UI 層自己監聽捲動、自己存 scrollTop,core 不持有任何響應式狀態。這也讓單測可以直接斷言數值邊界。
| 函式 / 常數 | 簽名 | 傳回 | 作用 |
|---|---|---|---|
getBackTopClassNames |
(state: { visible }) => string[] |
類別名 | 可見狀態驅動顯隱動畫 |
resolveScrollTarget |
(target?: string | HTMLElement) => HTMLElement | Window |
捲動目標 | 選擇器未命中回退 window |
getScrollTop |
(target) => number |
捲動距離 | 容器/視區自適應 |
backToTop |
(target, smooth) => void |
無 | 平滑回頂(含降級) |
isBackTopVisible |
(scrollTop, visibilityHeight) => boolean |
是否可見 | 閾值判定 |
supportsScrollEnd |
(target) => boolean |
是否支援 | 特性探測 |
常數:LH_BACKTOP_VISIBILITY_HEIGHT_DEFAULT = 200、LH_BACKTOP_SCROLLEND_DELAY = 150。
export interface LhBackTopState { visible: boolean // 當前是否可見(驅動顯隱類別名) target?: string | HTMLElement visibilityHeight?: number smooth?: boolean // 是否平滑捲動(預設 true)}// 按鈕的無障礙屬性在 UI 層組合:const aria = { type: 'button', 'aria-label': '回到顶部', // 圖示按鈕無文本,必須給標籤}type="button":回頂按鈕如果落在<form>裡,預設type="submit"會觸發表單提交——這是最常見的小 bug。- aria-label 必填:純圖示按鈕(上箭頭)對螢幕閱讀器是無名的,
aria-label="回到顶部"讓它可被播報。 - aria-hidden 與可見性同步:不可見時從可訪問性樹移除(
aria-hidden="true"或直接v-if卸載),避免螢幕閱讀器導航到一個看不見的按鈕。 - 鍵盤可達:按鈕就是原生
<button>,Tab 聚焦 + Enter/Space 觸發,天然支援。不要做成<div @click>。 - prefers-reduced-motion 的雙重意義:既是無障礙要求(前庭安全),也避免低階裝置上平滑捲動卡頓。
backToTop內部已處理,UI 層無需判斷。
import { resolveScrollTarget, getScrollTop, isBackTopVisible, backToTop, supportsScrollEnd, LH_BACKTOP_VISIBILITY_HEIGHT_DEFAULT, LH_BACKTOP_SCROLLEND_DELAY,} from '@lh-ui/share/core/backtop'
isBackTopVisible(200, 200) // true(>= 閾值)isBackTopVisible(199, 200) // falseisBackTopVisible(0, 200) // false
supportsScrollEnd(window) // 現代瀏覽器 truesupportsScrollEnd(someEl) // Chromium 114+ / Safari 18.2+ 才 true// 監聽策略:支援 scrollend 就只在靜止時判定,否則 scroll + 防抖const target = resolveScrollTarget(props.target)let visible = ref(false)
function refresh() { visible.value = isBackTopVisible( getScrollTop(target), props.visibilityHeight ?? LH_BACKTOP_VISIBILITY_HEIGHT_DEFAULT, )}
if (supportsScrollEnd(target)) { target.addEventListener('scrollend', refresh)} else { let timer: ReturnType<typeof setTimeout> | undefined target.addEventListener('scroll', () => { clearTimeout(timer) timer = setTimeout(refresh, LH_BACKTOP_SCROLLEND_DELAY) })}
function onClick() { backToTop(target, props.smooth ?? true)}<!-- Vue3 --><template> <button type="button" class="lh-backtop" :class="getBackTopClassNames({ visible })" aria-label="回到顶部" :aria-hidden="!visible" @click="backToTop(target, true)" > <LhIcon name="arrow-up" /> </button></template>// React:useEffect 註冊/卸載捲動監聽(依賴 target 與 visibilityHeight);// smooth 由 props 控制,reduced-motion 由 core 內部處理// uniapp:<button open-type?> 不適用;用 <view> + @tap;// 小程序端頁面捲動用 onPageScroll(uniapp 內置),元素捲動用 scroll-view 的 scroll 事件| 類別名 | 語義 |
|---|---|
.lh-backtop |
按鈕容器(fixed 右下角) |
.lh-backtop--visible |
可見(驅動淡入/上滑入場動畫) |
預設隱藏是 opacity: 0; visibility: hidden(用 visibility 而非 display,保留過渡動畫);圓形按鈕 --lh-backtop-size(如 40px);底色 --lh-bg-color + 陰影 --lh-box-shadow-base;z-index --lh-z-index-backtop。
- 捲動監聽要卸載:元件銷毀時
removeEventListener,否則引用著已移除的 DOM 導致記憶體漏失。Vue 的onUnmounted/ React 的 useEffect 清理函式。 window監聽的特殊性:scroll事件在 window 上是被動且高頻的,必須防抖或用 scrollend;直接每次讀getScrollTop在超長頁面上會卡。- 目標元素自己捲的場景:表格內部捲(不是頁面捲)要把
target指向表格容器——否則按鈕的可見性跟著頁面捲動走,與實際要回頂的容器無關。 - 平滑捲動被 CSS 覆蓋:如果頁面有
html { scroll-behavior: smooth },scrollTo({ behavior: 'auto' })會被 CSS 覆蓋——backToTop的 reduced-motion 降級會失效。需要嚴格降級時 UI 層要臨時去掉該 CSS。 - iOS Safari 的捲動慣性:iOS 的捲動有慣性,scrollend 在慣性結束才觸發;防抖兜底的 150ms 在快速滑動時可能過早重新整理可見狀態(按鈕閃一下)。這是已知的折中。
- 可見閾值 200 的含義:約半螢幕高度(視視區高度而定)。文件類頁面可以調小(100),工具類頁面調大(500)——
visibilityHeightprop 就是幹這個的。 - 按鈕不要遮擋內容:fixed 定位 + 右下角,行動端要避開底部導覽列(留出安全區高度)。
@lh-ui/share/core/backtop → isBackTopVisible( )(填好参数后点运行)Copyright © 2024-2026 FengMo WenXue