响应式设计与移动端适配
1. 为什么响应式不是"改几个 @media 那么简单"
做后端时你处理的是 JSON 和 HTTP 状态码,前端要处理的是:
- 屏幕尺寸:320px(老 iPhone SE)到 2560px(外接 4K 显示器)
- 像素密度:1x、2x、3x Retina
- 输入方式:鼠标、触屏、键盘、手写笔
- 浏览器:Chrome、Safari、微信 WebView(各有 bug)
- 横竖屏切换:手机转一下屏幕整个布局要变
- 安全区:iPhone 刘海、底部手势条不能盖内容
响应式 ≠ 写一堆 @media,它是一套系统化的"尺寸思维":
- 布局层面用 flex/grid 自适应
- 字号/间距用相对单位(rem)统一缩放
- 关键断点用
@media改布局形态(侧边栏变抽屉之类) - 移动端加点"触屏专属"补丁(热区、安全区、禁止缩放等)
这篇把四层串起来。
Java 对照:类似后端的"多租户" + "多环境配置"——同一份代码在不同上下文跑出不同行为,靠的是 Profile 切换和依赖注入,而不是给每个租户各拷一份代码。
2. 视口 meta:移动端一切的起点
打开 index.html,找到这行:
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
少这行,所有响应式设计都白搭——因为手机浏览器会把页面当成 980px 宽的桌面页渲染,然后整体缩放显示,你的 @media (max-width: 768px) 永远不生效。
逐字拆解:
width=device-width— 页面宽度 = 设备物理宽度(以 CSS 像素计)initial-scale=1.0— 初始缩放 1:1,不放大不缩小
常见追加配置(按需):
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
viewport-fit=cover — iPhone 刘海屏全屏铺满,配合后面讲的"安全区"使用。
不要加 user-scalable=no:禁用缩放严重损害可访问性,用户双指放大是合法需求(尤其老人看小字)。
3. CSS 单位:什么时候用什么
前端单位多得让人头大,实际用好 5 个就够:
| 单位 | 含义 | 用在哪 |
|---|---|---|
px |
绝对像素 | 边框、细节定位(不随缩放变化的地方) |
rem |
相对根元素字号 | 字号、间距、大部分尺寸 |
em |
相对当前元素字号 | 需要和当前字号绑定的(比如按钮的 padding) |
% |
相对父元素 | 布局宽度、进度条 |
vw / vh |
视口宽度/高度的 1% | 全屏元素、大标题 |
3.1 为什么优先 rem
默认 html 的字号是 16px,所以 1rem = 16px。它的好处:
用户在浏览器里调了默认字号(比如设成 20px),你的整个 UI 自动按比例放大——盲人、弱视用户的可访问性就是这么实现的。
html { font-size: 16px; } /* 不写也是默认值 */
.btn {
padding: 0.5rem 1rem; /* 8px 16px */
font-size: 0.875rem; /* 14px */
border-radius: 0.375rem; /* 6px */
}
Tailwind 已经这么干了:p-4 等于 padding: 1rem,整站跟着根字号缩放。不要去覆盖 html 的 font-size,会打破 Tailwind 的 scale。
3.2 什么时候用 em
em 相对当前元素,常用于"让内部元素跟外部字号缩放":
.btn {
font-size: 1rem;
padding: 0.5em 1em; /* padding 跟字号走 */
}
.btn-lg { font-size: 1.5rem; } /* 大按钮字号大,padding 自动也变大 */
换成 rem 就得每个尺寸写一套 padding,em 省了重复。
3.3 vw/vh 的坑
100vh 在手机浏览器里不等于屏幕可见高度——它是"没有地址栏时的全屏高度",地址栏在时内容会被挡。
解法:用现代浏览器支持的 svh(small viewport height,地址栏出现时的高度)和 dvh(dynamic viewport height,动态计算)。
/* 全屏登录页 */
.login-container {
min-height: 100vh; /* 老浏览器回退 */
min-height: 100dvh; /* 现代浏览器生效 */
}
4. 断点策略:移动优先 vs 桌面优先
4.1 移动优先(推荐)
默认写手机样式,往上加断点覆盖桌面:
/* 默认(手机) */
.card { padding: 1rem; }
/* 平板及以上 */
@media (min-width: 768px) {
.card { padding: 1.5rem; }
}
/* 桌面及以上 */
@media (min-width: 1024px) {
.card { padding: 2rem; }
}
Tailwind 内置这一套:
<div class="p-4 md:p-6 lg:p-8">...</div>
4.2 为什么移动优先
- 手机屏幕小,样式相对简单,默认状态承担最少的复杂度
- 手机性能弱,大多数 CSS 媒体查询不匹配时浏览器不计算规则,减少开销
- 符合"渐进增强"的思路:小屏能用,大屏更好用
4.3 桌面优先什么时候用
改造老项目时(原来只有桌面版,现在要加手机适配),反向用 max-width 更划算:
.sidebar { width: 240px; }
@media (max-width: 767px) {
.sidebar { display: none; } /* 小屏隐藏 */
}
新项目从头写,一律移动优先。
5. 实战:一个响应式的笔记列表
综合前面讲的 Flex/Grid/Tailwind,in4vue 的笔记列表:
<template>
<section class="mx-auto max-w-6xl px-4 py-8 md:px-6 md:py-12">
<!-- 标题区:手机单行,桌面两栏 -->
<header class="mb-6 flex flex-col gap-2 md:mb-10 md:flex-row md:items-end md:justify-between">
<div>
<h1 class="text-2xl font-bold md:text-4xl">学习笔记</h1>
<p class="mt-1 text-sm text-gray-500 md:text-base">
从 Java 后端视角理解前端
</p>
</div>
<el-input v-model="keyword" placeholder="搜索..." class="md:w-64" />
</header>
<!-- 列表:手机 1 列,平板 2 列,桌面 3 列 -->
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
<NoteCard v-for="n in notes" :key="n.id" :note="n" />
</div>
</section>
</template>
关键决策:
- 容器用
max-w-6xl mx-auto,超大屏不会铺满到边界 - 间距
px-4 py-8手机窄屏不挤,md:px-6 md:py-12桌面宽松 - 标题区
flex-col md:flex-row— 手机竖排,桌面横排 - 列表网格断点 1 / 2 / 3 列渐进
从 320px 拖到 2560px,始终合理。
6. 触摸交互的 3 个注意点
6.1 点击热区 ≥ 44×44px
苹果 HIG 和 Material Design 都建议:可点击元素的最小触摸区域 44×44 CSS 像素。
<!-- ❌ 图标太小,手指戳不准 -->
<button class="p-0"><el-icon><Delete /></el-icon></button>
<!-- ✅ 加内边距把热区撑大 -->
<button class="p-2"><el-icon><Delete /></el-icon></button>
<!-- 16px 图标 + 2×8px padding = 32px,再加 border 勉强够,建议 p-3 更稳 -->
Element Plus 的 <el-button> 默认尺寸已经够,自己用 icon 时要注意。
6.2 不要依赖 :hover
手机没有鼠标悬停。把 hover 态仅当锦上添花,核心信息别靠 hover 才出现。
<!-- ❌ 移动端看不到信息 -->
<div>
<span class="opacity-0 hover:opacity-100">删除</span>
</div>
<!-- ✅ 移动端直接可见,桌面悬停时再高亮 -->
<div>
<button class="text-gray-400 hover:text-red-500">删除</button>
</div>
6.3 消除 300ms 点击延迟
早期手机浏览器点击时会等 300ms 判断是不是双击。现代浏览器在**有 viewport meta(见第 2 节)**时已经自动消除,不用再加 fastclick 那种老库。
7. iPhone 刘海和底部手势条:安全区适配
iPhone X 之后引入刘海和手势条,不做适配的页面要么被遮挡要么留一条白边。
7.1 启用 viewport-fit=cover
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
开启后页面铺满整个屏幕,包括刘海区域——关键内容要自己避开。
7.2 用 env() 读取安全区
.header {
/* 固定顶部:原 12px 内边距 + 状态栏高度 */
padding-top: calc(12px + env(safe-area-inset-top));
}
.bottom-nav {
/* 固定底部:原 12px + 手势条高度 */
padding-bottom: calc(12px + env(safe-area-inset-bottom));
}
env(safe-area-inset-*) 在非刘海屏上返回 0,所以安全。需要 viewport-fit=cover 才生效。
Tailwind 里也支持:
<header class="pt-3 pt-[calc(0.75rem+env(safe-area-inset-top))]">...</header>
8. 图片适配:不同屏幕用不同资源
8.1 Retina 屏适配
<!-- 普通屏用 logo.png(100×100),Retina 用 logo@2x.png(200×200) -->
<img
src="logo.png"
srcset="logo.png 1x, logo@2x.png 2x, logo@3x.png 3x"
alt="logo"
/>
浏览器根据设备像素比自动选图。
8.2 不同尺寸屏用不同图
<picture>
<source media="(max-width: 639px)" srcset="hero-mobile.webp" />
<source media="(max-width: 1023px)" srcset="hero-tablet.webp" />
<img src="hero-desktop.webp" alt="hero" class="w-full" />
</picture>
手机下载 mobile 版(小而窄),桌面下载 desktop 版(大而宽),不要让手机下载 4K 图再缩小显示,浪费流量还卡。
8.3 懒加载
<img src="photo.webp" loading="lazy" alt="照片" />
loading="lazy" 是浏览器原生懒加载,滚到可视区才加载。非首屏图片都加上。
9. 排版:字号的响应式
正文字号在手机 16px、桌面 18px 通常最合适。别在手机用 14px——看着累。
9.1 简单写法
<h1 class="text-2xl md:text-4xl lg:text-5xl">响应式标题</h1>
<p class="text-base md:text-lg">响应式正文</p>
9.2 流式字号(进阶)
用 clamp() 让字号在屏幕尺寸间平滑插值:
h1 {
font-size: clamp(1.5rem, 2vw + 1rem, 3rem);
/* 最小 1.5rem,随 2vw + 1rem 增大,最大 3rem */
}
优点:不用写多个断点,所有尺寸都流畅过渡。 缺点:调起来不直观,设计师给的是具体值时用断点反而快。
10. 测试:别只靠 DevTools 的模拟器
Chrome DevTools 的设备工具栏(Cmd/Ctrl + Shift + M)能切换常见设备,90% 的问题能查出来,但它不能完全模拟:
- 真机的触摸反馈(页面回弹、键盘弹出)
- iOS Safari 的特殊行为(
100vh刘海、输入框聚焦顶起页面) - 微信 WebView 里的 UA 差异
in4vue 的测试流程:
- 开发时用 DevTools 拉宽高拖,覆盖 320 / 375 / 768 / 1024 / 1440 五档
- 关键功能用真机测一遍
- 用
pnpm dev --host让手机连局域网访问:
pnpm dev --host
# 输出:Network: http://192.168.1.x:5173/
手机连同 Wi-Fi 扫码或输这个地址,即可在真机调试。
11. 工具链速查
| 工具 | 作用 |
|---|---|
| Chrome DevTools Device Toolbar | 快速切换屏幕尺寸 |
| Safari 开发 → 响应式设计模式 | 测 Safari/iOS 特有行为 |
| Responsively App | 同屏预览多设备 |
pnpm dev --host + 手机 |
真机调试 |
| BrowserStack | 真机云测试(付费,有试用) |
12. 常见坑点速查
| 现象 | 原因 | 解法 |
|---|---|---|
| 手机上文字挤成一坨 | 没加 viewport meta | 检查 index.html 第 2 节 |
100vh 在 iOS 留白 |
Safari 地址栏高度问题 | 用 100dvh 或 min-height: -webkit-fill-available 兜底 |
| iPhone 输入框聚焦页面跳动 | 默认字号 < 16px 时 Safari 强制放大 | 给 input 加 font-size: 16px |
| 底部按钮被手势条盖住 | 没处理安全区 | 第 7 节的 env(safe-area-inset-bottom) |
@media 不生效 |
没加 viewport meta 或单位写 px 大小写错了 | 检查两处 |
| 横屏模式布局怪 | 没考虑横向 | 加 @media (orientation: landscape) 或用 flex wrap |
小练习
- 把 in4vue 的首页笔记列表改成 1/2/3 列响应式网格
- 给
index.html的 viewport meta 加上viewport-fit=cover,检查手机顶部是否铺满 - 用
pnpm dev --host让手机访问 in4vue,观察真实效果 - 给一个
position: fixed底部栏加env(safe-area-inset-bottom),在 iPhone 模拟器验证 - 把一个
100vh的登录背景图换成100dvh,在模拟手机地址栏动画里观察差异