移动端Web页面的终端适配详解

面向场景:移动端 Web 页面,需要全局适配各种不同分辨率的的手机。

基本概念

  • 物理像素(physical pixel):

物理像素又被称为设备像素,他是显示设备中一个最微小的物理部件。每个像素可以根据操作系统设置自己的颜色和亮度。正是这些设备像素的微小距离欺骗了我们肉眼看到的图像效果。
屏幕可视宽为 320 像素,非 retina 屏的 pp 是 320,retina 屏的则至少为 640。

  • 设备独立像素(density-independent pixel):

设备独立像素也称为密度无关像素,可以认为是计算机坐标系统中的一个点,这个点代表一个可以由程序使用的虚拟像素(比如说 CSS 像素),然后由相关系统转换为物理像素。
dip 是个虚拟单位,在 web 世界中也称为 css 像素[逻辑像素],屏幕可视宽为 320 像素,dip 也就是 320 像素 。

  • 设备像素比(device pixel ratio)

设备像素比简称为 dpr,其定义了物理像素和设备独立像素的对应关系。它的值可以按下面的公式计算得到:

1
设备像素比 = 物理像素 / 设备独立像素 // 在某一方向上,x方向或者y方向

在 JavaScript 中,可以通过 window.devicePixelRatio 获取到当前设备的 dpr。而在 CSS 中,可以通过-webkit-device-pixel-ratio,-webkit-min-device-pixel-ratio 和 -webkit-max-device-pixel-ratio 进行媒体查询,对不同 dpr 的设备,做一些样式适配(这里只针对 webkit 内核的浏览器和 webview)。

  • 屏幕尺寸:

屏幕尺寸是以屏幕对角线的长度来计量,计量单位为英寸。
例如,iPhone 5 是 4 英寸,iPhone6 是 4.7 英寸,指的是显示屏对角线的长度(diagonal)。

  • 屏幕比例

屏幕比例不是固定的,常见比例是 4:3、16:9,所以长和宽的尺寸是不固定的。
例如,iPhone 5 是 4:3,iPhone 6 是 16:9,现在流行的全面屏大多是 18:9。

部分机型示例

机型 iPhone 5/SE iPhone 6/7/8 iPhone 6/7/8 Plus
设备分辨率代表物理像素(pixel) 640×1136 750×1334 1080×1920(开发时应按照 1242x2208)
逻辑分辨率代表独立像素(point) 320×568 375×678 414×736
设备像素比(dpr) 2 2 3

相关 JS Web API

1
2
3
4
window.devicePixelRatio  // 设备像素比dpr
window.screen.width // 设备屏幕宽度
window.document.documentElement.clientWidth // 获取屏幕的可视区域的宽度(常用)
window.document.documentElement.getBoundingClientRect().width // html元素相对于页面左上角的距离

适配方案

参考淘宝的 flexible.js 修改(目前使用)

在入口页面的 head 标签内中添加以下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" />
<script>
!(function flexible(window) {
/* 设计图文档宽度 */
var docWidth = 750;

var doc = window.document,
docEl = doc.documentElement,
resizeEvt = 'orientationchange' in window ? 'orientationchange' : 'resize';
var recalc = function () {
var clientWidth = docEl.clientWidth;
docEl.style.fontSize = 100 * (clientWidth / docWidth) + 'px';

};
recalc();
if (!doc.addEventListener) return;
window.addEventListener(resizeEvt, recalc, false);
doc.addEventListener('DOMContentLoaded', recalc, false);
})(window);
</script>

这里我们使用的设计稿尺寸是以 750px 为基础设计的,所以要把视觉稿中的 px 转换成 rem

  • rem计算方式:设计图尺寸px / 100 = 实际rem 【例: 100px = 1rem,50px = 0.50rem】;

使用postcss-pxtorem,自动将 px 转为 rem,灰常便利

1
2
3
4
5
6
7
8
9
// px单位大写将忽略转化rem,例如border-width: 2PX;  // ignored
// 注意:以:style=""这样的方式设置的px像素值,其值为实际像素,不会进行转化
pxtorem({
rootValue: 50, //根元素字体大小
unitPrecision: 3, // 指定`px`转换为`rem`单位值的小数位数(很多时候无法整除)
propList: ['*', '!font*'], // 指定可以从`px`更改为`rem`的属性
selectorBlackList: ['.ignore ','.hairlines',], // 指定不转换为`rem`单位的类,可以自定义,可以无限添加,建议定义一至两个通用的类名
minPixelValue: 2, // 设置要替换的最小像素值(大于或等于`2px`才转换为`rem`单位),你也可以设置为你想要的值
})

Tips

  • 淘宝那边的适配方案,不推荐用 rem 作为文本字体大小单位。所以对于字体的设置,仍旧使用 px 作为单位,并配合用 data-dpr 属性来区分不同 dpr 下的的大小

    来自淘宝 lib-flexible:现在绝大多数的字体文件都自带一些点阵尺寸,通常是 16px 和 24px,所以我们不希望出现 13px 和 15px 这样的奇葩尺寸。

来自淘宝 lib-flexible:“针对 OS 9_3 的 UA,做临时处理,强制 dpr 为 1,即 scale 也为 1,虽然牺牲了这些版本上的高清方案,但是也只能这么处理了”;

所以我们干脆直接对所有字体大小使用 px 作为单位来定义,放弃 data-dpr 标识的区分,很鸡肋。(个人推荐用的 px 做字体单位)

  • 一些不推荐使用 rem 的 CSS 属性:border-width、border-radius、box-shadow、transform、background-size
  • 注意只有当你需要某元素的单位要根据屏幕宽度大小变化时,才需要 rem、em 这类动态宽度单位。一般情况下字体大小是没必要根据屏幕宽度变化

使用 vw,viewport 单位,表示将屏幕均分 100 份,1vw 就是 1 份(以后要流行的方案)

去参考下手淘(作者)分享的解决方案
如何在 Vue 项目中使用 vw 实现移动端适配

使用媒体查询(@media screen)

  • 设计图尺寸:750x1334 px
  • rem 计算方式:设计图尺寸 px / 100 = 实际 rem 【例: 100px = 1rem,50px = 0.50rem】
    在模板页面的 css 中添加以下代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
  html{
font-size: 26.66667vw;
}
body {
font-family: 'PingFang SC', Helvetica, 'STHeiti STXihei', 'Microsoft YaHei', Tohoma, Arial, sans-serif;
background-color: #f8f8f8;
font-size: 14px;
color: #333;
line-height: 1.4;
min-width:320px;
max-width:640px;
margin-left:auto;
margin-right:auto;
}
@media only screen and (max-width: 768px) {
html {
font-size:204.8px
}
}

@media only screen and (max-width: 640px) {
html {
font-size:170.66667px
}
}

@media only screen and (max-width: 480px) {
html {
font-size:128px
}
}

@media only screen and (max-width: 414px) {
html {
font-size:110.4px
}
}

@media only screen and (max-width: 412px) {
html {
font-size:109.86667px
}
}

@media only screen and (max-width: 400px) {
html {
font-size:106.66667px
}
}

@media only screen and (max-width: 393px) {
html {
font-size:104.8px
}
}

@media only screen and (max-width: 375px) {
html {
font-size:100px
}
}

@media only screen and (max-width: 360px) {
html {
font-size:96px
}
}

@media only screen and (max-width: 345px) {
html {
font-size:92px
}
}

@media only screen and (max-width: 320px) {
html {
font-size:85.33333px
}
}

其他

处理 1 像素边框

在 rem 方案中,如果没有特殊处理,1px 的线条在 dpr 大于 1 的设备会比较粗。根本原因是 1px 可能会占用两个物理像素点或以上。
可以采用 transform: scale(0.5);来解决,在上面的selectorBlackList添加'.ui-hairline'

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
.ui-hairline, .ui-hairline--bottom, .ui-hairline--left, .ui-hairline--right, .ui-hairline--surround, .ui-hairline--top, .ui-hairline--top-bottom {
position:relative;
}
.ui-hairline--bottom::after, .ui-hairline--left::after, .ui-hairline--right::after, .ui-hairline--surround::after, .ui-hairline--top-bottom::after, .ui-hairline--top::after, .ui-hairline::after {
content:'';
position:absolute;
top:0;
left:0;
width:200%;
height:200%;
-webkit-transform:scale(.5);
transform:scale(.5);
-webkit-transform-origin:0 0;
transform-origin:0 0;
pointer-events:none;
box-sizing:border-box;
border:0 solid #e5e5e5
}
.ui-hairline--top::after {
border-top-width:1px
}
.ui-hairline--left::after {
border-left-width:1px
}
.ui-hairline--right::after {
border-right-width:1px
}
.ui-hairline--bottom::after {
border-bottom-width:1px
}
.ui-hairline--top-bottom::after {
border-width:1px 0
}
.ui-hairline--surround::after {
border-width:1px
}

获取手机设备系统信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
var util = {
getSystemInfo:function(){
var ua = window.navigator.userAgent.toLowerCase();
ua = ua.toLowerCase();
function a(ua) {
if (ua.indexOf(android) > -1) return 'android';
if (/iphone|ipad|ipod|ios/.test(UA)) return 'ios';
return 'unknown';
};
function isWeiXin(ua) {
return ua.indexOf('micromessenger') > -1
};
var obj = {
userAgent:window.navigator.userAgent,
pixelRatio: window.devicePixelRatio,
platform: a(ua),
language:window.navigator.language,
screenWidth: window.screen.width,
screenHeight: window.screen.height,
windowWidth: document.documentElement.clientWidth,
windowHeight: document.documentElement.clientHeight
};
return obj;
}
}
console.log(util.getSystemInfo())

util.getSystemInfo()返回参数说明

属性 类型 说明
userAgent String 用户代理,简称 UA
platform String 客户端平台:Android,iOS
pixelRatio Number 设备像素比
screenWidth Number 屏幕宽度,单位 px
screenHeight Number 屏幕高度,单位 px
windowWidth Number 可使用窗口宽度,单位 px
windowHeight Number 可使用窗口高度,单位 px
language String 语言

Tips

  • userAgent其实里面包含更多信息,例如手机型号,系统版本,
1
2
3
4
5
// 小米note3(手机型号:Mi Note 3,系统版本:Android 9)
var ua = "Mozilla/5.0 (Linux; Android 9; RMX1901 Build/PKQ1.190101.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/66.0.3359.126 MQQBrowser/6.2 TBS/044705 Mobile Safari/537.36 MMWEBID/1437 MicroMessenger/7.0.4.1420(0x2700043C) Process/tools NetType/WIFI Language/zh_CN"

// iPhone 6 Plus(手机型号:unknown,系统版本:iPhone OS 12_3_1)
var ua = "Mozilla/5.0 (iPhone; CPU iPhone OS 12_3_1 like Mac OS X) AppleWebKit/605.1.15(KHTML, like Gecko) Mobile / 15E148 MicroMessenger / 7.0.4(0x17000428) NetType / WIFI Language / zh_CN"

不难发现,在 UA 中,可以看待安卓的手机型号一般在Build/前面,ios 的还并没有发现