用方案
1、设置根 font-size:625%(或其它自定的值,但换算规则 1rem 不能小于 12px)
2、通过媒体查询分别设置每个屏幕的根 font-size
3、CSS 直接除以 2 再除以 100 即可换算为 rem
优:有一定适用性,换算也较为简单。
劣:有兼容性的坑,对不同手机适配不是非常精准;需要设置多个媒体查询来适应不同手机,单某款手机尺寸不在设置范围之内,会导致无法适配。
网易方案
1、拿到设计稿除以 100,得到宽度 rem 值
2、通过给 html 的 style 设置 font-size,把 1 里面得到的宽度 rem 值代入x document.documentElement.style.fontSize =document.documentElement.clientWidth / x + ‘px‘;
3、设计稿 px/100 即可换算为 rem
优:通过动态根 font-size 来做适配,基本无兼容性问题,适配较为精准,换算简便。
劣:无 viewport 缩放,且针对 iPhone 的 Retina 屏没有做适配,导致对一些手机的适配不是很到位。
手淘方案
1、拿到设计稿除以 10,得到 font-size 基准值
2、引入 flexible
3、不要设置 meta 的 viewport 缩放值
4、设计稿 px/ font-size 基准值,即可换算为 rem
优:通过动态根 font-size、viewpor、dpr 来做适配,无兼容性问题,适配精准。
劣:需要根据设计稿进行基准值换算,在不使用 sublime text 编辑器插件开发时,单位计算复杂。
接:https://www.jianshu.com/p/3b45aa981e77
下面是一些基础概念的讲解,帮助理解各种适配方案实现。
像素:
1、物理像素(设备像素)
屏幕的物理像素,又被称为设备像素,他是显示设备中一个最微小的物理部件。任何设备屏幕的物理像素出厂时就确定了,且固定不变的。
2、设备独立像素
设备独立像素也称为密度无关像素,可以认为是计算机坐标系统中的一个点,这个点代表一个可以由程序使用的虚拟像素(比如说CSS像素),然后由相关系统转换为物理像素。
3、设备像素比
设备像素比简称为dpr,其定义了物理像素和设备独立像素的对应关系
设备像素比 = 物理像素 / 设备独立像素 以iphone6为例: iphone6的设备宽和高为375pt * 667pt,可以理解为设备的独立像素,而其设备像素比为2.固有设备像素为750pt * 1334pt
通过:window.devicePixelRatio获得。
设备像素比是区别是否是高清屏的标准,dpr大于1时就为高清屏,一般情况下dpr为整数,但是android有些奇葩机型不为整数。
4、css像素
在CSS、JS中使用的一个长度单位。单位px
注:在pc端1物理像素等于1px,但是移动端1物理像素不一定等于1px,1物理像素与px的关系与以下因素有关。(有些视口概念,可以把下面视口看完了再来看)
1、屏幕布局视口大小(下面会讲到) 2、屏幕的分辨率(物理像素)
对于一块屏幕,其物理像素是确定的。视觉视口尺寸是继承的布局视口的,而视觉视口里宽度即是css的px数。故在一块屏上物理像素与px的关系就是物理像素与布局视口的px数的关系。
比如iphone6,期物理像素为750,如果没有设置布局视口时,viewport为980px 此时:1物理像素长度等于980/750px = 1.3067px的长度 由于像素都是点阵的,故1物理像素相当于1.3067px * 1.3067px方格。 当在meta中设置了如下配置时 <meta name="viewport" content="width=device-width"> 相当于把布局视口设置为设备的宽度(即上面讲到的设备独立像素), 对于iphone6就是375px。 此时1物理像素长度等于375/750px = 0.5px的长度,故1物理像素相当于0.5px * 0.5px的方格。
视口:
1、布局视口:
在html中一般在meta中的name为viewport字段就是控制的布局视口。布局视口一般都是浏览器厂商给的一个值。在手机互联网没有普及前,网络上绝大部分页面都是为电脑端浏览而做的,根本没有做移动端的适配。随着移动端的发展,在手机上看电脑端的页面已成为非常普及现象。而电脑端页面宽度较大,移动端宽度有限,要想看到整个网页,会有很长的滚动条,看起来非常麻烦。于是浏览器厂商为了让用户在小屏幕下网页也能够显示地很好,所以把布局视口设置的很大,一般在768px ~ 1024px 之间,最常用的宽度就是 980。这样用户就能看到绝大部分内容,并根据具体内容选择缩放。
故布局视口是看不见的,浏览器厂商设置的一个固定值,如980px,并将980px的内容缩放到手机屏内。
布局视口可以通过:
document.documentElement.clientWidth(clientHeight) // 布局视口的尺寸。
2、视觉视口:
浏览器可视区域的大小,即用户看到的网页的区域。(其宽度继承的布局视口宽度)
window.innerWidth(innerHeight) // 视觉视口尺寸
3、理想视口:
布局视口虽然解决了移动端查看pc端网页的问题,但是完全忽略了手机本身的尺寸。所以苹果引入了理想视口,它对设备来说是最理想的布局视口,用户不需要对页面进行缩放就能完美的显示整个页面。最简单的做法就是使布局视口宽度改成屏幕的宽度。
可以通过window.screen.width获取。
<meta name="viewport" content="width=device-width">
移动端到底怎么适配不同的屏幕呢?最简单的方法是设置如下视口:
<meta name="viewport" content="width=device-width,initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no">
当使用以上方案定义布局视口时,即布局视口等于理想视口(屏幕宽度),屏幕没有滚动条,不存在高清屏下,字体较小的问题。但是在不同屏幕上,其视觉宽度是不同的,不能简单的将所有的尺寸都设置为px,可能会出现滚动条。小尺寸的可以用px,大尺寸的只能用百分比和弹性布局。
viewport缩放
对于上面的设置,再不同的屏幕上,css像素对应的物理像素具数是不一致的。
在普通屏幕下,dpr=1时,
1个css像素长度对应1个物理像素长度,1个css像素对应1个物理像素。
而在Retina屏幕下,如果dpr=2,
1个css像素长度对应2个物理像素长度,1css像素对应4个物理像素。
此时如果css中写
border: 1px solid red; // 此时1px 对应的宽度是2物理像素的宽度。
而一般现在移动端设计稿都是基于iphone设计的,稿子一般为750px或640px,这正好是iphone6和iphone5的物理像素。在设计稿中,一般有些边框效果,这时边框的线宽为1px,对应的就是1物理像素。而对于iphone5和iphone6,当width=device-width时,css的1px显示出来的是2个物理像素,所以看起来线就比较粗。怎么解决呢?1px边框效果其实有很多hack方法,其中一种就是通过缩放viewport。
initial-scale是将布局视口进行缩放,initial-scale是相对于理想视口的,即initial-scale=1与width=device-width是一样的效果。initial-scale=0.5等效于width= 2倍的device-width,所以设置initial-scale和width都可以改变布局视口的大小。
<meta name="viewport" content="width=device-width,initial-scale=0.5, maximum-scale=0.5, minimum-scale=0.5, user-scalable=no">
对于iphone6当添加如上设置后,initial-scale=0.5,即将页面缩小2倍后等于屏幕宽度。
布局视口width: width / 2 = 375px; width = 750px;
所以此时布局视口为750px,此时1px等于1物理像素。
适配方案:
上面讲了一些基础概念,下面讲具体适配。
对于ui设计师给的一张设计稿,怎么将其还原到页面上?对于不同手机屏幕,其dpr不同,屏幕尺寸也不同,考虑到各种情况,有很多适配方案,所以不同的适配方案,实现方法不同,处理复杂度也不同,还原程度也不同。
方案一:
固定高度,宽度自适应。
这种方案是目前使用较多的方案,也是相对较简单的实现方案:
该方法使用了理想视口:
<meta name="viewport" content="width=device-width,initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no">
垂直方向使用固定的值,水平方向使用弹性布局,元素采用定值、百分比、flex布局等。这种方案相对简单,还原度也非常低。
方案二:
固定布局视口宽度,使用viewport进行缩放
如:荔枝FM、网易应用
荔枝的代码:
if(/Android (\d+\.\d+)/.test(navigator.userAgent)){ var version = parseFloat(RegExp.$1); if(version>2.3){ var phoneScale = parseInt(window.screen.width)/640; if(/MZ-M571C/.test(navigator.userAgent)){ document.write('<meta name="viewport" content="width=640, minimum-scale = 0.5, maximum-scale= 0.5">'); }else if(/M571C/.test(navigator.userAgent)&&/LizhiFM/.test(navigator.userAgent)){ document.write('<meta name="viewport" content="width=640, minimum-scale = 0.5, maximum-scale= 0.5">'); }else{ document.write('<meta name="viewport" content="width=640, minimum-scale = '+ phoneScale +', maximum-scale = '+ phoneScale +', target-densitydpi=device-dpi">'); } }else{ document.write('<meta name="viewport" content="width=640, target-densitydpi=device-dpi">'); } }else{ document.write('<meta name="viewport" content="width=640, user-scalable=no, target-densitydpi=device-dpi">'); }
网易应用:
var win = window, width = 640, iw = win.innerWidth || width, ow = win.outerHeight || iw, sw = win.screen.width || iw, saw = win.screen.availWidth || iw, ih = win.innerHeight || width, oh = win.outerHeight || ih, ish = win.screen.height || ih, sah = win.screen.availHeight || ih, w = Math.min(iw, ow, sw, saw, ih, oh, ish, sah), ratio = w / width, dpr = win.devicePixelRatio; if (ratio = Math.min(ratio, dpr), 1 > ratio) { var ctt = ",initial-scale=" + ratio + ",maximum-scale=" + ratio, metas = document.getElementsByTagName("meta");ctt += ""; for (var i = 0, meta; i < metas.length; i++) meta = metas[i], "viewport" == meta.name && (meta.content += ctt) }
固定布局视口,宽度设置固定的值,总宽度为640px,根据屏幕宽度动态生成viewport。(设计稿应该是640px的)
<meta name="viewport" content="width=640, minimum-scale = 0.5625, maximum-scale = 0.5625, target-densitydpi=device-dpi">
这种方式布局如荔枝FM的网页宽度始终为640px。缩放比例scale为:
var scale = window.screen.width / 640
设计稿为640px时,正好可以1:1以px来写样式。但是1px所对应的物理像素就不一定是1了。
(window.screen.width * dpr) / 640 // 1px对应的物理像素
iphone5.png
iphone6.png
方案三:
根据不同屏幕动态写入font-size,以rem作为宽度单位,固定布局视口。
如网易新闻:
<meta name="viewport" content="width=device-width,initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no">
以640px设计稿和750px的视觉稿,网易这样处理的:
var width = document.documentElement.clientWidth; // 屏幕的布局视口宽度 var rem = width / 7.5; // 750px设计稿将布局视口分为7.5份 var rem = width / 6.4; // 640px设计稿将布局视口分为6.4份
这样不管是750px设计稿还是640px设计稿,1rem 等于设计稿上的100px。故px转换rem时:
rem = px * 0.01;
在750px设计稿上:
75px 对应 0.75rem, 距离占设计稿的10%; 在ipone6上: width = document.documentElement.clientWidth = 375px; rem = 375px / 7.5 = 50px; 0.75rem = 37.5px; (37.5/375=10%;占屏幕10%) 在ipone5上: width = document.documentElement.clientWidth = 320px; rem = 320px / 7.5 = 42.667px; 0.75rem = 32px; (32/320=10%;占屏幕10%)
故对于设计稿上任何一个尺寸换成rem后,在任何屏下对应的尺寸占屏幕宽度的百分比相同。故这种布局可以百分比还原设计图。
iphone5-2.png
iphone6-2.png
方案四:
以rem作为宽度单位,动态写入viewport和font-size进行缩放。
根据设置的dpr设置font-size。如:
document.documentElement.style.fontSize = 50 * dpr; // dpr 为设置的设备像素比。(注意不是设备自身的设备像素比,而是认为设置的dpr)
这种情况下,dpr = 1时,1rem = 50px;
dpr = 2时, 1rem = 100px;
当设计以iphone6为标准,出750px的设计稿时,此时dpr=2,故1rem 等于100px,将图上的尺寸转换为rem非常方便,除以100就行。代码如下:
var scale = 1.0; var dpr = 1; var isAndroid = window.navigator.appVersion.match(/android/gi); var isIPhone = window.navigator.appVersion.match(/iphone/gi); var devicePixelRatio = window.devicePixelRatio; // 此处只简单对ios做了伸缩处理,安卓没有做伸缩处理,统一dpr = 1 if ( isIPhone ) { scale /= devicePixelRatio; dpr *= devicePixelRatio; } var viewport = document.getElementById('viewport'); var content = 'initial-scale=' + scale + ', maximum-scale=' + scale + ',minimum-scale=' + scale + ', width=device-width, user-scalable=no'; viewport.setAttribute( 'content', content ); document.documentElement.style.fontSize = 50 * dpr + 'px'; document.documentElement.setAttribute('data-dpr', dpr);
对于该方案,
假设肉眼看到的宽度(视觉宽度):visualWidth,令dpr=1时,其1rem对应的宽度为50. dpr = 1 时, 1rem = 50px, initial-scale=1, 缩放为1。 visualWidth = 50 * 1 = 50; dpr = 2 时, 1rem = 100px, initial-scale=0.5, 缩放为0.5。 visualWidth = 100 * 0.5 = 50; dpr = 3 时, 1rem = 150px, initial-scale=0.3333, 缩放为0.3333。 visualWidth = 150 * 0.3333 = 50;
所以该方案,1rem在所有屏幕上对应的肉眼距离相同,故不同屏幕下,总的rem数不同,大屏下总的rem数大于小屏下,如iphone6下,总宽度为7.5rem,iphone5下,总宽度为6.4rem。故此方案不能百分比还原设计稿,故写样式时,对于大块元素应该用百分比,flex等布局,不能直接用rem。
关于这个方案的详细教程请参考这篇文章传送门
iphone5-3.png
iphone6-3.png
方案五:
根据不同屏幕动态写入font-size和viewport,以rem作为宽度单位
将屏幕分为固定的块数10:
var width = document.documentElement.clientWidth; // 屏幕的布局视口宽度 var rem = width / 10; // 将布局视口分为10份
这样在任何屏幕下,总长度都为10rem。1rem对应的值也不固定,与屏幕的布局视口宽度有关。
对于动态生成viewport,他们原理差不多,根据dpr来设置缩放。看看淘宝的:
var devicePixelRatio = window.devicePixelRatio; var isIPhone = window.navigator.appVersion.match(/iphone/gi); var dpr,scale; if (isIPhone) { if (devicePixelRatio >=3) { dpr = 3; } else if (devicePixelRatio >=2) { dpr = 2; } else { dpr = 1; } } else { dpr = 1; } scale = 1 / dpr;
淘宝只对iphone做了缩放处理,对于android所有dpr=1,scale=1即没有缩放处理。
此方案与方案三相似,只是做了viewport缩放,能百分比还原设计稿。
iphone5-4.png
iphone6-4.png
适配中要解决的问题 :
移动端适配最主要的是使在不同屏幕下不用缩放页面就能正常显示整个页面。以上方案都完成了这一需求。其次有几个需求:
1、解决高清屏下1px的问题,其实有很多hack方法,这里只讲了缩放视口。先将布局视口设置为高清屏的物理像素。这样css中1px就是1个物理像素,这样看到的线条才是真正的1px。但是此时视口宽度大于设备的宽度,就会出现滚动条。故对视口进行缩放,使视口宽度缩放到设备宽度。
淘宝团队在处理安卓端的缩放存在很多问题,所以dpr都做1处理,所以安卓端就没有解决1px的问题。
2、在大屏手机中一行看到的段落文字应该比小屏手机的多。
由于淘宝和网易新闻rem都是百分比,故如果用rem一行显示的文字个数应该是相同的。故对于段落文本不能用rem作为单位,应该用px处理,对于不同的dpr下设置不同的字体。
.selector { color: red; font-size: 14px; } [data-dpr="2"] .selector { font-size: 28px; // 14 * 2 } [data-dpr="3"] .selector { font-size: 42px; // 14 * 3 }
对于方案四,不管什么情况下,1rem对应的视觉上的宽度都是一样的,而对应的大屏、小屏手机其视觉宽度当然不同,故字体设置为rem单位时,也能满足大屏手机一行显示的字体较多这个需求。
五种方案对比:
上面四种方案对设计稿还原程度是有差别的。
除了方案一和方案四以外,其他方案都是百分比还原设计稿,大屏下元素的尺寸就大。
方案一还原设计稿程度较低,这里不做说明。
方案二做了百分比适配,部分1px适配,没有字体适配。
方案三做了百分比适配,没有1px适配,有字体大小适配。
方案四没有做百分百适配,布局要用百分百和flex布局,做了1px的适配,并且对于段落文字直接可以用rem做单位,不需要做适配。
方案五做了百分比适配,有1px适配,有字体大小适配。
项目中遇到的问题:
在我们项目中方案四和方案五都用过。
方案五在使用中没有遇到什么问题,就是刚开始没有做字体适配都是用的rem,后面加入了字体适配,这种方案设计师相对轻松些,不用考虑在大小屏幕下的布局效果。
方案四时没有跟ui设计师沟通清楚,导致设计师在设计图上一行排了很多交互元素,在小屏下放不下去,又不能简单放百分比(元素里的文字放不下)。所以还是要做动态判断大小屏,做出相应适配。这个方案可能设计师需要考虑的多些,尽量减少一行内的交互元素,当一行交互元素多时要考虑小屏手机怎么适配。
其实对于1px的适配在苹果端很好,在android端各个厂商手机差别太大,适配有很多问题。这是为什么绝大多数方案里都放弃了android端1px适配。不过最近看到很多网站都用了densitydpi=device-dpi这个安卓的私有属性来兼容部分安卓机型,这个属性在新的webkit已经被移除了,使用它主要为了兼容低版本的android系统。
这里大漠老师针对flexible方案进行了改版,兼容了更多的android机型的1px效果。文章传送门
他给了个压缩版的方案,我看了下源码,把它写了一遍,不知道有没有问题,效果是一样的。
var dpr, scale, timer, rem; var style = document.createElement('style'); dpr = window.devicePixelRatio || 1; scale = 1 / dpr; document.documentElement.setAttribute('data-dpr', dpr); var metaEl = document.createElement('meta'); metaEl.setAttribute('name', 'viewport'); metaEl.setAttribute('content', 'target-densitydpi=device-dpi, initial-scale=' + scale + ', maximum-scale=' + scale + ', minimum-scale=' + scale + ', user-scalable=no'); document.documentElement.firstElementChild.appendChild(metaEl); document.documentElement.firstElementChild.appendChild(style); if (980 === document.documentElement.clientWidth) { metaEl.setAttribute('content', 'target-densitydpi=device-dpi,width=device-width,user-scalable=no,initial-scale=1,maximum-scale=1,minimum-scale=1'); } function refreshRem () { var c = '}'; var width = document.documentElement.clientWidth; var isPhone = window.navigator.userAgent.match(/Android|BlackBerry|iPhone|iPad|iPod|Opera Mini|IEMobile/i); if (!isPhone && width > 1024) { width = 640; c = 'max-width:' + width + 'px;margin-right:auto!important;margin-left:auto!important;}'; } window.rem = rem = width / 16; style.innerHTML = 'html{font-size:' + rem + 'px!important;}body{font-size:' + parseInt(12 * (width / 320)) + 'px;' + c;; } refreshRem(); window.addEventListener('resize', function () { clearTimeout(timer); timer = setTimeout(refreshRem, 300); }, false); window.addEventListener('pageshow', function (e) { if (e.persisted) { clearTimeout(timer); timer = setTimeout(refreshRem, 300); } }, false);
这些方案只是针对绝大部分机型,项目中可能有些特殊机型有特殊问题,需要特殊对待。比如在这篇文章中作者使用flexible在小米max和荣耀8中有问题,需要特殊hack。传送门,我没有这种手机,也没有对此做验证。
对于上面的五种方案,方案五看似是适配最好的,但是当项目中引入第三方插件时可能要一一适配,比如:引入一个富文本,里面设置字体大小的一般都是px,你需要将其一一转换成rem。而对于方案二,可以直接用px做单位来百分百还原设计稿,引入的插件时也不用适配。所以说,具体项目中用哪个方案,其实不光是前端的选择,还要跟设计师讨论下,满足设计需求,选择最适合项目的方案。
移动端虽然整体来说大部分浏览器内核都是 webkit,而且大部分都支持 css3 的所有语法。但是,由于手机屏幕尺寸不一样,分辨率不一样,或者你需要考虑横竖屏的问题,或者考虑到各式各样的移动端兼容性问题。这时候你也就不得不解决在不同手机上,不同情况下的展示效果,所以就需要一个开箱即用并且行之有效的移动端适配方案。
工欲善其事必先利其器,在具体介绍适配方案前,在本章我们会学习下适配相关的知识点,便于后续适配方案的直接上手接收。
像素单位有设备像素、逻辑像素、CSS 像素 3 种。
设备像素(device pixels)也叫物理像素,指的是显示器上的真实像素,每个像素的大小是屏幕固有的属性,屏幕出厂以后就不会再改变。
设备分辨率描述的就是这个显示器的宽和高分别是多少个设备像素,例如常见的显示器的分辨率为 1920 * 1080。
设备像素和设备分辨率是由操作系统来管理的,浏览器不知道、也不必知道设备分辨率的大小,它主要根据逻辑分别率(下一小节介绍)来计算的。
设备独立像素(device independent pixels)是操作系统定义的一种像素单位,应用程序将设备独立像素告诉操作系统,操作系统再将设备独立像素转化为设备像素,从而控制屏幕上真正的物理像素点。
为什么需要在应用程序与设备像素之间定义这么一种单位呢?为什么应用程序不应该直接使用设备像素?
例如原先在 1280×720 设备分辨率的显示屏中,显示高度为 12 个设备像素的字体,现在放到设备分辨率为 2560 ×1440 的显示屏中,如果要想得到原先的大小,则需要 24 个设备像素,如果应用程序直接使用设备像素,那么编写应用程序则将变得非常困难,需要编写应用程序逻辑:字体在一些屏幕上高度为 12 个设备像素,在另一些屏幕上却需要 24 个设备像素。
因此操作系统定义了一个单位:设备独立像素。操作系统保证:用设备独立像素定义的尺寸,不管屏幕的参数如何,都能以合适的大小显示(这也是设备独立像素名字的由来)。操作系统是如何做到的呢?对于那些像素密度高的屏幕,将多个设备像素划分为一个逻辑像素。至于将多少设备像素划分为一个逻辑像素,这由操作系统决定。
对于上面的例子:“原本高度为 12 个设备像素的字体,现在高度为 24 个设备像素才能得到相同的大小”,操作系统会将一个逻辑像素定义为 2*2个 真实像素,从而设备独立像素尺寸不需要改变,而且不管在新、旧设备上,显示的尺寸大致相同。
设备独立像素与设备像素之间的比例是多少,显示器厂商和操作系统厂商会通过调查研究来得出最利于观看的比例。普遍规律是,屏幕的像素密度越高,就需要更多的设备像素来显示一个设备独立像素。
逻辑分辨率用屏幕的 宽高 来表示(单位:设备独立像素),我们通过操作系统的分辨率设置来改变设备独立像素的大小。例如屏幕的设备分辨率是19201200(单位:设备像素),我们可以在当前的分辨率下设置逻辑分辨率是1280*800(单位:设备独立像素)。那么横、纵方向的设备像素数量恰好是设备独立像素的1.5倍。这也意味着,设备独立像素的边长是设备像素边长的1.5倍。
在 CSS 中使用的 px 都是指 css 像素,比如 width: 128px。css 像素的大小是很容易变化的,当我们缩放页面的时候,元素的 css 像素数量不会改变,改变的只是每个 css 像素的大小。也就是说 width: 128px 的元素在缩放200% 以后,宽度依然是 128 个 css 像素,只不过每个 css 像素的宽度和高度变为原来的两倍。如果原本元素宽度为 128 个设备独立像素,那么缩放 200% 以后元素宽度为 256 个设备独立像素。
(1)css 像素与设备独立像素的关系
(2)css 像素与设备像素的关系
window.devicePixelRatio 设备像素比,devicePixelRatio = (在相同长度的直线上)设备像素的数量 / CSS 像素的数量。这个比例也等价于 CSS 像素边长/设备像素边长。如 devicePixelRatio = 2,表示在相同长度的直线上,设备像素的数量是 CSS 像素数量的 2 倍,因此 CSS 像素的边长是设备像素的 2 倍。缩放会导致 CSS 像素边长的改变,从而导致 window.devicePixelRatio 的改变!
viewport 表示浏览器的可视区域,也就是浏览器中用来显示网页的那部分区域。存在三种 viewport 分别为 layout viewport、visual viewport 以及 ideal viewport,我们接下来分别介绍三种。
layout viewport 为布局视口,即网页布局的区域,它是 html 元素的父容器,只要不在 css 中修改 元素的宽度, 元素的宽度就会撑满 layout viewport 的宽度。
很多时候浏览器窗口没有办法显示出 layout viewport 的全貌,但是它确实是已经被加载出来了,这个时候滚动条就出现了,你需要通过滚动条来浏览 layout viewport 其他的部分。
layout viewport 用 css 像素来衡量尺寸,在缩放、调整浏览器窗口的时候不会改变。缩放、调整浏览器窗口改变的只是 visual viewport。
在桌面浏览器中,缩放100% 的时候,Layout Viewport 宽度等于内容窗口的宽度。(你几乎不会在电脑上见过横向滚动条,除非你调整缩放)
但是在移动端,缩放为 100% 的时候,Layout Viewport 不一定等于内容窗口的大小。当你用手机浏览浏览宽大的网页(这些网页没有采用响应式设计)的时候,你只能一次浏览网页的一个部分,然后通过手指滑动浏览其他部分。这就说明整个网页(Layout Viewport)已经加载出来了,只不过你要一部分一部分地看。
visual viewport 为视觉视口,就是显示在屏幕上的网页区域,它往往只显示 layout viewport 的一部分。
visual viewport 就像一台摄像机,layout viewport 就像一张纸,摄像机对准纸的哪个部分,你就能看见哪个部分。你可以改变摄像机的拍摄区域大小(调整浏览器窗口大小),也可以调整摄像机的距离(调整缩放比例),这些方法都可以改变 visual viewport,但是 layout viewport 始终不变。
ideal viewport 为理想视口,不同的设备有自己不同的 ideal viewport,ideal viewport 的宽度等于移动设备的屏幕宽度,所以其是最适合移动设备的 viewport。只要在 css 中把某一元素的宽度设为 ideal viewport 的宽度(单位用 px ),那么这个元素的宽度就是设备屏幕的宽度了,也就是宽度为100% 的效果。 ideal viewport 的意义在于,无论在何种分辨率的屏幕下,那些针对ideal viewport 而设计的网站,不需要用户手动缩放,也不需要出现横向滚动条,都可以完美的呈现给用户。
移动设备默认的 viewport 是 layout viewport,也就是那个比屏幕要宽的 viewport,但在进行移动设备网站的开发时,我们需要的是 ideal viewport。那么怎么才能得到 ideal viewport 呢?
我们在开发 h5 页面时,最经常见的标签如下所示
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
该 meta 标签的作用是让当前 viewport 的宽度等于设备的宽度,同时不允许用户手动缩放。如果你不这样的设定的话,那就会使用那个比屏幕宽的默认 viewport(layout viewport),也就是说会出现横向滚动条。
相关的属性意义如下所示
width | 设置 layout viewport 的宽度,为一个正整数,或字符串 "width-device" |
height | 设置页面的初始缩放值,为一个数字,可以带小数 |
initial-scale | 允许用户的最小缩放值,为一个数字,可以带小数 |
minimum-scale | 允许用户的最大缩放值,为一个数字,可以带小数 |
maximum-scale | 设置 layout viewport 的高度,这个属性对我们并不重要,很少使用 |
user-scalable | 是否允许用户进行缩放,值为"no"或"yes", no 代表不允许,yes 代表允许 |
在前端滚滚潮流的历史发展中的不同时期分别出现了一些极具代表性的适配方案,以下分别进行简单介绍。
基于 css 的媒体查询属性 @media 分别为不同屏幕尺寸的移动设备编写不同尺寸的 css 属性,示例如下所示。虽然此方法能在一定程度上解决移动设备适配的问题,但我们也可以看出其存在以下问题,所以其已几乎被历史潮流淘汰。
@media only screen and (min-width: 375px) {
.logo {
width : 62.5px;
}
}
@media only screen and (min-width: 360px) {
.logo {
width : 60px;
}
}
@media only screen and (min-width: 320px) {
.logo {
width : 53.3333px;
}
}
rem(font size of the root element)是指相对于根元素的字体大小的单位,如果我们设置 html 的 font-size 为 16px,则如果需要设置元素字体大小为 16px,则写为 1rem。但是其还是必须得借助 @media 属性来为不同大小的设备设置不同的 font-size,相对上一种方案,可以减少重复编写相同属性的代价,简单示例如下所示。
我们也能看到该方案存在以下问题:
@media only screen and (min-width: 375px) {
html {
font-size : 375px;
}
}
@media only screen and (min-width: 360px) {
html {
font-size : 360px;
}
}
@media only screen and (min-width: 320px) {
html {
font-size : 320px;
}
}
//定义方法:calc
@function calc($val){
@return $val / 1080;
}
.logo{
width : calc(180rem);
}
在 rem 方案上进行改进,我们可以使用 js 动态来设置根字体,这种方案的典型代表就是 flexible 适配方案。
它的核心代码如下所示
// set 1rem = viewWidth / 10
function setRemUnit () {
var rem = docEl.clientWidth / 10
docEl.style.fontSize = rem + 'px'
}
setRemUnit();
上面的代码中,将 html 节点的 font-size 设置为页面 clientWidth(布局视口)的 1/10,即 1rem 就等于页面布局视口的 1/10,这就意味着我们后面使用的 rem 都是按照页面比例来计算的。
设置 viewport 的 width 为 device-width,改变浏览器 viewport(布局视口和视觉视口)的默认宽度为理想视口宽度,从而使得用户可以在理想视口内看到完整的布局视口的内容。
等比设置 viewport 的 initial-scale、maximum-scale、minimum-scale 的值,从而实现 1 物理像素=1 css像素,以适配高倍屏的显示效果(就是在这个地方规避了大家熟知的“1px 问题”)
var metaEL= doc.querySelector('meta[name="viewport"]');
var dpr = window.devicePixelRatio;
var scale = 1 / dpr
metaEl.setAttribute('content', 'width=device-width, initial-scale=' + scale + ', maximum-scale=' + scale + ', minimum-scale=' + scale + ', user-scalable=no');
不可否认 flexible 在兼容性不友好的某个时期还是极大帮助来成千上万的开发者,但是该方案自身是存在一些问题的。
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;
}
其实 flexible 方案是在 模拟 viewport 功能,只是随着浏览器的发展及兼容性增强,viewport 已经能兼容绝大部分主流浏览器,并且 flexible 方案自身存在的问题,所有其也已几乎退出历史潮流。引用 lib-flexible 的 github 主页的原话:
由于 viewport 单位得到众多浏览器的兼容,lib-flexible 这个过渡方案已经可以放弃使用,不管是现在的版本还是以前的版本,都存有一定的问题。建议大家开始使用 viewport 来替代此方案。
由于 viewport 单位得到众多浏览器的兼容,所以目前基于 viewport 的移动端适配方案被各大厂团队所采用。
vw 作为布局单位,从底层根本上解决了不同尺寸屏幕的适配问题,因为每个屏幕的百分比是固定的、可预测、可控制的。 viewport 相关概念如下:
假设我们拿到的视觉稿宽度为 750px,视觉稿中某个字体大小为 75px,则我们的 css 属性只要如下这么写,不需要额外的去用 js 进行设置,也不需要去缩放屏幕等;
.logo {
font-size: 10vw; // 1vw = 750px * 1% = 7.5px
}
在 html 头部设置 mata 标签如下所示,让当前 viewport 的宽度等于设备的宽度,同时不允许用户手动缩放。
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
设计师一般给宽度大小为 375px 或 750px 的视觉稿,我们采用 vw 方案的话,需要将对应的元素大小单位 px 转换为 vw 单位,这是一项影响开发效率(需要手动计算将 px 转换为 vw)且不利于后续代码维护(css 代码中一堆 vw 单位,不如 px 看的直观)的事情;好在社区提供了 postcss-px-to-viewport 插件,来将 px 自动转换为 vw,相关配置步骤如下:
(1) 安装插件
npm install postcss-px-to-viewport --save-dev
(2)webpack 配置
官网是使用 glup 进行配置,但是我们项目模版中是使用 webpack 进行 postcss 插件以及相关样式插件的配置,所以我们就使用 webpack 进行配置使用,不需要额外引入 gulp 编译;webpack 相关配置如下,且每个属性表示的意义进行了备注:
module.exports = {
plugins: {
// ...
'postcss-px-to-viewport': {
// options
unitToConvert: 'px', // 需要转换的单位,默认为"px"
viewportWidth: 750, // 设计稿的视窗宽度
unitPrecision: 5, // 单位转换后保留的精度
propList: ['*', '!font-size'], // 能转化为 vw 的属性列表
viewportUnit: 'vw', // 希望使用的视窗单位
fontViewportUnit: 'vw', // 字体使用的视窗单位
selectorBlackList: [], // 需要忽略的 CSS 选择器,不会转为视窗单位,使用原有的 px 等单位
minPixelValue: 1, // 设置最小的转换数值,如果为 1 的话,只有大于 1 的值会被转换
mediaQuery: false, // 媒体查询里的单位是否需要转换单位
replace: true, // 是否直接更换属性值,而不添加备用属性
exclude: undefined, // 忽略某些文件夹下的文件或特定文件,例如 'node_modules' 下的文件
include: /\/src\//, // 如果设置了include,那将只有匹配到的文件才会被转换
landscape: false, // 是否添加根据 landscapeWidth 生成的媒体查询条件
landscapeUnit: 'vw', // 横屏时使用的单位
landscapeWidth: 1125, // 横屏时使用的视窗宽度
},
},
};
相关配置属性,通过注释一目了然其作用,其中需要强调的点为 propList 属性,我们配置了 font-size 不进行转换 vw,也就是说在不同手机屏幕尺寸下的字体大小是一样的。 其中 font-size 是否需要根据屏幕大小做适配,或者怎么做,一直是个争论不休的话题;考虑到我们移动端没有平板的需求,且咨询过团队业务设计师的意见,所以对模版进行以上默认配置;当然如果你的视觉要求你的项目要做字体大小适配,修改 propList 属性的配置即可。
(3)效果展示 我们在项目代码中,进行如下 css 编码:
.hello {
color: #333;
font-size: 28px;
}
启动项目,我们可以看到浏览器渲染的页面中,postcss-px-to-viewport 已经帮我们做进行了 px -> vw 的转换;如下所示:
在项目中,如果设计师要求某一场景不做自适配,需为固定的宽高或大小,这时我们就需要利用 postcss-px-to-viewport 插件的 Ignoring 特性,对不需要转换的 css 属性进行标注,示例如下所示:
/* example input: */
.class {
/* px-to-viewport-ignore-next */
width: 10px;
padding: 10px;
height: 10px; /* px-to-viewport-ignore */
}
/* example output: */
.class {
width: 10px;
padding: 3.125vw;
height: 10px;
}
考虑 Retina 屏场景,可能对图片的高清程度、1px 等场景有需求,所以我们预留判断 Retina 屏坑位。 相关方案如下:在入口的 html 页面进行 dpr 判断,以及 data-dpr 的设置;然后在项目的 css 文件中就可以根据 data-dpr 的值根据不同的 dpr 写不同的样式类;
(1)index.html 文件
// index.html 文件
const dpr = devicePixelRatio >= 3? 3: devicePixelRatio >= 2? 2: 1;
document.documentElement.setAttribute('data-dpr', dpr);
(2)样式文件
[data-dpr="1"] .hello {
background-image: url(image@1x.jpg);
[data-dpr="2"] .hello {
background-image: url(image@2x.jpg);
}
[data-dpr="3"] .hello {
background-image: url(image@3x.jpg);
}
场景:当你需要写行内样式的代码(style)时,postcss-px-to-viewport 插件 无法进行 px 单位无法转换,需要自己手动计算好 vw;
最佳实践:通过添加、修改、删除 className 的方式进行处理此类场景,不直接操作行内样式,这更符合将 js 和 css 隔离开的更佳实践。
retina 屏下 1px 问题是个常谈的问题,相比较普通屏,retina 屏的 1px 线会显得比较粗,设计美感欠缺;在视觉设计师眼里的 1px 是指设备像素 1px,而如果我们直接写 css 的大小 1px,那在 dpr = 2 时,则等于 2px 设备像素,dpr = 3 时,等于 3px 设备像素。所以对于要求处理 1px 的场景,我们要进行特殊处理。
以下介绍常用的几种方法
可以使用 transform: scale(0.5) 进行 X、Y 轴的缩放,如下示例所示
.class1 {
height: 1px;
transform: scaleY(0.5);
}
优点是编写简单,但是如果实现上下左右四条边框会比较难搞,并且如果有嵌套存在的话,会对包含的元素产生影响,所以结合 :before 和 :after 来使用。
此种方式能解决例如 标签上下左右边框 1px 的场景,以及有嵌套元素存在的场景,比较通用,示例如下所示
.calss1 {
position: relative;
&::after {
content:"";
position: absolute;
bottom:0px;
left:0px;
right:0px;
border-top:1px solid #666;
transform: scaleY(0.5);
}
}
利用 css 对阴影处理来模拟边框,示例如下所示,底部一条线,缺点是存在阴影不好看。
.class1 {
box-shadow: 0 1px 1px -1px rgba(0, 0, 0, 0.5);
}
还有如下等方式处理 1px 问题,但不推荐,了解即可
图片高清的问题:
所以如果对性能、美观要求很高的场景,需要根据 dpr 区分使用对应的图片,我们在文章 viewport 适配方案中针对 retina 屏预留了 dpr 方案,相关 css 写法如下:
[data-dpr="1"] .hello {
background-image: url(image@1x.jpg);
[data-dpr="2"] .hello {
background-image: url(image@2x.jpg);
}
[data-dpr="3"] .hello {
background-image: url(image@3x.jpg);
}
iPhoneX 取消了物理按键,改成底部小黑条,这一改动导致网页出现了比较尴尬的屏幕适配问题。对于网页而言,顶部(刘海部位)的适配问题浏览器已经做了处理,所以我们只需要关注底部与小黑条的适配问题即可(即常见的吸底导航、返回顶部等各种相对底部 fixed 定位的元素)。 比如一些需要贴在底部的按钮,和呼起的tabBar和底部弹出框,在iphoneX上就会出现被小黑条遮挡内容,或者页面上出现白色空隙的问题。处理前后截图如下所示
安全区域指的是一个可视窗口范围,处于安全区域的内容不受圆角(corners)、齐刘海(sensor housing)、小黑条(Home Indicator)影响,如下图蓝色区域:
也就是说,我们要做好适配,必须保证页面可视、可操作区域是在安全区域内。 更详细说明,参考文档:Human Interface Guidelines - iPhoneX
iOS11 新增特性,苹果公司为了适配 iPhoneX 对现有 viewport meta 标签的一个扩展,用于设置网页在可视窗口的布局方式,可设置三个值。
需要注意:网页默认不添加扩展的表现是 viewport-fit=contain,需要适配 iPhoneX 必须设置 viewport-fit=cover,这是适配的关键步骤。更详细说明,参考文档:viewport-fit-descriptor
iOS11 新增特性,Webkit 的一个 CSS 函数,用于设定安全区域与边界的距离,有四个预定义的变量:
这里我们只需要关注 safe-area-inset-bottom 这个变量,因为它对应的就是小黑条的高度(横竖屏时值不一样)。
注意:当 viewport-fit=contain 时 env() 是不起作用的,必须要配合 viewport-fit=cover 使用。对于不支持 env() 的浏览器,浏览器将会忽略它。
需要注意的是之前使用的 constant() 在 iOS11.2 之后就不能使用的,但我们还是需要做向后兼容,像这样:
padding-bottom: constant(safe-area-inset-bottom); /* 兼容 iOS < 11.2 */
padding-bottom: env(safe-area-inset-bottom); /* 兼容 iOS >= 11.2 */
注意:env() 跟 constant() 需要同时存在,而且顺序不能换。 更详细说明,参考文档:Designing Websites for iPhone X
新增 viweport-fit 属性,使得页面内容完全覆盖整个窗口,前面也有提到过,只有设置了 viewport-fit=cover,才能使用 env()
<meta name="viewport" content="width=device-width, viewport-fit=cover">
可以通过加内边距 padding 扩展高度:
{
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
}
或者通过计算函数 calc 覆盖原来高度:
{
height: calc(60px(假设值) + constant(safe-area-inset-bottom));
height: calc(60px(假设值) + env(safe-area-inset-bottom));
}
注意,这个方案需要吸底条必须是有背景色的,因为扩展的部分背景是跟随外容器的,否则出现镂空情况。
还有一种方案就是,可以通过新增一个新的元素(空的颜色块,主要用于小黑条高度的占位),然后吸底元素可以不改变高度只需要调整位置,像这样:
{
margin-bottom: constant(safe-area-inset-bottom);
margin-bottom: env(safe-area-inset-bottom);
}
空的颜色块:
{
position: fixed;
bottom: 0;
width: 100%;
height: constant(safe-area-inset-bottom);
height: env(safe-area-inset-bottom);
background-color: #fff;
}
像这种只是位置需要对应向上调整,可以仅通过下外边距 margin-bottom 来处理
{
margin-bottom: constant(safe-area-inset-bottom);
margin-bottom: env(safe-area-inset-bottom);
}
或者,你也可以通过计算函数 calc 覆盖原来 bottom 值:
{
bottom: calc(50px(假设值) + constant(safe-area-inset-bottom));
bottom: calc(50px(假设值) + env(safe-area-inset-bottom));
}
Android 4.4 之下和 iOS 8 以下的版本有一定的兼容性问题(ps:几乎绝迹,大家可以统计下你们的用户使用的系统版本占比),但是社区提供了兼容性解决方案,其为 viewport 的 buggyfill:Viewport Units Buggyfill,可以访问其 github 官网查看。
我们也做了对应的实践,但是考虑到性能,我们项目模版中不会进行引入,有兴趣的同学可以查看以下实践总结;
viewport-units-buggyfill 主要有两个 JavaScript 文件:viewport-units-buggyfill.js 和 viewport-units-buggyfill.hacks.js。你只需要在你的 HTML 文件中引入这两个文件,比如在 react 项目中的 index.html 引入它们;
<script src="//g.alicdn.com/fdilab/lib3rd/viewport-units-buggyfill/0.6.2/??viewport-units-buggyfill.hacks.min.js,viewport-units-buggyfill.min.js"></script>
第二步,在HTML文件中调用 viewport-units-buggyfill,比如:
<script>
window.onload = function () {
window.viewportUnitsBuggyfill.init({
hacks: window.viewportUnitsBuggyfillHacks
});
}
</script>
但是为保证 Viewport Units Buggyfill 起作用,我们必须在我们样式文件中用到了viewport 的单位(vw、vh、vmin 或 vmax )地方添加 content,如下所示:
.my-viewport-units-using-thingie {
width: 50vmin;
height: 50vmax;
top: calc(50vh - 100px);
left: calc(50vw - 100px);
/* hack to engage viewport-units-buggyfill */
content: 'viewport-units-buggyfill; width: 50vmin; height: 50vmax; top: calc(50vh - 100px); left: calc(50vw - 100px);';
}
在 1 步骤中,我们人肉引入 content 属性,效率是非常低下的,好在社区提供了 postcss-viewport-units 插件,帮我们自动处理 content:
我们执行以下命令,进行 postcss-viewport-units 插件的安装:
tnpm i postcss-viewport-units --save-dev
在我们的项目配置文件 webpack.config.js 中进行对应的插件引入配置:
{
loader: 'postcss-loader',
options: {
ident: 'postcss',
plugins: () => [
// 我们加的配置
require('postcss-viewport-units'),
],
sourceMap: isProductionEnv,
},
},
我们在项目代码中,进行如下编码:
.hello {
color: #333;
font-size: 28px;
}
展示的页面中,postcss-viewport-units 已经帮我们添加了 content 属性;如下所示:
*请认真填写需求信息,我们会在24小时内与您取得联系。