一文搞懂移动端适配方案

Daotin 于 2022-09-10 发布 编辑

1. 原始阶段

让我们聊聊移动端适配的”原始阶段”。

还记得智能手机刚开始普及的时候吗?那时候的网页都是为电脑设计的,放在手机上查看时要么太大要么太小,经常需要手动缩放才能看清内容。这就是为什么我们需要专门的移动端适配方案。

最开始的时候,开发者们的做法特别简单粗暴 —— 直接给页面设置一个固定的宽度,比如 320px 或者 640px。代码大概是这个样子:

.page {
	width: 320px;
	margin: 0 auto; /* 让页面居中显示 */
}

.header {
	height: 44px;
	background: #f5f5f5;
}

.content {
	font-size: 14px;
	line-height: 1.5;
}

你可能会问:”这样做不行吗?”确实,这种方案简单直接,写起来也很轻松。但是实际用起来问题可就大了:

想象一下,你拿着一个屏幕宽度只有 300px 的小手机访问这个页面,会发现右边被截了一段,还得左右滑动才能看完整个页面,体验相当糟糕。换成一个 400px 宽的大屏手机,页面两边又会留下大片空白,看起来就像是在浪费屏幕空间。

为了解决这个问题,开发者们开始尝试使用百分比来布局。这样页面就能根据屏幕宽度自动伸缩了:

.page {
	width: 100%; /* 宽度占满整个屏幕 */
	max-width: 640px; /* 但不能太宽,避免在平板上变形 */
}

.content {
	padding: 0 5%; /* 两边留5%的边距 */
}

.left-section {
	width: 30%; /* 左边占30% */
	float: left;
}

.right-section {
	width: 70%; /* 右边占70% */
	float: left;
}

这样看起来是不是好多了?页面可以自适应不同的屏幕宽度。但是用百分比也有让人头疼的地方:

  1. 假设你想让一个元素的高度永远是宽度的一半,你可能会写height: 50%。但实际上这样并不会生效,因为父元素如果没有固定高度,子元素的百分比高度就会失效。

  2. 内边距(padding)和外边距(margin)的百分比值都是相对于父元素的宽度来计算的,这一点特别容易让人困惑。比如:

.box {
	width: 300px;
	padding: 10%; /* 这里的padding值是300px的10%,也就是30px */
}
  1. 更要命的是,一些属性用百分比特别别扭,比如想让字体大小随着屏幕变化,用百分比就不太合适。

于是有人想到了用em这个单位。em是相对于父元素的字体大小来计算的,听起来挺好用的:

.parent {
	font-size: 16px;
}

.child {
	font-size: 1.2em; /* 会是 16px * 1.2 = 19.2px */
	padding: 1em; /* 会是 19.2px */
	margin-bottom: 0.5em; /* 会是 9.6px */
}

但是等你实际用起来就会发现,这简直是个噩梦!因为em会层层叠加:

.level-1 {
	font-size: 1.2em; /* 16px * 1.2 = 19.2px */
}
.level-2 {
	font-size: 1.2em; /* 19.2px * 1.2 = 23.04px */
}
.level-3 {
	font-size: 1.2em; /* 23.04px * 1.2 = 27.648px */
}

看到这个计算过程,你就知道为什么后来的开发者都不愿意用em了 —— 这谁能算得清啊!而且一旦某个地方的字体大小改变了,所有用em的地方都要重新计算,维护起来特别痛苦。

正是因为这些问题,我们才需要更好的解决方案。接下来,就到了响应式布局的时代,媒体查询和 rem 的组合开始大放异彩。

2. 响应式过渡阶段

前面我们聊到,用百分比和 em 单位来做移动端适配都不够理想。这时候,CSS3 带来了一个强大的新功能 —— 媒体查询(Media Queries)。有了它,我们终于可以针对不同的设备写不同的样式了!

来看个简单的例子。假设我们在做一个新闻网站,需要在手机和平板上都能好好展示。用媒体查询,我们可以这样写:

/* 默认样式 - 适用于手机 */
.news-container {
	width: 100%;
	padding: 0 15px;
}

.news-item {
	font-size: 14px;
	margin-bottom: 10px;
}

/* 平板和小屏电脑 */
@media screen and (min-width: 768px) {
	.news-container {
		width: 750px;
		margin: 0 auto;
		padding: 0 20px;
	}

	.news-item {
		font-size: 16px;
		margin-bottom: 15px;
	}
}

/* 大屏电脑 */
@media screen and (min-width: 1200px) {
	.news-container {
		width: 1170px;
	}
}

这段代码是不是很有意思?它的意思是:

这样,同一个网站在不同设备上都能获得最佳的阅读体验。但是很快,开发者们又发现了新问题:不同的手机屏幕有不同的设备像素比(Device Pixel Ratio,简称 DPR)。

比如 iPhone 6/7/8 的 DPR 是 2,这意味着我们写的1px在屏幕上实际显示是2个物理像素。到了 iPhone X 这样的设备,DPR 更是达到了 3。这就导致同样的14px字体,在不同手机上看起来大小不一。

而且,你看上面那段代码,我们需要给每种屏幕尺寸都写一套样式,如果设备种类再多一点,代码量就会暴增。维护起来也特别麻烦,改个字体大小可能要改好几处。

这时候,rem 这个 CSS 单位就派上用场了。rem 跟 em 类似,但是它永远都是相对于根元素(html 标签)的字体大小来计算的。比如:

html {
	font-size: 16px;
}

.box {
	width: 10rem; /* 160px */
	height: 5rem; /* 80px */
	font-size: 1rem; /* 16px */
}

聪明的开发者们想到了:如果我们能根据屏幕宽度动态设置 html 的 font-size,那不就可以实现所有使用 rem 单位的元素都跟着屏幕大小自动缩放了吗?

/* 在不同屏幕下设置不同的根字体大小 */
@media screen and (max-width: 320px) {
	html {
		font-size: 14px;
	}
}
@media screen and (min-width: 321px) and (max-width: 375px) {
	html {
		font-size: 16px;
	}
}
@media screen and (min-width: 376px) {
	html {
		font-size: 18px;
	}
}

/* 所有尺寸都用rem */
.button {
	width: 5rem;
	height: 2rem;
	font-size: 0.875rem;
	border-radius: 0.25rem;
}

这样一来,同样的按钮在不同屏幕上就会自动调整大小,既保持了整体的协调性,又能适应不同的屏幕。而且最重要的是,我们只需要写一次样式,剩下的就交给 rem 自动处理了!

不过,手动设置这些媒体查询断点还是有点麻烦。而且,我们永远不能穷举所有的媒体查询宽度。

那么,能不能让根字体大小完全根据屏幕宽度自动计算?…这个想法直接推动了下一个阶段的到来:淘宝团队的 flexible 方案!

3. 工程化解决方案

前面我们说到,手动设置媒体查询的方式还是不够优雅。毕竟,谁也不想写一堆的媒体查询来适配不同的屏幕尺寸。而且,设计师给我们的设计稿通常是固定宽度的(比如 750px),我们还得手动计算每个尺寸对应的 rem 值,这也太麻烦了。

这时候,淘宝的前端团队想出了一个聪明的主意:为什么不让 JavaScript 来帮我们自动计算根字体大小呢?这就是著名的 flexible 方案:

// 核心思路:将屏幕宽度平均分成10份,1rem就等于其中的1份
function setRem() {
	// 假设设计稿是750px
	const htmlWidth = document.documentElement.clientWidth || document.body.clientWidth
	const htmlDom = document.getElementsByTagName('html')[0]
	htmlDom.style.fontSize = htmlWidth / 10 + 'px'
}

// 初始化
setRem()
// 窗口变化时重新设置
window.addEventListener('resize', setRem)

这段代码的巧妙之处在于:它把屏幕宽度平均分成 10 份,1rem 就等于其中的 1 份。这样,在 750px 的设计稿上,1rem 就等于 75px。比如设计稿上一个按钮是 150px 宽,我们只要写 2rem 就可以了,简单直观!

不仅如此,flexible 方案还考虑到了设备像素比(DPR)的问题,它会自动设置 viewport 的缩放比例:

const scale = 1 / devicePixelRatio
document
	.querySelector('meta[name="viewport"]')
	.setAttribute(
		'content',
		'width=device-width,initial-scale=' +
			scale +
			',maximum-scale=' +
			scale +
			',minimum-scale=' +
			scale +
			',user-scalable=no',
	)

这样就能确保在不同 DPR 的设备上显示效果一致。后来,淘宝团队还推出了升级版的 lib-flexible,增加了更多功能和优化。

但是,我们还是要手动计算 px 到 rem 的转换,虽然计算方式简单了(用设计稿的 px 值除以 75),但还是挺烦的。于是postcss-pxtorem这个神器就登场了!

它是一个 PostCSS 插件,可以自动将 px 转换为 rem。我们只需要简单配置一下:

// postcss.config.js
module.exports = {
	plugins: {
		'postcss-pxtorem': {
			rootValue: 75, // 设计稿宽度的1/10
			propList: ['*'], // 需要转换的属性,这里表示全部都转换
			selectorBlackList: ['.no-rem'], // 不转换的选择器
			minPixelValue: 2, // 小于2px的不转换
		},
	},
}

有了这个插件,我们就可以直接按照设计稿的 px 值写样式了:

.button {
	width: 150px; /* 会被自动转换成 2rem */
	height: 60px; /* 会被自动转换成 0.8rem */
	font-size: 28px; /* 会被自动转换成 0.373333rem */
	border-radius: 8px; /* 会被自动转换成 0.106667rem */
}

这样一来,我们的开发流程就变得超级顺畅:

  1. 拿到设计稿,直接按照上面的 px 值写样式
  2. 保存文件时,postcss-pxtorem 自动将 px 转换为 rem
  3. flexible.js 自动设置根字体大小
  4. 完美适配各种屏幕!

所以,flexable 解决的是媒体查询无法穷举的问题,通过 JS 动态计算使得适配更加精确和自动化;而 postcss-pxtorem 则解决了”开发时需要手动计算 rem 值”的问题,让开发者可以直接使用设计稿的 px 值,提升了开发效率。这两个工具的组合使用,正好解决了 rem 适配方案中的两大痛点。

不过,随着 viewport 单位(vw、vh)的兼容性越来越好,我们又有了更好的选择。

4. 现代化方案

前面我们聊到了 flexible 和 postcss-pxtorem 的组合,这个方案用了很长时间。

但是你有没有发现,它还是有点麻烦?首先需要引入 JavaScript 脚本,其次还要配置 postcss 插件。而且 rem 的计算规则(屏幕宽度/10)说实话也不够直观。

这时候,viewport 单位(vw/vh)站了出来说:”让我来试试!”

viewport 单位超级简单:

比如在 375px 宽的手机上,1vw 就等于 3.75px,这比 rem 的计算方式直观多了!

而且,我们有了新的 PostCSS 插件:postcss-px-to-viewport。它的配置比 postcss-pxtorem 还要简单:

// postcss.config.js
module.exports = {
	plugins: {
		'postcss-px-to-viewport': {
			viewportWidth: 750, // 设计稿宽度
			viewportHeight: 1334, // 设计稿高度
			unitPrecision: 3, // 保留的小数位数
			viewportUnit: 'vw', // 转换成的单位
			selectorBlackList: ['.ignore'], // 不需要转换的类名
			minPixelValue: 1, // 小于1px不转换
			mediaQuery: false, // 是否允许在媒体查询中转换px
		},
	},
}

有了这个插件,我们依然可以按照设计稿的 px 来写样式:

.button {
	width: 150px; /* 会被转换成 20vw */
	height: 60px; /* 会被转换成 8vw */
	font-size: 28px; /* 会被转换成 3.733vw */
	border-radius: 8px; /* 会被转换成 1.067vw */
}

看到没有?不需要 JavaScript,不需要计算,样式表里的 px 会自动转换成 vw。而且计算规则特别简单:

vw = px / (设计稿宽度 / 100)

不过,单纯使用 vw 也有一个小问题:当屏幕很大时(比如平板或者 PC),页面元素会变得特别大。

最简单的方案 - 限制容器最大宽度:

.container {
	width: 100vw;
	max-width: 750px; /* 或其他合适的最大宽度 */
	margin: 0 auto;
}

当然,我们也可以结合 rem 来解决这个问题,这就是 vw + rem 混合方案:

/* 设置根字体大小,但限制最大值 */
html {
	/* 基准字号使用vw */
	font-size: 10vw; /* 在750px设计稿下,1rem = 75px */

	/* 限制最大字号 */
	@media screen and (min-width: 750px) {
		font-size: 75px; /* 750px * 10% = 75px */
	}
}

/* 页面内容区域限制最大宽度 */
.container {
	width: 7.5rem; /* 等于设计稿宽度 */
	margin: 0 auto;
}

/* 其他元素使用rem */
.button {
	width: 2rem; /* 原本150px */
	height: 0.8rem; /* 原本60px */
	font-size: 0.373333rem; /* 原本28px */
}

总结与选型建议

让我们回顾一下移动端适配方案的发展历程:

  1. 原始阶段:固定宽度和百分比布局,实现简单但适配效果差
  2. 响应式过渡:媒体查询+rem,开始关注不同设备的适配,但代码量大
  3. 工程化方案:flexible+postcss-pxtorem,让开发更自动化,但依赖 JS
  4. 现代化方案:viewport 单位+容器查询,更简单直观,但需要考虑兼容性

各方案对比

方案 优点 缺点 适用场景
flexible + rem - 兼容性好
- 工程化成熟
- 用法简单
- 需要引入 JS
- 计算不直观
- 配置略繁琐
兼容性要求高的项目
vw + viewport - 原生支持
- 计算直观
- 无需 JS
- 大屏幕缩放问题
- 兼容性要求高
现代化项目
vw + rem 混合 - 兼顾灵活性
- 解决大屏问题
- 配置简单
- 代码稍复杂
- 需要同时理解两种单位
大中型项目

选型推荐

  1. 如果你的项目需要兼容老旧浏览器(如 iOS 8、Android 4.4):

    • 推荐使用 flexible + postcss-pxtorem 方案
    • 这是经过时间考验的解决方案,兼容性最好
  2. 如果你在开发一个全新的项目

    • 推荐使用 vw + postcss-px-to-viewport 方案
    • 更简单、更直观,且不需要引入额外的 JavaScript
  3. 如果你的项目需要同时适配移动端和 PC 端

    • 推荐使用 vw + rem 混合方案
    • 可以更好地控制在大屏幕下的展示效果

最后的建议:对于新项目,如果不需要考虑兼容性,直接上 vw + postcss-px-to-viewport 是最省心的选择。它既简单又好用,还是未来的趋势。