跳转到内容
关系图谱

reading-progress:顶部阅读进度条

1,674 字 6 分钟

属性 1
series
VaultKit 包指南

@vaultkit/vitepress-plugin-reading-progress 是一个页面顶部阅读进度条 Vue 组件:随滚动从左到右填充,让读者对「还剩多少」有直觉感知。零依赖、零配置即可用——现在就往下滚动本页,注意浏览器窗口最顶端那条与主题品牌色同色的细线,它就是本组件的现场演示。

整个包只导出一个组件、三个 props、一个 CSS 变量,但每一处实现都有讲究:passive 监听 + rAF 节流保证滚动零卡顿,SSR 首帧透明隐藏避免闪线,prefers-reduced-motion 下自动关过渡。本文把这些细节逐一讲透。

为什么需要它

长文阅读时,右侧滚动条给出的是整页位置,而且在 macOS / 移动端默认隐藏。顶部进度条是更轻的方案:始终可见、不占布局空间(position: fixed + pointer-events: none)、视觉负担只有 2px。

社区同类组件常见两个毛病:

  1. 滚动监听不节流——scroll 事件每帧可触发多次,直接在回调里读 scrollHeight 会强制同步布局(layout thrashing)
  2. SSR 闪线——服务端渲染出一条 0% 的色线,客户端水合前它以未知宽度闪现

本包对这两点都有针对性设计(见下文设计机制),且不引任何滚动库,依赖只有 vue ^3.5.0(peer)。

安装

sh
pnpm add -D @vaultkit/vitepress-plugin-reading-progress

使用 @vaultkit/vitepress-theme-vault 主题则开箱即带(features.readingProgress 开关控制),无需单独安装。

快速上手

推荐挂载在 VitePress 布局的 #layout-top 插槽——它位于整个布局最顶端,语义正好:

.vitepress/theme/Layout.vue
vue
<script setup lang="ts">
import { ReadingProgress } from '@vaultkit/vitepress-plugin-reading-progress'
import { useData } from 'vitepress'
import DefaultTheme from 'vitepress/theme'

const { frontmatter } = useData()
</script>

<template>
  <DefaultTheme.Layout>
    <template #layout-top>
      <!-- 显隐由宿主决定:如首页不渲染 -->
      <ReadingProgress v-if="frontmatter.layout !== 'home'" aria-label="页面阅读进度" />
    </template>
  </DefaultTheme.Layout>
</template>

然后把自定义 Layout 接入主题入口:

.vitepress/theme/index.ts
ts
import DefaultTheme from 'vitepress/theme'
import Layout from './Layout.vue'

export default {
  extends: DefaultTheme,
  Layout,
}
Important

本包发布的是含 .vue 源文件的产物,消费方 VitePress 项目必须把它加进 vite.ssr.noExternal,否则 SSR 构建会报错:

ts
// .vitepress/config.ts
import { defineConfig } from 'vitepress'

export default defineConfig({
  vite: { ssr: { noExternal: ['@vaultkit/vitepress-plugin-reading-progress'] } },
})

公开导出一览

导出形态说明
ReadingProgressVue 组件(具名导出)进度条本体
默认导出同上import ReadingProgress from '...' 亦可

组件刻意不导出任何「隐藏逻辑」:显隐完全交给宿主的 v-if(首页不渲染、归档页不渲染等策略都在使用侧表达),组件内部只管「渲染时把进度算对」。

Props 逐项详解

只有三个 props,全部可选。

height

  • 类型:number,默认 2
  • 语义:进度条高度,单位 px。2 是「可感知但不打扰」的平衡点;想更醒目可调到 3~4
vue
<ReadingProgress :height="3" />

注意组件不占布局空间(position: fixed),调高不会把页面内容往下推,但会更多地压在导航栏顶部之上。

zIndex

  • 类型:number,默认 1000
  • 语义:层叠顺序。默认值压过 VitePress 导航栏(--vp-z-index-nav 为 30 量级),保证进度条始终可见。如果你的站点有更高层级的自定义浮层(如公告条)需要盖住进度条,把它们的 z-index 抬到 1000 以上,或者反向调低本组件:
vue
<ReadingProgress :z-index="50" />

ariaLabel

  • 类型:string,默认 'Reading progress'
  • 语义:无障碍标签。组件输出 role="progressbar" + aria-valuenow/valuemin/valuemax,屏幕阅读器会播报「页面阅读进度,45%」这样的语义。中文站点建议注入:
vue
<ReadingProgress aria-label="页面阅读进度" />

三项合起来的完整示例:

vue
<ReadingProgress
  :height="3"
  :z-index="1000"
  aria-label="页面阅读进度"
/>

CSS 变量

变量默认值说明
--reading-progress-colorvar(--vp-c-brand-1)进度条颜色

缺省取 VitePress 品牌色,主题换色时进度条自动跟随,明暗两态也无需分别配置。想单独定制(比如做成渐变色打底的纯色)只需一行:

css
/* .vitepress/theme/custom.css */
:root {
  --reading-progress-color: #e67e22;
}

设计机制

rAF 节流:每帧至多读一次 scrollHeight

scroll 事件的触发频率可以远高于屏幕刷新率,而回调里读 document.documentElement.scrollHeight 会触发强制布局。本组件的监听回调只做一件事——若本帧尚未排程,就 requestAnimationFrame(update)

ts
function schedule() {
  if (!frame)
    frame = requestAnimationFrame(update)
}

于是无论 scroll / resize 每帧触发多少次,布局读取每帧至多一次,滚动主线程零卡顿。这个方案还有个隐性好处:文档高度变化天然跟随。VitePress 路由切换、图片懒加载完成都会改变 scrollHeight,而这些行为几乎必然伴随 scroll 或 resize 事件(或下一次用户滚动),进度会在下一帧自动校正——不需要再挂 ResizeObserver / MutationObserver 这类额外观察器。

进度计算本身是一行防御性算术:总可滚距离 scrollHeight - innerHeight,为 0 或负(短页)时进度归 0,否则钳制在 0~100。

SSR 防闪线:首帧透明 + visible 类门控

监听在 onMounted 里才绑定,所以 SSR 输出与客户端首帧的 progress 都是 0——两端一致,无水合差异。但仅仅宽度为 0 还不够:如果用户从页面中部刷新(浏览器恢复滚动位置),水合后进度会从 0 突跳到实际值,期间可能闪出一条色线。所以样式层再加一道门:

css
.reading-progress-bar {
  opacity: 0;
  transition:
    width 0.15s linear,
    opacity 0.3s;
}

.reading-progress-bar.visible {
  opacity: 1;
}

progress > 0 才挂 visible 类,配合 0.3s 的透明度过渡,进度条是「淡入」而非「闪现」——页面加载期间顶部永远干净。

动画偏好

prefers-reduced-motion: reduce 时移除全部过渡,宽度直接跳变。平滑动画对前庭障碍用户是负担,这一行媒体查询是无障碍的基本修养。

高级与边界

  • 只在文章页显示:本站的做法是主题层用 features.readingProgress 配置门控,更细粒度可以按 frontmatter 判断,如 v-if="frontmatter.blogPost"
  • 横向书写模式:组件假设纵向滚动(读 scrollY / innerHeight),RTL 站点进度方向仍是从左到右,竖排(writing-mode: vertical-rl)场景不适用
  • 嵌套滚动容器:进度基于 window 滚动计算,若正文在自定义 overflow: auto 容器内滚动,进度不会动——这类布局需要 fork 改造为容器监听
  • 多实例:组件无全局状态,理论上可挂多个(如页顶 + 页底各一条),互不干扰

常见问题

Q:进度条一直不出现? 检查页面是否可滚——短页 scrollHeight <= innerHeight 时进度恒为 0,组件保持透明,这是刻意行为而非 bug。

Q:进度条被导航栏盖住了? 你的主题可能改过导航 z-index,把 zIndex prop 调到更高即可。

Q:SSR 构建报 .vue 文件无法解析? 没配 vite.ssr.noExternal,见上文安装一节。

Q:想要底部进度条或环形进度? 本组件只做顶部线形这一件事。环形「回顶部 + 进度」组合可以关注同系列的 scroll-navigator:回顶部与到底部浮钮,两者常配合使用。

相关链接