跳到主要内容

移动端适配

物理像素

物理像素又被称为设备像素、设备物理像素,它是显示器(电脑、手机屏幕)最小的物理显示单位,每个物理像素由颜色值和亮度值组成。

所谓的一倍屏、二倍屏(Retina)、三倍屏,指的是设备以多少物理像素来显示一个 CSS 像素,也就是说,多倍屏以更多更精细的物理像素点来显示一个 CSS 像素点。

在普通屏幕下 1 个 CSS 像素对应 1 个物理像素,而在 Retina 屏幕下,1 个 CSS 像素对应的却是 4 个物理像素(参照下文田字示意图理解)。

设备独立像素

设备独立像素又被称为 CSS 像素,是我们写 CSS 时所用的像素,它是一个抽像的单位,主要使用在浏览器上,用来精确度量 Web 页面上的内容。

设备像素比

设备像素比简称为 dpr,定义了物理像素和设备独立像素的对应关系:设备像素比 = 物理像素 / 设备独立像素。

CSS 的 1px 等于几个物理像素,除了和屏幕像素密度 dpr 有关,还和用户缩放有关系。

例如,当用户把页面放大一倍,那么 CSS 中 1px 所代表的物理像素也会增加一倍;相反把页面缩小一倍,CSS 中 1px 所代表的物理像素也会减少一倍。

viewport

viewport 就是设备上用来显示网页的那一块区域,但 viewport 又不局限于浏览器可视区域的大小,它可能比浏览器的可视区域要大,也可能比浏览器的可视区域要小。

一般来讲,移动设备上的 viewport 都是要大于浏览器可视区域的,这是因为考虑到移动设备的分辨率相对于桌面电脑来说都比较小,所以为了能在移动设备上正常显示那些传统的为桌面浏览器设计的网站,移动设备上的浏览器都会把自己默认的 viewport 设为 980px 或 1024px(也可能是其它值,这个是由设备自己决定的),但带来的后果就是浏览器会出现横向滚动条,因为浏览器可视区域的宽度是比这个默认的 viewport 的宽度要小的。

viewport

明确三种不同的 viewport 视口:

visual viewport: 可见视口,指屏幕宽度

layout viewport: 布局视口,指 DOM 宽度

ideal viewport: 理想适口,使布局视口就是可见视口即为理想适口

获取屏幕宽度(visual viewport)的尺寸:

window.innerWidth
window.innerHeight

获取 DOM 宽度(layout viewport)的尺寸:

document.documentElement.clientWidth
document.documentElement.clientHeight

设置理想视口 ideal viewport:

<meta
name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"
/>

该 meta 标签的作用是让 layout viewport 的宽度等于 visual viewport 的宽度,同时不允许用户手动缩放,从而达到理想视口。

适配方案

  1. 媒体查询:通过 CSS3 中的媒体查询,根据设备的屏幕大小动态设置样式。这种方案适用于一些简单的页面,但需要编写大量的媒体查询规则,维护起来较为麻烦。
  2. REM 布局:将页面的尺寸都转化为相对于根元素(HTML 元素)的单位 REM,通过设置根元素的字体大小,实现页面的自适应。这种方案需要计算和设置基准字体大小,比较灵活,但对于一些细节问题需要进行额外的处理。
  3. Flex 布局:使用 CSS3 中的 flex 布局,通过设置容器的 flex 属性,实现页面的自适应。这种方案比较简单易用,但对于某些较复杂的页面,可能需要写大量的 CSS 代码。
  4. 视口单位:通过使用 CSS3 中的视口单位(vw、vh、vmin、vmax),根据设备的屏幕大小动态设置样式。这种方案可以方便地设置相对于视口的尺寸,但对于某些浏览器的兼容性存在问题。

在实际开发中,通常需要结合不同的方案进行使用,以适应不同的场景和要求。例如,可以使用 REM 布局实现基本的自适应效果,再结合媒体查询或 Flex 布局进行优化和调整。

rem 适配方案

提示

amfe-flexible中修改屏幕的放大缩小比例(scale),应该是为了解决 1px 线的问题。

因为最新代码中已经不修改 scale 值了,而是在检测 0.5px 兼容性后添加了 class 类 hairlines

适配是为了使页面在不同手机设备上,相对保持统一的效果。移动端自适应方案很多,有百分比布局,弹性盒模型布局等,这里介绍 rem 布局。

rem 是相对于根元素的字体大小的单位,我们可以根据设备宽度动态设置根元素的 font-size,使得以 rem 为单位的元素在不同终端上以相对一致的视觉效果呈现。下面介绍 3 种根据屏幕宽度设置 rem 基准值的方法。(注:为了换算方便,以下三种方法都用 1:100 的比例,即 1rem=100px。)

用 JS 设置 rem 基准值

/* 设计稿是750,采用1:100的比例,用1rem表示100px,font-size为100 * (clientWidth / 750) */
;(function (doc, win) {
var docEl = doc.documentElement,
resizeEvt = 'orientationchange' in window ? 'orientationchange' : 'resize',
recalc = function () {
var clientWidth = docEl.clientWidth
if (!clientWidth) return
docEl.style.fontSize = 100 * (clientWidth / 750) + 'px'
}
if (!doc.addEventListener) return
win.addEventListener(resizeEvt, recalc, false)
doc.addEventListener('DOMContentLoaded', recalc, false)
})(document, window)

媒体查询设置 font-size

/* 以min-width: 750px时font-size: 100px为基准,min-width每缩小100px,font-size就缩小13.3333px。*/
@media screen and (min-width: 320px) {
html {
font-size: 42.6667px;
}
}
@media screen and (min-width: 375px) {
html {
font-size: 50px;
}
}
@media screen and (min-width: 425px) {
html {
font-size: 56.6667px;
}
}
@media screen and (min-width: 768px) {
html {
font-size: 102.4px;
}
}

用单位 vw 设置 font-size

1vw 等于屏幕可视区宽度的可视区域的百分之一。

/* 设计稿是750,采用1:100的比例,用1rem表示100px,font-size为100*(100vw/750) */
html {
font-size: calc(100 * 100vw / 750);
}

位图

位图, 又称为点阵图像、像素图或栅格图像,是由称作像素(图片元素)的单个点组成。这些点可以进行不同的排列和染色以构成图样。

位图的单位:像素(Pixel);

像素(Pixel):指可以表现亮度甚至色彩变化的一个点,是构成数字图像的最小单位。像素具有大小相同、明暗和颜色的变化。特点是有固定的位置和特定的颜色值。

位图特点:

位图图像善于重现颜色的细微层次,能够制作出色彩和亮度变化丰富的图像,可逼真地再现这个世界,文件庞大,不能随意缩放;打印和输出的精度是有限的;

矢量图

矢量又称为“向量”,矢量图形中的图形元素(点和线段)称为对象,每个对象都是一个单独的个体,它具有大小、方向、轮廓、颜色和屏幕位置等属性。简单地说,矢量图形软件就是用数学的方法来绘制矩形等基本形状。

矢量图特点:

矢量图形能重现清晰的轮廓,线条非常光滑、且具有良好的缩放性;因为图像中保存的是线条和图块的信息,与分辨率和图形大小无关,只与图像的复杂程度有关,所以图像文件所占的存储空间交较小;此外文字编辑能力强。

与位图相比,在显示和打印方面都快的多;图形不真实生动,颜色不丰富。无法像照片一样真实地再现这个世界的景色。

图片模糊的问题

一个位图像素是栅格图像(如:png, jpg, gif 等)最小的数据单元。每一个位图像素都包含着一些自身的显示信息(如:显示位置,颜色值,透明度等)。理论上,1 个位图像素对应于 1 个物理像素,图片才能得到完美清晰的展示。对于 dpr=2 的 Retina 屏幕而言,1 个位图像素对应于 4 个物理像素,由于单个位图像素不可以再进一步分割,所以只能就近取色,导致图片看起来比较模糊,如下图。

模糊

对于图片模糊问题,比较好的方案就是用多倍图片(@2x)。如:一个 200×300(CSS pixel)的 img 标签,对于 dpr=2 的屏幕,用 400×600 的图片,如此一来,位图像素点个数就是原来的 4 倍,在 Retina 屏幕下,位图像素点个数就可以跟物理像素点个数形成 1 : 1 的比例,图片自然就清晰了。

如果普通屏幕下,也用了两倍图片,会怎样呢?

在普通屏幕下,200×300(CSS pixel)img 标签,所对应的物理像素个数就是 200×300 个,而两倍图片的位图像素个数是 200×300×4 个,所以就出现一个物理像素点对应 4 个位图像素点,但它的取色也只能通过一定的算法取某一个位图像素点上的色值,这个过程叫做(downsampling),肉眼看上去虽然图片不会模糊,但是会觉得图片缺少一些锐利度,或者是有点色差,如下图。

失真

所以最好的解决办法是:不同的 dpr 下,加载不同的尺寸的图片。不管是通过 CSS 媒体查询,还是通过 JS 条件判断都是可以的。

canvas 绘制模糊

因为 canvas 不是矢量图,而是像图片一样是位图模式的。剩下的原理就是和图片模糊的问题一样了。

解决方式就是把 canvas 画布大小调整到设备的物理像素宽度,然后通过 style 显示为需要的大小

<canvas class="my-canvas" width="200" height="200" style="width: 100px; height: 100px;"></canvas>
function setupCanvas(canvas) {
var dpr = window.devicePixelRatio || 1
var rect = canvas.getBoundingClientRect()
canvas.width = rect.width * dpr
canvas.height = rect.height * dpr
var ctx = canvas.getContext('2d')
ctx.scale(dpr, dpr)
return ctx
}

var ctx = setupCanvas(document.querySelector('.my-canvas'))
ctx.lineWidth = 5
ctx.beginPath()
ctx.moveTo(100, 100)
ctx.lineTo(200, 200)
ctx.stroke()

1px 细线问题

先明确一个很重要的点:

在不同的屏幕上(普通屏幕 vs retina 屏幕),css 像素所呈现的大小(物理尺寸)是一致的,不同的是 1 个 css 像素所对应的物理像素个数是不一致的。

那为什么设计师会觉得 1px 的线太粗了?

因为设计师口中说的 1px 是针对设备物理像素的。

对于普通屏幕,1 物理像素就是 1px,问题可能不大;但是在 Retina@2 下,1 物理像素是 0.5px,所以理论上开发应该写成 0.5px。然而并不是所有的手机浏览器都能支持 0.5px

这个就是 1px 问题的来源。

伪元素+transform

构建 1 个伪元素, border 为 1px, 再以 transform 缩放到 50%。

/* 设计稿是750,采用1:100的比例,font-size为100*(100vw/750) */
.border-1px {
position: relative;
}
@media screen and (-webkit-min-device-pixel-ratio: 2) {
.border-1px:before {
content: ' ';
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 1px;
border-top: 1px solid #d9d9d9;
color: #d9d9d9;
-webkit-transform-origin: 0 0;
transform-origin: 0 0;
-webkit-transform: scaleY(0.5);
transform: scaleY(0.5);
}
}

用 JS 计算 rem 基准值和 viewport 缩放值

/* 设计稿是750,采用1:100的比例,font-size为100 * (docEl.clientWidth * dpr / 750) */
var dpr, rem, scale
var docEl = document.documentElement
var fontEl = document.createElement('style')
var metaEl = document.querySelector('meta[name="viewport"]')
dpr = window.devicePixelRatio || 1
rem = 100 * ((docEl.clientWidth * dpr) / 750)
scale = 1 / dpr
// 设置viewport,进行缩放,达到高清效果
metaEl.setAttribute(
'content',
'width=' +
dpr * docEl.clientWidth +
',initial-scale=' +
scale +
',maximum-scale=' +
scale +
', minimum-scale=' +
scale +
',user-scalable=no',
)
// 设置data-dpr属性,留作的css hack之用,解决图片模糊问题和1px细线问题
docEl.setAttribute('data-dpr', dpr)
// 动态写入样式
docEl.firstElementChild.appendChild(fontEl)
fontEl.innerHTML = 'html{font-size:' + rem + 'px!important;}'

相较与于上文 rem 适配方案里“用 JS 计算 rem 基准值”的方案,这个“用 JS 计算 rem 基准值和 viewport 缩放值”的方案可以解决 1px 细线问题。 同时对于图片模糊问题,只需要根据 data-dpr 的值动态加载不同尺寸的图就可以了。

遗留问题

  1. 阿里手淘 h5 页面的适配方案

阿里手淘 h5 页面的适配方案lib-flexible,之前也是用 js 来计算 rem 和 viewport。但后来的2.0 版本却不再计算 viewport,而是固定为 1。

暂时不明白 viewport 的缩放会带来哪些问题?

参考文章