工程手记

首页改成八屏

把博客首页从连续滚动改成八屏整屏分页。吸附用 Lenis 自带能力而不是 CSS 原生,断点按实测调整,每一档的溢出与填充率都有数字。

STRUCTURE ── 篇幅剖面10 节 · 点击跳转

这个博客的首页原来是连续滚动。改成了整屏分页,一页一件事,共八屏:

首屏 / 自我介绍 01 / 自我介绍 02 / 做过的事 / 精选复盘 / 项目 / 专栏 / 联系

一屏容纳是逐档实测调出来的,不是估的。

一、吸附用谁的

第一个决定是吸附用哪套机制。

原生有 scroll-snap,CSS 几行就能写。但这里用不了——页面上的平滑滚动是 Lenis 提供的,它用 JS 驱动 scrollTop

吸附用 Lenis 自带的 Snap 而不是 CSS scroll-snap——Lenis 是用 JS 驱动 scrollTop 的,和原生吸附会互相抢控制权、滚起来发抖。Snap 挂在同一个 Lenis 实例上没有这个问题,onSnapComplete 还能驱动指示器。

两个机制都想控制滚动位置,结果就是抖。

挂在同一个实例上之后,吸附完成后还能拿到回调,右侧的页码指示器直接跟着它更新。

二、mandatory 是错的

第一版用 type: 'mandatory'。用户反馈手感是「被抢走了」。

原因是 mandatory 在用户还在滑的时候就强行把页面拽向最近一页。滑动过程被插手,手感不是吸附,是失控。

改成 proximity

type: 'proximity',
distanceThreshold: '30%',
duration: 1.1,
debounce: 500,

三条参数各管一件事:

  • proximity 只在停下来且已经接近边界时才轻推一把。
  • distanceThreshold: '30%' 定义「接近」——落点离边界不到 30% 视口高才吸附,停在页面中间就让它停着。
  • debounce: 500 等惯性完全停下来再判断,不在滑行中途插手。

debounce 从 220 提到 500:

debounce 太小会在惯性还没停时就抢着吸附,手感发涩。

实测结果:

滑动全程 0 反向帧(原来会被拽回)
停在离边界 306px(超过 30% 阈值)时保持不动
停在边界附近才对齐整页

三、一屏装不装得下

分页的前提是内容能装进一屏。这一条逐档量过。

首屏原高 921px:

  • 1440×900 超出 89px
  • 1366×768 超出 201px

精选复盘 788px,在 1366×768 超出 88px。

两个方向可选:砍内容,或者按档收紧间距。选了后者,分三档:

min-width: 1024px and max-height: 820px   深压
min-width: 1024px and max-height: 900px   只压首屏
min-width: 1024px and min-height: 1000px  放大间距吃大屏留白

中间那档的调整方式值得记一笔。为了挤出 27px,第一版直接砍了字号:

font-size: clamp(46px, 9.2vw, 132px)  →  clamp(30px, 5.4vw, 62px)

砍掉一半多。首屏标题是整页的主体,砍字号会让整屏塌下来。改成全部从内边距要:

@media (min-width: 1024px) and (max-height: 900px) {
  .page-hero :deep(.hero) { padding-top: 20px; padding-bottom: 24px; }
  .page-hero :deep(.hero-foot) { margin-top: 26px; }
}

只有 700px 可用高度那一档才让一档字号。三档实测:

1440×900    132px
1920×1080   132px
1366×768     92.9px

四、检测判据写错过一次

判断页面有没有溢出,第一版用的是:

scrollHeight > clientHeight

这个判据永远为假。页面用的是 min-height,内容超了页面会自己长高——scrollHeightclientHeight 一起变大,比值不变。

改成直接比可用高度:

// 检测判据从 scrollHeight > clientHeight 改为直接比可用高度——
// 页面用的是 min-height,内容超了页面会自己长高,前者永远比不出溢出

这个错误值得单独说:判据本身写错了,所以「三档零溢出」这个结论在修正之前是不成立的。

修正之后三档均为 8/8 页零溢出。

五、不分页的三种情况

窄屏(< 1024px)
矮视口(< 620px)
减弱动效(prefers-reduced-motion)

三种都退回连续滚动。

前两种有实测依据:

实测 390px 下自我介绍 1448px、精选复盘 1114px,强行一屏只会截断内容

第三种是硬要求。整屏吸附会强制改变滚动位置,这和「尊重减弱动效偏好」直接冲突。CSS 里也要同步禁用:

@media (prefers-reduced-motion: reduce) {
  .page { min-height: 0; }
  .pager { display: none; }
}

六、分页之后,每页只剩半屏内容

八屏分配完之后,出现一个反向问题:每页内容太少。

实测 1440×900 下多数页只填了 47%~63%,「做过的事」那页 63% 是空的。

既然分了页,每页就得撑得住。逐页补实:

做过的事:加页首概览条(项目数/时间跨度/人手),每项补详细说明、关键指标与技术标签。308px → 548px
项目:3 张卡补到 6 张,两行三列。362px → 511px
联系:补引言、about 与 timeline 两个入口、站点信息带。452px → 791px
专栏:每张卡补该栏最新一篇标题。512px → 597px
自我介绍 01:四张能力卡各补首组的真实条目。504px → 596px
自我介绍 02:补技术栈横带。522px → 603px

填充率从 47%~63% 提到 61%~100%。

同一轮里把两个区块的入场动画也补了常驻循环,因为:

实测入场动画本来就在跑(轴线 scaleX 0→0.99、圆点带回弹、卡片 clip-path 从 78% 揭到 4%、指标读数往上跳),问题是太隐蔽:一次性、1 秒内结束、触发点又在区块刚露头时,等正眼看过去通常已经播完。

触发点 top 88% → 95%,轴线 0.9s → 1.25s,圆点与节点错峰 0.11 → 0.16

七、断点从 900 降到 820

第一版把「矮视口」定在 900px 高。1440×900 的屏幕因此被当成矮视口,白白砍掉了列表摘要——精选复盘从 788px 缩到 389px。

而 1440×900 明明有空间。断点降到 820:

820 以下   深压
900 以下   只压首屏
1000 以上  放大间距吃掉大屏留白

八、两个 CSS 优先级问题

首屏被挤成一个框。

.page { padding: 24px 0; }
.page-hero { padding: 0; }

.page-hero { padding: 0 } 写在媒体查询之前,被后面同优先级的 .page { padding: ... } 覆盖了。首屏于是四边都留出内边距,看起来像一个框。

修法是在每一档里重新声明一次:

/* 首屏必须满幅出血。这行不能省——上面那条 .page 同优先级且更靠后,
   会把顶部声明的 .page-hero { padding: 0 } 覆盖掉。 */
.page-hero { padding: 0; }

标题被大屏规则压小。

大屏档里有一条:

.page :deep(.big) { font-size: clamp(34px, 3.4vw, 56px); }

首屏主标题的类名也是 .big。1920×1080 下它从 132px 被压到 56px,首屏整个塌掉。

改成排除首屏:

/* 必须排除首屏——hero 里的 .big 是那个巨大的主标题,
   被这条命中会从 132px 压到 56px,整个首屏塌掉 */
.page:not(.page-hero) :deep(.big) { font-size: clamp(34px, 3.4vw, 56px); }

两个问题的成因相同:同一个类名在两种语义下被复用.page 既是容器也是列表页的内边距来源,.big 既是区块小标题也是首屏主标题。分开之后就没这个问题。

修完实测:hero 左 0、底 1、右 10(右侧那 10px 是站点滚动条本身的宽度),标题 132px 满值,8/8 页仍零溢出。

九、加载页期间的一个时序漏洞

这个站有一个启动加载页,首次访问时盖几秒。加载页盖着的时候,入场动画被暂停了——否则动画会在遮挡期间自己跑完。

副作用是页面切换的幕布动画也被冻住:

实测(加载页 3s 时点击):t=745 路由已切到 /columns 而幕布 display:none,t=1406 幕布才出现并扫过。

路由在没有幕布的情况下切换了,等加载页结束、时间线恢复之后,被冻住的补间才补播。

结论是补一行守卫:加载页盖着时直接放行,不放幕布。理由是:

真实用户点不到——加载页是全屏遮罩。但浏览器前进/后退、程序化跳转仍可能在窗口内触发导航。

十、整屏分页的账

这套改动最后收敛成一张数字表:

项目数值
页数8
吸附类型proximity,阈值 30%,防抖 500ms
吸附时长1.1s
分页生效条件宽 ≥ 1024px 且 高 ≥ 620px 且 未开启减弱动效
溢出三档均 0/8
填充率61%~100%(改前 47%~63%)
首屏标题132px(1440×900、1920×1080)、92.9px(1366×768)

每一条都有一次实测在后面。这类改动没别的办法——「一屏装不装得下」这件事只能量,量完才知道该压间距还是该砍内容。

中间还有两处是判断上的错,不是量的问题:一是用 mandatory 吸附(把用户的滑动抢走了),二是把「矮视口」定在 900(把有空间的屏幕当成没空间)。前者的修法是换模式,后者的修法是挪断点。

星野的头像

星野 XINGYE

全栈工程师。这里记录 82 篇复盘:24 份故障档案、OTA、架构演进与工作流。