移动端HTML5响应式布局适配解决方案

目录

前言

  移动端适配是一个十分重要的问题。在移动端页面中,其布局和交互方式可能相比于PC端少一些,但是由于设备的多样化,其适配问题就必须多加关注。设备情况种类繁多复杂,从网上的几张图中足以看出:










基本概念

物理像素(physical pixel)

一个物理像素是显示器(手机屏幕)上最小的物理显示单元,在操作系统的调度下,每一个设备像素都有自己的颜色值和亮度值。

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

设备独立像素(也叫密度无关像素),可以认为是计算机坐标系统中得一个点,这个点代表一个可以由程序使用的虚拟像素(比如: css像素),然后由相关系统转换为物理像素。可以理解为就是下面要说的逻辑像素。所以说,物理像素和设备独立像素之间存在着一定的对应关系,这就是接下来要说的设备像素比。

设备像素比(device pixel ratio,即简称dpr)


设备像素比(简称dpr)定义了物理像素和设备独立像素的对应关系,它的值可以按如下的公式的得到:
设备像素比 = 物理像素 / 设备独立像素

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

viewport

通俗来讲,移动端的viewport就是我们所能看到的手机端浏览器的可视窗口大小,但viewport又不仅仅局限于可视窗口的大小,一般情况下,它是比默认窗口大小要大的,这是因为考虑到移动设备的分辨率相对于桌面电脑来说都比较小,所以为了能在移动端正常显示为桌面浏览器而设计的网页,移动端的浏览器都会默认把自己的默认的viewport设为980px到1024px不等,但其后果就是会出现横向滚动条,因为移动端浏览器可视区域的大小是比默认的viewport宽度要小的。

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

该meta标签的作用是让当前viewport的宽度等于设备的宽度,同时不允许用户手动缩放。

PPI(Pixels Per Inch)


像素密度,这项指标是连接数字世界与物理世界的桥梁。Pixels per inch,准确的说是每英寸的长度上排列的像素点数量。1英寸是一个固定长度,等于2.54厘米,大约是食指最末端那根指节的长度。像素密度越高,代表屏幕显示效果越精细。Retina屏比普通屏清晰很多,就是因为它的像素密度翻了一倍。

DPI(Dots Per Inch)

每英寸所打印的点数,是打印机、鼠标等设备分辨率的单位。这是衡量打印机打印精度的主要参数之一,一般来说,该值越大,表明打印机的打印精度越高。dpi是指每英寸的像素,也就是扫描精度。国际上都是计算一平方英寸面积内像素的多少。

倍率与逻辑像素


  我们在css中声明的是逻辑像素值,而不同设备具体用多少物理像素进行显示有所不同。Retina屏幕把2x2个像素当1个像素使用。比如原本44像素高的顶部导航栏,在Retina屏上用了88个像素的高度来显示。导致界面元素都变成2倍大小,画质更清晰。iOS应用的资源图片中,同一张图通常有多个尺寸。你会看到文件名有的带@2x、@3x字样,有的不带。其中不带@2x、@3x的用在普通屏上,带@2x的用在Retina屏上(iPhone6等尺寸),带@3x的也用在Retina屏上(iPhone6plus等尺寸)。只要图片准备好,iOS会自己判断用哪张,Android道理也一样。而在web app开发中,则需要自己对图片进行选择。苹果以普通屏为基准,给Retina屏定义了一个2倍或3倍的倍率。实际像素除以倍率,就得到逻辑像素尺寸。只要两个屏幕逻辑像素相同,它们的显示效果就是相同的。


依赖库 lib-flexible


使用实践

引入

1.npm包安装
npm install --save-dev lib-flexible
2.在基础js文件靠前位置引入包文件(ES6写法)
import 'lib-flexible';

对于长度、宽度等“距离”的写法(less文件)

1
2
3
4
5
6
7
8
9
@font-size-base: 75;

.home {
position: fixed;
top: 88rem / @font-size-base;
bottom: 98rem / @font-size-base;
width: 100%;
background-color: #ddd;
}

对于文字字号大小的写法(less文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@font-size-base: 75;
.name {
display: flex;
font-family: @font-family-base;
color: #000000;
letter-spacing: 0;
font-size: 17px;
[data-dpr="2"] & {
font-size: 34px;
}
[data-dpr="3"] & {
font-size: 51px;
}
font-weight: bold;
padding-bottom: 20rem/@font-size-base
}

源码、原理分析

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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116

;(function(win, lib) {
var doc = win.document;
var docEl = doc.documentElement;
var metaEl = doc.querySelector('meta[name="viewport"]');
var flexibleEl = doc.querySelector('meta[name="flexible"]');
var dpr = 0;
var scale = 0;
var tid;
var flexible = lib.flexible || (lib.flexible = {});

if (metaEl) {
console.warn('将根据已有的meta标签来设置缩放比例');
var match = metaEl.getAttribute('content').match(/initial\-scale=([\d\.]+)/);
if (match) {
scale = parseFloat(match[1]);
dpr = parseInt(1 / scale);
}
} else if (flexibleEl) {
var content = flexibleEl.getAttribute('content');
if (content) {
var initialDpr = content.match(/initial\-dpr=([\d\.]+)/);
var maximumDpr = content.match(/maximum\-dpr=([\d\.]+)/);
if (initialDpr) {
dpr = parseFloat(initialDpr[1]);
scale = parseFloat((1 / dpr).toFixed(2));
}
if (maximumDpr) {
dpr = parseFloat(maximumDpr[1]);
scale = parseFloat((1 / dpr).toFixed(2));
}
}
}
if (!dpr && !scale) {
var isAndroid = win.navigator.appVersion.match(/android/gi);
var isIPhone = win.navigator.appVersion.match(/iphone/gi);
var devicePixelRatio = win.devicePixelRatio;
if (isIPhone) {
// iOS下,对于2和3的屏,用2倍的方案,其余的用1倍方案
if (devicePixelRatio >= 3 && (!dpr || dpr >= 3)) {
dpr = 3;
} else if (devicePixelRatio >= 2 && (!dpr || dpr >= 2)){
dpr = 2;
} else {
dpr = 1;
}
} else {
// 其他设备下,仍旧使用1倍的方案
dpr = 1;
}
scale = 1 / dpr;
}
docEl.setAttribute('data-dpr', dpr);
if (!metaEl) {
metaEl = doc.createElement('meta');
metaEl.setAttribute('name', 'viewport');
metaEl.setAttribute('content', 'initial-scale=' + scale + ', maximum-scale=' + scale + ', minimum-scale=' + scale + ', user-scalable=no');
if (docEl.firstElementChild) {
docEl.firstElementChild.appendChild(metaEl);
} else {
var wrap = doc.createElement('div');
wrap.appendChild(metaEl);
doc.write(wrap.innerHTML);
}
}

function refreshRem(){
var width = docEl.getBoundingClientRect().width;
if (width / dpr > 540) {
width = 540 * dpr;
}
var rem = width / 10;
docEl.style.fontSize = rem + 'px';
flexible.rem = win.rem = rem;
}

win.addEventListener('resize', function() {
clearTimeout(tid);
tid = setTimeout(refreshRem, 300);
}, false);
win.addEventListener('pageshow', function(e) {
if (e.persisted) {
clearTimeout(tid);
tid = setTimeout(refreshRem, 300);
}
}, false);

if (doc.readyState === 'complete') {
doc.body.style.fontSize = 12 * dpr + 'px';
} else {
doc.addEventListener('DOMContentLoaded', function(e) {
doc.body.style.fontSize = 12 * dpr + 'px';
}, false);
}


refreshRem();

flexible.dpr = win.dpr = dpr;
flexible.refreshRem = refreshRem;
flexible.rem2px = function(d) {
var val = parseFloat(d) * this.rem;
if (typeof d === 'string' && d.match(/rem$/)) {
val += 'px';
}
return val;
}
flexible.px2rem = function(d) {
var val = parseFloat(d) / this.rem;
if (typeof d === 'string' && d.match(/px$/)) {
val += 'rem';
}
return val;
}

})(window, window['lib'] || (window['lib'] = {}));

手淘团队相关文献参考

使用Flexible实现手淘H5页面的终端适配