概念
对于移动端开发来说,无可避免的就是直面各种设备不同分辨率和不同DPR(设备像素比)的问题,在此忽略其他兼容性问题的探讨。
移动端像素
-
设备像素(dp),也叫物理像素。指设备能控制显示的最小物理单位,意指显示器上一个个的点。从屏幕在工厂生产出的那天起,它上面设备像素点就固定不变了。
-
分辨率,屏幕上物理像素的数量。
-
设备独立像素(dip),又称密度无关像素。可以认为是计算机坐标系统中的一个点,这个点代表一个可以由程序使用并控制的虚拟像素。由相关系统转化为物理像素在设备上体现。
-
css像素,web编程中的概念,属于设备独立像素中的一种,独立于设备,属于逻辑上衡量像素的单位。
-
设备像素比(dpr) = 设备像素值(dps) / 设备独立像素值(dips),代表系统转化时一个css像素占有多少个物理像素。
-
像素密度(ppi),设备(屏幕)每英寸内有多少个像素点。
移动端三个视口
移动端视口 viewport(div100%时的css大小):移动设备上的 viewport 就是设备的屏幕上能用来显示我们的网页的那一块区域,可能与浏览器的可视区域不同。默认比浏览器可视区域要大(980px),这也是为什么一般的PC端网页放在移动端会出现横向滚动条的原因。
移动端中的三个不同的可视区域大小,来自于ppk关于移动设备的viewport研究:
-
布局视口(layout viewport),浏览器默认的viewport,一般比浏览器可视区域大。
-
视觉视口(visual viewport),浏览器的可视区域大小(浏览器的可见区域css像素值)
-
理想视口(ideal viewport),设备的实际物理宽度(device-width),是一种与ppi无关的设备原始的宽度(英寸),例如320px和660px下的iphone的理想视口都是320px。
位图像素
一个位图像素是栅格图像(如:png, jpg, gif等)最小的数据单元。每一个位图像素都包含着一些自身的显示信息(如:显示位置,颜色值,透明度等)。
理论上,1个位图像素对应于1个物理像素,图片才能得到完美清晰的展示。当遇上对应的位图像素与物理像素不统一的时候。
-
位图像素 < 物理像素。 1个位图像素对应于多个物理像素,由于单个位图像素不可以再进一步分割,所以只能就近取色,从而导致图片模糊。(具体取决于设备系统的图像算法,并不是简单的切割图片)(图片拉伸)
-
位图像素 > 物理像素。1个物理像素对应多个位图像素,所以它的取色也只能通过一定的算法(显示结果就是一张位图像素只有原图像素总数四分之一的图片),肉眼看上去虽然图片不会模糊,但是会觉得图片缺少一些锐利度,或者是有点色差(但还是可以接受的)(图片挤压)
rem适配
什么是rem
即以根节点(html)的字体大小作为基准值进行长度计算。
假定 html 的 fontSize 为 16px,则 1rem = 16px
如果我们更改 html 的 fontSize,rem 也会更新,总是保持 1rem = 1 fontSize (html)
为什么使用rem
开发过移动端项目的同学应该都知道,不同手机设备的大小是不一样的,在进行移动端开发时,我们通常会为 html 加上 viewport meta
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
这里得结合上面的移动端像素和移动端视口进行分析,width=device-width
将此时的页面宽度设置为设备宽度(理想视口),所以此时页面宽度等于设备宽度,不同手机的设备宽度是不同的所以页面宽度也不同
iPhone4 页面宽度 = window.innerWidth = 设备宽度 = 320px
iPhone6 页面宽度 = window.innerWidth = 设备宽度 = 375px
所以为了适配不同的设备宽度,我们通常不直接用px来写css代码,因为在不同手机中页面宽度不同,此时px的相对大小也是不同的。如果我们把一个元素设置为375px来达到100%宽度效果的话,在320设备宽度的手机就出问题了。
由此我们引入了 rem 来做适配,在 css 中直接使用 rem 作为计量单位,如果不做些什么的话,1rem = 16px(浏览器默认字体大小),在不同手机上都是一样,还是无法适配,所以要点在于如何根据设备宽度在做转化
// 假定设计稿宽度750px
const designWidth = 750;
// 通过设备宽度(window.innerWidth)和设计稿宽度(designWidth)的比例来设置 html fontSize
document.documentElement.style.fontSize = (window.innerWidth / designWidth) + ‘px‘;
通过上面代码的设置,我们就可以很轻松的适配移动端项目了,假定设计稿上一个元素宽度750px,那我们就在css定义750rem
在设备宽度为320px的手机上
750rem = 750 * 1rem = 750 * (window.innerWidth / designWidth) px = 750 * (320 / 750) px = 320px
同理,在设备宽度为375px的手机上
750rem = 750 * 1rem = 750 * (window.innerWidth / designWidth) px = 750 * (375 / 750) px = 375px
可能还有个问题,为什么不直接用百分比来适配?因为百分比在很多情况下是除不尽或者带有小数的,显然带有小数点的px会带来各种各样的误差
高清适配
如果你觉得移动端适配像上面一样简单转化下就行,那就 too young too sample
1px问题
什么是 1px 问题?
以 iphone6 为例,大家应该听过啥视网膜像素之类的,2倍屏之类的吧。其实也就是此时 设备像素比(dpr) = 设备像素值(dps) / 设备独立像素值(dips),即一个css像素对应两个物理像素,也就是你在css中写的1px其实在设备显示的是两个像素,当你设置 border = 1px
时看起来就没有那种1px的纤细效果,总感觉不尽如人意,差那么一点点味道。
你以为的1px
用户看到的1px(请忽略颜色不同)
追求用户体验的公司通常是不能容忍 1px 问题的
图片的模糊问题
同样的以 iphone6 为例,我们如果定义一张图片宽度为375px,如果图片的像素(位图像素),此时一个像素的图片会对应两个物理像素(参考上面的位图像素),就会造成图片模糊的问题了。你可能会问?那我直接加载750px像素的图片不就好了(位图像素大于物理像素时很多人是看不出失真的)。
答案当然是可以的,但你觉得追求用户体验的公司能容忍无故的流量耗费和性能浪费么?当然不能
解决方案
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
前面也有介绍过这部分代码,但是没有说明 initial-scale=1
的作用,initial-scale
定义了页面的初始缩放,1代表不缩放。initial-scale
的值也会影响页面宽度,即此时的css像素。
前面我们说过,在 viewport meta 的约束下
页面宽度 = window.innerWidth = 设备宽度
,但其实正确的是 页面宽度 = window.innerWidth = 设备宽度 / scale
,为什么是除呢?大家可以想象一下,当页面缩放时(例如scale=0.5),是不是会导致更多的内容内容展示在当前可见区域中,css像素(页面)是变大了。
以 iphone6 为例,当我们设置
<meta name="viewport" content="width=device-width, initial-scale=0.5, user-scalable=no">
此时页面宽度 = window.innerWidth = 设备宽度 / scale = 375 / 0.5 = 750px
,也就是说现在页面宽度(对应css像素)和物理像素是相等的,所以我们设置的 1px 在手机中将真正显示 1pt(1个物理像素),也就解决了1px的问题。
所以解决方法如下
// 获取设备dpr
const dpr = window.devicePixelRatio;
// 计算缩放比例
const scale = 1 / dpr;
// 动态设置meta
const metaEl = doc.createElement(‘meta‘);
metaEl.setAttribute(‘name‘, ‘viewport‘);
metaEl.setAttribute(‘content‘, ‘width=device-width,initial-scale=‘ + scale + ‘, user-scalable=no‘);
对应图片而言,要想达到最清晰的显示状态则要使图片的位图像素与设备的物理像素对应,所以可以对图片做如下适配
[dpr=1] img {
width: 200rem;
background: ‘@1x.png‘;
}
[dpr=2] img {
width: 200rem;
background: ‘@2x.png‘;
}
此方案的原理就是利用meta来更过css像素(因为css像素是虚拟像素由计算机定义的,见上文),以此达到一个css像素对应一个物理像素的效果,1px == 1pt
rem高清适配
利用上文提供的rem移动端适配思路,加上现在的高清适配思路,就可以完成移动端高清适配啦
直接贴代码,来自前端:『REM』手机屏幕高清适配方案
(function(designWidth, rem2px) {
var win = window;
var doc = win.document;
var docEl = doc.documentElement;
var metaEl = doc.querySelector(‘meta[name="viewport"]‘);
var dpr = 0;
var scale = 0;
var tid;
if (!dpr && !scale) {
var devicePixelRatio = win.devicePixelRatio;
if (win.navigator.appVersion.match(/iphone/gi)) {
if (devicePixelRatio >= 3 && (!dpr || dpr >= 3)) {
dpr = 3;
} else if (devicePixelRatio >= 2 && (!dpr || dpr >= 2)){
dpr = 2;
} else {
dpr = 1;
}
} else {
dpr = 1;
}
scale = 1 / dpr;
}
docEl.setAttribute(‘data-dpr‘, dpr);
if (!metaEl) {
metaEl = doc.createElement(‘meta‘);
metaEl.setAttribute(‘name‘, ‘viewport‘);
metaEl.setAttribute(‘content‘, ‘width=device-width,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);
}
} else {
metaEl.setAttribute(‘name‘, ‘viewport‘);
metaEl.setAttribute(‘content‘, ‘width=device-width,initial-scale=‘ + scale + ‘, maximum-scale=‘ + scale + ‘, minimum-scale=‘ + scale + ‘, user-scalable=no‘);
}
// 以上代码是对 dpr 和 viewport 的处理,代码来自 lib-flexible。
// 一下代码是处理 rem,来自上篇文章。不同的是获取屏幕宽度使用的是
// document.documentElement.getBoundingClientRect
// 也是来自 lib-flexible ,tb的技术还是很强啊。
function refreshRem(_designWidth, _rem2px){
// 修改viewport后,对网页宽度的影响,会立刻反应到
// document.documentElement.getBoundingClientRect().width
// 而这个改变反应到 window.innerWidth ,需要等较长的时间
// 相应的对高度的反应,
// document.documentElement.getBoundingClientRect().height
// 要稍微慢点,没有准确的数据,应该会受到机器的影响。
var width = docEl.getBoundingClientRect().width;
var d = window.document.createElement(‘div‘);
d.style.width = ‘1rem‘;
d.style.display = "none";
docEl.firstElementChild.appendChild(d);
var defaultFontSize = parseFloat(window.getComputedStyle(d, null).getPropertyValue(‘width‘));
// d.remove();
var portrait = "@media screen and (width: "+ width +"px) {html{font-size:"+ ((width/(_designWidth/_rem2px)/defaultFontSize)*100) +"%;}}";
var dpStyleEl = doc.getElementById(‘dpAdapt‘);
if(!dpStyleEl) {
dpStyleEl = document.createElement(‘style‘);
dpStyleEl.id = ‘dpAdapt‘;
dpStyleEl.innerHTML = portrait;
docEl.firstElementChild.appendChild(dpStyleEl);
} else {
dpStyleEl.innerHTML = portrait;
}
// 由于 height 的响应速度比较慢,所以在加个延时处理横屏的情况。
setTimeout(function(){
var height = docEl.getBoundingClientRect().height;
var landscape = "@media screen and (width: "+ height +"px) {html{font-size:"+ ((height/(_designWidth/_rem2px)/defaultFontSize)*100) +"%;}}"
var dlStyleEl = doc.getElementById(‘dlAdapt‘);
if(!dlStyleEl) {
dlStyleEl = document.createElement(‘style‘);
dlStyleEl.id = ‘dlAdapt‘
dlStyleEl.innerHTML = landscape;
docEl.firstElementChild.appendChild(dlStyleEl);
} else {
dlStyleEl.innerHTML = landscape;
}
},500);
}
// 延时,让浏览器处理完viewport造成的影响,然后再计算root font-size。
setTimeout(function(){
refreshRem(designWidth, rem2px);
}, 1);
})(750, 100);
代码比较多,有兴趣的可以直接上github上找到源代码(https://github.com/hbxeagle/rem/blob/master/HD_ADAPTER.md)
后记
这是一篇很早之前写的总结了,今天又复习修改了一下,写的有错误或者写的不清楚的地方请大家多多指正。
这么多年过去,其实现在已经逐渐流行直接使用 vw vh 来做移动端适配了,因为随着设备的更新兼容性的问题已经大大减少。但使用 rem 模式还是有一定需求的,毕竟vw还没有全部兼容,可以参考vw兼容性。还有就是有pc浏览器打开并限制最大宽度的需求使用vw就不可以了。
后面有时间将写写利用 vw vh
来进行移动端适配的总结,会比这个简单。
参考
欢迎到前端学习打卡群一起学习~516913974