前言
PC Web的移动端适配相信是很多前端开发不得不品的一环(🥲,特别是门户网站或者官网,很多人会采用PC端一套样式,移动端一套样式的方式去适配。
但众所周知,我们的浏览器窗口是自由的,屏幕规格也是自由的(带鱼屏、折叠屏、奇怪宽高比的数据大屏),因此两套样式的适配形式往往覆盖不了全部的展示场景,同时还会有共有样式重复造成的冗余、组建抽离难度大等问题。
常见适配方案
先不管移动端还是PC端,看看常见的适配方案有哪些。
px转rem
H5常用适配方案,利用rem单位的特性(根据根节点 font-size 来计算实际尺寸,如font-size为16时,1rem=16px),实时计算设计稿的宽度和实际窗口宽度的比例,动态改变根节点的font-size的值。
// 设计稿宽度
const DESIGN_WIDTH = 375
export default function setRem(designWidth = DESIGN_WIDTH) {
const docEl = document.documentElement
const resizeEvt = 'orientationchange' in window ? 'orientationchange' : 'resize'
const reCalc = function () {
const clientWidth = docEl.clientWidth
if (!clientWidth) return
// 16为根节点font-size默认值,需要与后文插件的rootValue保持一致
docEl.style.fontSize = 16 * (clientWidth / designWidth) + 'px'
}
reCalc()
if (document.addEventListener) {
window.addEventListener(resizeEvt, reCalc, false)
document.addEventListener('DOMContentLoaded', reCalc, false)
}
}
为了方便开发,一般会使用postcss插件将px单位转为rem。这样我们就能按照设计稿的尺寸直接使用px值,不用担心页面在不同规格屏幕的展示效果问题。
// vite.config.ts
postcss: {
plugins: [
// ...
postcssPxToRem({
rootValue: 16,
unitPrecision: 5,
propList: ['*'],
selectorBlackList: [],
replace: true,
mediaQuery: false,
minPixelValue: 0,
exclude: /node_modules/
})
],
}
px转vw
H5常用适配方案。与px转rem类似,但插件会同时进行缩放比例计算和单位换算,我们只需要传入配置化的参数,更加方便。
Scale
一般用于大屏,简单粗暴的使用 transform 属性等比缩放网页。屏幕比例和设计稿不一致时周边会有留白,且文字或图片有可能模糊。
移动端PC端两套样式
开头说的双端适配常用方案,能满足基本的适配要求,但由于双端样式不能共用,甚至HTML结构也可能双端独立,适配的成本高,且有大量的冗余代码。
媒体查询
CSS3功能,允许开发者根据设备特性(宽高、横竖屏等)来应用不同的样式规则,也是本文主要使用的适配方案。
原理
Breakpoint 宽屏向窄屏适配
很多团队PC Web项目都是优先出PC端设计稿,在功能开发完成后再做移动端适配,因此我们首先应该明确适配的整体思路——以PC端样式为基础,逐渐由宽屏向窄屏适配,最后覆盖到移动端。
| 屏幕规格 | Breakpoint | 尺寸 |
|---|---|---|
| 超小屏 | xs | size < 640 |
| 小屏 | sm | 640 <= size <768 |
| 中等屏 | md | 768 <= size <1024 |
| 大屏 | lg | 1024 <= size <1280 |
| 超大屏 | xl | 1280 <= size <1536 |
| 超超大屏 | 2xl | 1536 <= size |
这里有一个适配方向上的问题,即从宽屏向窄屏适配或是从窄屏向宽屏适配。如果采用从宽屏向窄屏适配的方案,就表示我们的默认样式对应的屏幕规格就是 2xl ,因此在适配时就只需要考虑其他小于 2xl 的断点。反之默认样式对应 xs 规格的情况下,只需要考虑大于 xs 的断点。
Tailwind CSS
Tailwind 是一个流行的CSS库,通过组合各种预设class来构建UI。
媒体查询支持
<div
class="grid max-w-6xl grid-cols-2 gap-x-12 bg-white px-16 py-12 text-black max-md:grid-cols-1 max-md:px-12 max-md:py-6 max-sm:px-8"
>
<img
class="col-start-2 row-span-3 row-start-1 w-full h-full rounded-xl object-cover max-md:col-start-1 max-md:row-span-1 max-md:aspect-[16/9] max-sm:row-start-1"
src="https://public.ysjf.com/product/preview/Y9s2dgnGrU.jpg"
alt="Pic"
/>
<div
class="relative col-start-1 row-start-1 flex flex-col-reverse items-start gap-2 max-md:row-start-2 max-md:mt-8 max-sm:row-start-1 max-sm:text-white max-sm:p-5"
>
<div class="text-4xl font-bold max-sm:text-lg">这是一个标题</div>
<div class="text-2xl text-gray-600 max-sm:text-lg max-sm:text-white">副标题</div>
</div>
<div class="my-8 flex flex-col gap-4 max-md:flex-row max-md:justify-between max-sm:flex-col max-sm:my-5 max-sm:text-sm">
<div class="flex items-center font-semibold text-[#D4237A]">
<img class="mr-2 w-6 max-sm:w-4" src="./assets/collection.png" alt="like"/>
129
</div>
<button class="rounded-lg !bg-[#D4237A] px-12 py-3 text-white max-md:px-9 max-md:py-2 max-sm:text-xs">查看详情</button>
</div>
<p class="text-sm text-gray-500 max-sm:text-xs">
一大段文字一大段文字一大段文字一大段文字一大段文字一大段文字一大段文字一大段文字一大段文字一大段文字一大段文字一大段文字一大段文字一大段文字一大段文字一大段文字一大段文字
一大段文字一大段文字一大段文字一大段文字一大段文字一大段文字一大段文字一大段文字一大段文字一大段文字一大段文字一大段文字一大段文字一大段文字一大段文字一大段文字一大段文字
</p>
</div>
由于Tailwind的默认规则是 min-width 查询(移动优先),在我们先有PC端样式的情况下直接使用比较反直觉,需要不停地对已经适配的样式修正生效断点范围,因此我在上面示例中使用了 max-* 断点,即使用 max-width 查询。
max-* 是v4.1版本新特性,如果是旧版本,我们可以在Tailwind的配置文件中覆盖默认断点规则为 max-width 查询。
// tailwind.config.js
module.exports = {
// ...
theme: {
// 断点配置的顺序会决定样式生效的优先级,越往后优先级越高。
screens: {
xl: { max: '1535px' },
lg: { max: '1279px' },
md: { max: '1023px' },
sm: { max: '767px' },
xs: { max: '639px' },
},
},
// ...
};
新版本不支持覆盖默认断点的查询行为,仅支持更改断点对应的宽度值。你也可以直接使用自己的断点。
@import "tailwindcss";
@theme {
--breakpoint-xs: 30rem;
--breakpoint-2xl: 100rem;
--breakpoint-3xl: 120rem;
}
@theme {
// 移除默认断点
--breakpoint-*: initial;
--breakpoint-tablet: 40rem;
--breakpoint-laptop: 64rem;
--breakpoint-desktop: 80rem;
}
优势
- 减少代码量
在语义化CSS的样式复用中,从CSS的角度抽出mixin,很难发现什么样式是可复用的。就算抽离出来mixin,可读性也很差,不像组件可以从prop中或是像方法可以从形参了解到哪些参数需要交由外部处理。而CSS原子类本身可以看做一种粒度较细的封装。
// Tailwind
class="flex item-center justify-center"
// CSS
class="center-box"
.center-box {
display: flex;
justify-content: center;
align-items: center;
}
- 优化项目体积
Tailwind基本不会造成css文件膨胀。他提供的一系列可重用的、公共的实用类,供开发者任意组合,可以覆盖90%以上的布局需求,这也是其“原子”这一特性的体现。同时Tailwind会在编译时删除未使用的样式,进一步优化体积(最终输出CSS文件大小大部分都小于10KB)。
// Tailwind
class="flex item-center justify-center"
// 编译后CSS
.flex {
display: flex;
}
.item-center {
justify-content: center;
}
.justify-center {
justify-content: center;
}
// Tailwind
class="rotate-90 translate-x-1/2"
// 编译后CSS
.rotate-90 {
--tw-rotate: 90deg;
transform: translate(var(--tw-translate-x), var(--tw-translate-y))
rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y))
scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
.-translate-y-1\/2 {
--tw-translate-y: 50%;
transform: translate(var(--tw-translate-x), var(--tw-translate-y))
rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y))
scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
- 降低组件封装成本,提高开发效率
理想情况下使用Tailwind替代语义化CSS,相当于直接剔除了一层树形结构,即只保留HTML结构和JS业务逻辑。这样在抽离出组件时,就可以无需考虑CSS样式的拆分,大大降低了组件封装成本。这种结构也更加贴合原生和Flutter的框架模型。
无需脱离HTML构建UI本身也简化了工作流程,开发人员无需在HTML和CSS样式表之间切换或者拆分窗口编写UI。
- 无需命名class
避免了繁杂的、消耗精力的class命名工作。用类似行内样式的形式,但更精简的方式专注于样式的表达。
过于精简的class命名可读性差,过于复杂的命名影响开发效率。BEM的命名风格用于工具库的开发是一个非常适合的选择,但在日常业务开发中成本过高。
Less/Sass(SCSS)
除了Tailwind的形式,正常使用Less或Sass进行媒体查询当然也是可行的。我们可以利用各个预编译CSS语言的特性简化这一过程,以Less为例:
// var.less
@screen-sm: ~"screen and (max-width: 640px)";
@screen-tablet: ~"screen and (max-width: 768px)";
@screen-desktop: ~"screen and (min-width: 769px)";
@screen-lg: ~"screen and (max-width: 1024px)";
@screen-xl: ~"screen and (max-width: 1280px)";
@screen-2xl: ~"screen and (max-width: 1536px)";
// 使用
.box {
padding: 20px;
@media @screen-2xl {
padding: 12px;
}
}
自动引入
一般情况下Less的全局变量和方法在vue文件中使用需要 @import 引入,使用Less提供的动态代码注入或者 style-resources-loader 插件可以简化这一步。
// config.less
@import "var.less";
@import "mixin.less";
// vite.config.ts
css: {
preprocessorOptions: {
less: {
modifyVars: {
hack: `true; @import (reference) "${resolve('src/style/config.less')}";`,
},
},
},
}
JS媒体查询
使用CSS理论上可以实现所有适配过程中的布局控制。如不能通过直接调整样式而必须改变HTML结构的情况,也可以使用 display 属性来实现。但还有一些场景需要在JS层面对不同屏幕规格做处理,使用动态绑定自定义class,或是部分第三方组件只能通过prop传参的方式控制显示效果等,此时也可以使用JS进行媒体查询。
如果你是Vue项目建议直接使用 vueuse 的封装 useMediaQuery。
// 断点枚举(与Tailwind保持一致)
export enum MQ {
xl = '(max-width: 1535px)',
lg = '(max-width: 1279px)',
md = '(max-width: 1023px)',
sm = '(max-width: 767px)',
xs = '(max-width: 639px)',
}
// 返回响应式变量
const isLarge = useMediaQuery(MQ.lg);
布局单位
前文提到的三种类型的单位——px、rem、vw/vh,我们到底应该使用哪一种呢?应该怎么使用,该不该使用转换插件呢?
一般情况,基于媒体查询的响应式布局,固定尺寸的单位只需要用到px单位即可。要保持最佳的展示效果,布局元素的尺寸不可能随着窗口尺寸变化而线性变化,因此我们也不再依赖H5的动态尺寸适配。
不过真到了急着上线,没空做适配了,也有懒可偷🥵——引入pxToRem类型的插件,通过媒体查询手动调整根节点的 font-size,达到对UI元素尺寸整体上控制的效果,这样或许可以减少一部分断点的适配。
@media @screen-xl {
:root {
font-size: 12px !important;
}
}
@media @screen-xs {
:root {
font-size: 9px !important;
}
}
但这种方式有一个致命缺陷——调整过后的尺寸转化比例会和原来不同。例如默认16px=1rem的情况下,当将根节点font-size改为8px后,由于转化插件的rootValue未发生改变,会出现16px实际对应8px显示效果的情况。这时如果再使用固定尺寸布局,就不能直接使用设计稿中的值。
这种粗糙的适配对后期维护、代码可读性都有较大影响,毫无疑问是“魔道”,不建议作为常规方案实施。最佳方案还是要在最初布局时就采用少使用固定尺寸、多对组件进行封装、多使用响应式布局等思路。
案例
准则
使用 flex
不同断点之间过渡的过程中,横纵方向上的变动最常见的几个场景之一。因此在初期布局时就使用弹性布局是很好的习惯,适配过程中通过 flex-derection 一个属性就实现行列转变、顺逆排列的变化,即使你使用标准流布局似乎也能达到想要的效果。高效的适配就是用最小的代码改动兼容最多的视窗规格。
列表/表格间距使用 gap,不使用 margin
除了行列变化,flex和grid布局之间的转变也是很常见的场景。此时如果使用 gap/column-gap/row-gap 来控制行列之间的间距,就可以在两种布局之间同时生效,减少代码改动。
慎用固定宽高
为了保持好的显示效果,布局元素的宽高在不同视窗大小之间的变动是非常频繁的。因此使用了越多固定宽高值,就意味着适配工作量越大。实在不能避免时,应该遵循“宽度上的固定值越靠近根节点越好,高度上的固定值越远离根节点越好”的思路,使宽度上子元素通过百分比布局、弹性布局的方式自适应;高度上由子元素向父元素传递大小,撑开父容器。
使用 max-width/min-width/max-height/min-height
相比使用固定宽高,使用约束宽高是更好的选择。例如常见的PC端版心布局(如淘宝、京东、政务官网)就是给最外层容器设置 min-width 达到“当屏幕大于版心宽度时,版心居中显示;当屏幕小于版心宽度时,出现横向滚动条”的效果。
当然,我们的移动端适配不可能采用滚动条的方式,但其引申出来的“小版心”布局却是我们多端适配的一大利器。与版心布局不同的是,当屏幕小于版心宽度时,我们可以利用百分比布局,或是前文改变行列布局、flex/grid布局互转,改变 grid-template-columns 值等方式,将横向排列过于局促的元素往纵向延伸。即横向永远不出现滚动条,而纵向滚动符合我们的操作习惯,我们可以认为其有无限的空间,不过此时我们就需要设置版心的 max-width 而不是 min-width 。
总结
媒体查询、响应式布局相信大家都有所了解。本文强调的是如何低成本的、快速的利用这些技术做多端适配。
细心的朋友应该能发现 Tailwind 占了本文比较大的篇幅。这个库确实不是做响应式适配必备的,而且很多人觉得它学习成本比较高,属性太多记不住,默认的间距值、文字大小还得换算一下才能和设计稿对应上。但大部分的属性和原有的CSS属性名称其实都有关联,熟悉CSS的开发连蒙带猜都能猜出个七八成。最重要的,它极大的降低了媒体查询的使用成本,甚至一定程度上把CSS优化掉了(,我觉得最难的,可能是说服你的团队去使用它。