基于Vue移动端自适应方案与视网膜1px问题

开始之前

需要了解的知识有:

  1. 移动web适配利器-rem
  2. 使用Flexible实现手淘H5页面的终端适配

简单粗暴的粗略版移动端自适应

昨天说的粗糙的方案,现在写个栗子。重温一下那个思路:

  1. 获取设备宽度width
  2. 跟据设备宽度设置rem的值(1rem=width/倍数),设置body的font-size即可,即font-size = width/倍数
  3. 设计稿上的px等比换算成rem

核心代码:

1
2
3
4
5
6
7
let docEl = window.document.documentElement;
// 获取当前窗口的宽度
let width = docEl.getBoundingClientRect().width;
// 计算rem的值,这里1rem=width/10,即把屏幕等分为10份
let rem = width / 10;
// 设置html节点的font-size值
docEl.style.fontSize = rem + "px";

之后就可以根据设计稿进行换算啦。就假设设计稿给出的是375px,这时候计算出1rem=37.5px。

然后假设有一个div的宽度是120px,这时候可计算出width = 120px/37.5 = 3.2rem。

1
2
3
div{
width:3.2rem
}

完整代码:

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
<template>
<div class="continer">
<img class="banner" src="https://upload-images.jianshu.io/upload_images/7166236-cb6e172d201bd31e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240"/>
<div class="item" v-for="n in 3" >
<div class="item-left">left</div>
<div class="item-right">
<div class="item-right-sub1">11111111111111111</div>
<div class="item-right-sub1">222222222222222222</div>
<div class="item-right-sub1">333333333333333333</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "normal-rem",
created(){
let docEl = window.document.documentElement;
// 获取当前窗口的宽度
let width = docEl.getBoundingClientRect().width;
// 计算rem的值,这里1rem=width/10,即把屏幕等分为10份
let rem = width / 10;
// 设置html节点的font-size值
docEl.style.fontSize = rem + "px";
}
}
</script>
<style scoped>
.continer{width: 10rem;margin: 0 auto;font-size: 0.5rem;}
.continer .banner{display: inline-block;width: 10rem;}
.continer .item{margin: 0.27rem 0;width: 10rem;height: 3.2rem;background-color: aqua;display: flex;}
.continer .item .item-left{background-color: brown; width: 3.2rem;height: 3.2rem;}
.continer .item .item-right{height: 3.2rem;display: flex;flex-direction: column;}
.continer .item .item-right .item-right-sub1{height: 1.07rem;}
</style>

看几个效果截图:

image.png

image.png

image.png

至此,完成一个简易版的移动端自适应方案,但是,这个方案存在几个问题,首先就是兼容性问题,还有视网膜1px的问题也没做处理,还有就是。。设计稿给的每个以px为单位的属性,都要手动计算转化为rem,很麻烦。

基于Vue移动端自适应方案

基本思路也是上面的例子的思路,但是用到了2个包。

淘宝-lib-flexible:通过动态设置meta和动态设置font-size来实现自适应,解决了上述例子存在的问题。

px2rem:自动计算实现了px向rem转化。

Reference-vue-cli 配置flexible

安装依赖

安装flexible:
1
npm install lib-flexible --save

引入flexible:在项目入口文件main.js中添加如下代码:

1
import 'lib-flexible'
安装px2rem-loader:
1
npm install px2rem-loader --save-dev

配置px2rem-loader:

在vue-cli生成的文件中,找到以下文件 build/utils.js,如下图添加配置 :

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
const cssLoader = {
loader: 'css-loader',
options: {
sourceMap: options.sourceMap,
importLoaders:2 //在css-loader前应用的loader数目,默认为0
//如果不加这个,@import的外部css文件将不能正常转化。
//如若还不行,试着调大数字。更改之后必须重启项目才能生效。
}
}
const px2remLoader={
loader:'px2rem-loader',
options:{
remUnit:37.5 //设计稿的1/10,假设设计稿是375px
}
}
function generateLoaders (loader, loaderOptions) {
const loaders = options.usePostCSS ? [cssLoader, postcssLoader,px2remLoader] : [cssLoader,px2remLoader]
if (loader) {
loaders.push({
loader: loader + '-loader',
options: Object.assign({}, loaderOptions, {
sourceMap: options.sourceMap
})
})
}

px2rem-loader用法:

1
2
3
直接写px,编译后会直接转化成rem ---- 除开下面两种情况,其他长度用这个
在px后面添加/*no*/,不会转化px,会原样输出。 --- 一般border需用这个
在px后面添加/*px*/,会根据dpr的不同,生成三套代码。---- 一般字体需用这个
重启项目

就能直接按照设计稿上的标注做了。

源码如下,就不截图了:

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
<template>
<div class="continer">
<img class="banner" src="https://upload-images.jianshu.io/upload_images/7166236-cb6e172d201bd31e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240"/>
<div class="item" v-for="n in 3" >
<div class="item-left">left</div>
<div class="item-right">
<div class="item-right-sub1">11111111111111111</div>
<div class="item-right-sub1">222222222222222222</div>
<div class="item-right-sub1">333333333333333333</div>
</div>
</div>
</div>
</template>
<script>
export default {
data () {return {}},
created(){
document.getElementsByTagName('html')[0].style.backgroundColor = '#F8F8FF';
}
}
</script>
<style scoped>
.continer{width: 375px;height: 200px;margin: 0 auto;}/*单位是px*/
.continer .banner{display: inline-block;width: 375px;}
.continer .item{margin: 10px 0;width: 375px;height: 120px;background-color: aqua;display: flex;}
.continer .item .item-left{background-color: brown;width: 120px;height: 120px;}
.continer .item .item-right{height: 120px;display: flex;flex-direction: column;}
.continer .item .item-right .item-right-sub1{height: 40px;}
</style>

Rem存在的问题

  1. 开头要引入一段js代码,单位都要改成rem(font-size可以用px),计算rem比较麻烦(可以引用预处理器,但是增加了编译过程,相对麻烦了点)。pc和mobile要分开。
  2. 会出现出计算出小数的情况,不同浏览器的行为表现可能不同。
  3. 若手机修改了默认字体大小,可能会出现意想不到的情况,布局错乱。

剖析flexible

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

;(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) {
// 通过设置meta解决1px问题
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);
}
}
//重点:首先获取document元素的宽度值,给html设置rem的值
function refreshRem(){
var width = docEl.getBoundingClientRect().width;
//540是个经验值,或者最大值。目前主流的手机最大的css像素尺寸,是540(devicePixelRatio为2,分辨率 //是1080x1920的手机),所以用了这个经验值。
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') {
//body上设置12*dpr的font-size值,是为了重置页面中的默认字体,不然没有设置font-size的元素会继承 //html上的font-size,变得更大。
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'] = {}));

1px问题

成因

这个问题本来不是特别理解成因是什么,不过刚刚同学给我解释了,大概明白了。

  • 设备独立像素:设备独立像素(也叫密度无关像素),可以认为是计算机坐标系统中得一个点,这个点代表一个可以由程序使用的虚拟像素(比如: css像素),然后由相关系统转换为物理像素。
  • 物理像素:一个物理像素就是显示器上最小的物理显示单元,在操作系统的调度下,每一个设备像素都有自己的颜色值和亮度值。
  • 像素比(devicePixelRatio dpr):设备物理像素 / 设备独立像素,由于不同的手机拥有的像素比也不尽相同,因此一个css像素占据的物理像素个数也是不一样,也就是说css中的1px并不等于移动设备的1px (有人说是为了降低颗粒感,增强用户体验)。

UI设计师设计的时候,画的1px(真实像素)实际上是0.5px(css)的线或者边框。但是他不这么认为,他认为他画的就是1px的线,因为他画的稿的尺寸本身就是屏幕尺寸的2倍。假设手机视网膜屏的宽度是320x480宽,但实际尺寸是640x960宽,设计师设计图的时候一定是按照640x960设计的。但是前端工程师写代码的时候,所有css都是按照320x480写的,写1px(css),浏览器自动变成2px(真实像素)。

那么前端工程师为什么不能直接写0.5px(css)呢?因为在老版本的系统里写0.5px(css)的话,会被浏览器解读为0px(css),就没有边框了。所以只能写成1px(css),实际在屏幕上显示出来就是设计师画的1px(真实像素)的2倍那么宽,所以设计师会觉得这个线太粗了,和他的设计稿不一样。在新版的系统里,已经开始逐渐支持0.5px(css)这种写法。所以如果设计师在大图上设计了一个1px(真实像素)的线的话,前端工程师直接除以2,写0.5px(css)就好了。

https://segmentfault.com/q/1010000010750697

解决方案

0.5px边框

由于存在兼容问题,可通过js检测浏览器能否处理0.5px边框,如果可以,给html标签元素添加个class。

优点:简单,不需要过多的代码。

缺点:无法兼容安卓设备,ios8以下的设备。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if (window.devicePixelRatio && devicePixelRatio >= 2) {
var testElem = document.createElement('div');
testElem.style.border = '.5px solid transparent';
document.body.appendChild(testElem);
if (testElem.offsetHeight == 1) {
document.querySelector('html').classList.add('hairlines');
}
document.body.removeChild(testElem);
}

div {
border: 1px solid #bbb;
}
.hairlines div {
border-width: 0.5px;
}
动态设置viewport

淘宝也是这种解决方案。

优点:所有场景都能满足,一套代码,可以兼容基本所有布局。

在devicePixelRatio = 2 时,输出viewport:

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

在devicePixelRatio = 3 时,输出viewport:

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