从等比缩放到 Mobile-First 的移动端重构

【这是一篇写在此项目开发期的文章】 我的博客移动端一直只是把桌面版按断点往下塌缩,字号还整体偏大。这篇记录我用容器查询替掉固定断点、把触摸目标补到 44px、用根字号加 rem 统一收紧全站字号的过程,以及导航抽屉被 Header 裁掉、改根字号却不见效这些遇到的问题。

从等比缩放到 Mobile-First 的移动端重构

从等比缩放到 Mobile-First 的移动端重构

主要问题:移动端只是把桌面版缩小了

我之前的移动端适配,主要是给桌面布局加几个断点往下塌。首页文章网格是桌面三列,到 900px 掉成两列,640px 再掉成一列;而工具页是桌面六列,卡片靠一个 toolSpan() 函数算 Bento 跨列。

这套做法的问题是,断点之间完全没有过渡。比如窗口从 1200px 缩到 901px,列数一直是三列,卡片却越挤越窄。而到了小屏,虽然不横向溢出了,但本质上只是把桌面版缩小了一遍,很多地方是"勉强能看"。

还遇到了一个更奇怪的问题:全站字号整体偏大

我一开始以为这是真机的问题,专门按 375~430px 的逻辑宽度去收紧了一批标题和卡片字号。改完之后在模拟器上看,还是偏大。直到有次我把浏览器缩到 75%,发现整个页面反而舒服多了,才意识到是设计基准本身偏大的问题。

解决方案

网格改用容器查询

先解决布局。固定断点换成容器查询,让列数跟着可用宽度平滑变化:

.article-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 300px), 1fr));
  gap: clamp($spacing-4, 2vw, $spacing-6);
}

min(100%, 300px) 这层是为了小屏兜底,保证容器再窄卡片也不会溢出。间距用 clamp() 跟着视口流体变化,不再需要为间距单独写断点。

同一个写法套到了几个地方,只是最小宽度不同:

位置 最小宽度
工具页卡片网格 230px
首页文章网格 300px
文章相关推荐 300px
归档统计卡 180px
关于页统计卡 160px

现在删掉了一堆东西:各页面配套的固定断点塌缩块,以及工具页的 tool-span-2/3/6 和那个 toolSpan() 函数。容器查询自己会填充末行,不用再手算跨列。

触摸目标补到 44px

收藏按钮只有 30px,手指点很容易点偏。我不想把按钮视觉上放大,就用 ::before 把命中区撑开:

.fav-btn {
  position: relative;
  // 30px 的视觉尺寸
}

/* 移动端触摸热区:用 ::before 把 30px 视觉目标的命中区扩展到 44px */
.fav-btn::before {
  content: "";
  position: absolute;
  inset: -7px;
}

30 + 7 × 2 = 44px。工具页的分类胶囊在移动端也补了 min-height: 44px

字号改成流体

主标题从固定 px 改成 clamp(),让它在视口宽度变化时平滑缩放:

.tools-hero h1 {
  font-size: clamp(1.5rem, 4.5vw, 2.375rem);
  line-height: 1.2;
}

文章卡片摘要则是随断点减行,网格卡在移动端从两行收到一行,hero 卡从三行收到两行。

文章页加底部操作栏

移动端读长文时,目录在侧边栏,返回顶部按钮浮在右下角,都不太顺手。所以给文章详情加了一条固定在底部的操作栏,三个按钮:目录、评论、顶部。

目录按钮打开的是一个底部抽屉,直接复用了侧边栏的 tocItems 数据,没有另做一套:

.mobile-bottom-bar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 120;
  display: none;
  grid-template-columns: repeat(3, 1fr);
  padding: 6px 8px calc(6px + env(safe-area-inset-bottom));
}

@media (max-width: 768px) {
  .mobile-bottom-bar {
    display: grid;
  }
}

env(safe-area-inset-bottom) 是给带 Home 指示条的机型留出空间。

配套改了两处:移动端把原来的悬浮返回圆钮隐藏(否则和底栏重叠),以及给 .article-detailpadding-bottom: 76px,避免底栏盖住评论区。

全局字号基准:根字号加 rem 主导

这部分是问题的主要成因。

我的设计令牌本来就是 rem 的,$font-size-base1rem。但实际写页面的时候,我大量把字号写成了 px,或者写成 clamp(px, vw, px),这些都不跟根字号走。所以改根字号,标题纹丝不动。

意识到这点之后,我决定分两步来解决。

第一步,把根字号缩放,一次带动所有 rem 令牌(字号、间距、圆角):

html {
  font-size: clamp(14px, 13.2px + 0.9vw, 15px);
}

@media (max-width: 480px) {
  html {
    font-size: clamp(13px, 13px + 0.25vw, 14px);
  }
}

第二步,把各页面主标题从 px 改成 rem 主导,否则它们不参与缩放:

/* 原来是 32px / 28px */
.archive-page h1 {
  font-size: clamp(1.5rem, 3.5vw, 2rem);
}

文章卡片的标题和摘要也做了同样的换算,按 16px = 1rem 折算成 rem 主导的 clamp()

另外把文章正文的默认字号从 17px 收到 16px,阅读设置组件里的默认值也同步改了。

其他修复的问题:导航抽屉被裁掉

这个问题是当天早些时候发现的,比字号问题严重得多。

移动端汉堡按钮点开抽屉,只有顶部一小条,内容全没了。量了一下高度只有 68px,正好等于 Header 的高度。原因是 .header 上用了 backdrop-filter,它会创建一个 containing block,导致内部 el-drawerposition: fixed 被限制在 Header 容器里。

我的修法是移除 .headerbackdrop-filter,背景改用渐变补偿,并新增了一个 --bg-header-solid 变量。当时也试过给抽屉加 append-to-body,但那样 body 干脆不渲染了,只留下 header,所以还是回到消除 containing block 这条路。

防横向滚动

移动端长单词、毛玻璃溢出都可能撑出横向滚动条,所以给 htmlbody 都加了 overflow-x: hidden 兜底。

不过这只是兜底,具体溢出源还是得单独治。比如正文代码块用的是 overflow-x: auto,表情面板用 overflow-x: hidden

返回列表页保留滚动位置

从文章详情返回列表页时,总是跳回顶部,长列表里翻找很烦。新增了 app/router.options.ts

export default <RouterOptions>{
  scrollBehavior: (_to, _from, savedPosition) => {
    return savedPosition || { top: 0, left: 0 };
  },
};

这里有一点要注意:Nuxt 3.17 不支持在 nuxt.config.ts 里写 app.scrollBehavior,会报 TS2353,所以只能放到这个文件里导出。

这次踩到的几个坑

改完根字号,标题一点没变小

当我把 html { font-size } 改小了时,刷新一看,发现正文确实小了,但各级标题几乎没变。

原因就是前面说的:标题写的是 32px 这种绝对值,根本不读根字号。根字号只影响 rem,而标题是 px。所以必须先把标题改成 rem 主导,根字号才起作用。

clamp(px, vw, px) 的 vw 会把下限顶上去

标题改 clamp() 之后,我以为移动端会跟着根字号缩,结果还是偏大。

因为 clamp(1.4rem, 2.6vw, 1.8rem) 里的 2.6vw 是按视口算的,不受根字号影响。移动端 390px 下 2.6vw 大概 10px,小于下限 1.4rem,于是整个表达式被卡在下限上,等于没缩。

要让移动端真的缩下去,得让 vw 那一项足够小,或者干脆另设一条移动端规则。

内联 style 优先于媒体查询

正文默认字号我一开始想用媒体查询覆盖,写完之后没生效。

因为 --article-font-size 是通过内联 style 绑到正文容器上的,内联样式优先级最高。想收紧正文,只能改默认值本身(也就是 readingPrefs 和阅读设置组件里的 DEFAULT 两处,得同步改)。

移动端规则要写在主规则之后

CSS 里同优先级时后写的生效。我最初把 @media 块写在了主规则前面,结果卡片内边距、标题字号这些都没生效,只有那些主规则里没写过的属性才起作用。

后来把这些移动端规则统一挪到文件末尾就好了。

底部操作栏和悬浮返回按钮重叠

底栏一加上,右下角的悬浮返回圆钮就和它叠在一起。最后是移动端直接隐藏返回圆钮,让底栏的"顶部"按钮接管,同时给正文区留出底部空间,避免挡住评论区。

还没确定的地方

还有一批小字号是 px。 组件里 12~14px 的那些字号,我这次没动。它们本身已经够小,对"偏大"的观感影响不大,但严格说仍然不跟根字号走。还没决定要不要全量换成 rem。

底部操作栏只做了文章页。 列表页、归档页其实也都有"回顶部"的需求,但目前只有文章详情有底栏。是每个页面都加,还是做一个全局的,我还没确定下来。

安全区只处理了底部。 env(safe-area-inset-bottom) 给了底栏,但顶部和左右两侧(横屏时)没处理。

浙ICP备2026077668号

欢迎来到我的网站 这个网站主要是作技术和生活两个维度的记录