参考文章:
- https://segmentfault.com/a/1190000008767416
- https://www.jianshu.com/p/2c33921d5a68
在不同尺寸的手机设备上,页面“相对性的达到合理的展示(自适应)”或者“保持统一效果的等比缩放(看起来差不多)”。
viewport是严格的等于浏览器的窗口。
visual viewport
可见视口 屏幕宽度
layout viewport
布局视口 DOM宽度
ideal viewport
理想适口:使布局视口就是可见视口
设备宽度(visual viewport
)与DOM宽度(layout viewport), scale
的关系为:
(visual viewport)= (layout viewport)* scale
获取屏幕宽度(visual viewport)
的尺寸:window. innerWidth/Height
。
获取DOM宽度(layout viewport)
的尺寸:document. documentElement. clientWidth/Height
。
设置理想视口: 把默认的layout viewport的宽度设为移动设备的屏幕宽度,得到理想视口(ideal viewport)
:
属性 | 属性值 | 描述 |
---|---|---|
width | 数值 / device-width | 视口宽度 |
height | 数值 / device-height | 视口高度 |
initial-scale | 0.0 ~ 10.0 | 设备宽度与视口大小之间的缩放比率 |
maximum-scale | 0.0 ~ 10.0 | 缩放最大值 |
minimum-scale | 0.0 ~ 10.0 | 缩放最小值 |
user-scalable | 布尔值 | 默认yes,为no时用户不能缩放网页 |
物理像素又被称为设备像素,他是显示设备中一个最微小的物理部件。每个像素可以根据操作系统设置自己的颜色和亮度。所谓的一倍屏、二倍屏(Retina)、三倍屏,指的是设备以多少物理像素来显示一个CSS像素,也就是说,多倍屏以更多更精细的物理像素点来显示一个CSS像素点,在普通屏幕下1个CSS像素对应1个物理像素,而在Retina屏幕下,1个CSS像素对应的却是4个物理像素。
CSS像素是一个抽像的单位,主要使用在浏览器上,用来精确度量Web页面上的内容。一般情况之下,CSS像素称为与设备无关的像素(device-independent pixel),简称DIPs。CSS像素就是写CSS时所用的像素。
设备像素比简称为dpr,其定义了物理像素和设备独立像素的对应关系。它的值可以按下面的公式计算得到:
设备像素比 = 物理像素 / 设备独立像素
在Retina屏的iphone上,devicePixelRatio的值为2,也就是说1个css像素相当于2个物理像素。通常所说的二倍屏(retina)的dpr是2, 三倍屏是3。
JavaScript: window.devicePixelRatio
。CSS: -webkit-device-pixel-ratio, -webkit-min-device-pixel-ratio, -webkit-max-device-pixel-ratio
。dip或dp,(device independent pixels,设备独立像素)与屏幕密度有关。dip可以用来辅助区分视网膜设备还是非视网膜设备。
屏幕像素密度是指一个设备表面上存在的像素数量,它通常以每英寸有多少像素来计算(PPI)。屏幕像素密度与屏幕尺寸和屏幕分辨率有关,在单一变化条件下,屏幕尺寸越小、分辨率越高,像素密度越大,反之越小。
屏幕密度 = 对角线分辨率/屏幕尺寸
屏幕尺寸、屏幕分辨率-->对角线分辨率/屏幕尺寸-->屏幕像素密度PPI
|
设备像素比dpr = 物理像素 / 设备独立像素dip(dp)
|
viewport: scale
|
CSS像素px
下面大致列下前端在实现适配上常采用的方式。
<meta name="viewport" content="width=width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
设置理想视口(页面不缩放,禁止用户缩放),使得DOM宽度(layout viewport)与屏幕宽度(visual viewport)一样大,DOM文档主宽度即为屏幕宽度。1个CSS像素(1px)由多少设备像素显示由具体设备而不同。
对于安卓,所有设备缩放设为1,对于IOS,根据dpr不同,设置其缩放为dpr倒数。设置页面缩放可以使得1个CSS像素(1px)由1个设备像素来显示,从而提高显示精度;因此,设置1/dpr的缩放视口,可以画出1px的边框。
不管页面中有没有设置viewport,若无,则设置,若有,则改写,设置其scale为1/dpr。
(function (doc, win) {
var docEl = win.document.documentElement;
var resizeEvt = 'orientationchange' in window ? 'orientationchange' : 'resize';
var metaEl = doc.querySelector('meta[name="viewport"]');
var dpr = 0;
var scale = 0;
// 对iOS设备进行dpr的判断,对于Android系列,始终认为其dpr为1
if (!dpr && !scale) {
var isAndroid = win.navigator.appVersion.match(/android/gi);
var isIPhone = win.navigator.appVersion.match(/[iphone|ipad]/gi);
var devicePixelRatio = win.devicePixelRatio;
if(isIPhone) {
dpr = devicePixelRatio;
} else {
drp = 1;
}
scale = 1 / dpr;
}
// 设置data-dpr和viewport
docEl.setAttribute('data-dpr', dpr);
// 动态改写meta:viewport标签
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');
document.documentElement.firstElementChild.appendChild(metaEl);
} else {
metaEl.setAttribute('content', 'width=device-width, initial-scale=' + scale + ', maximum-scale=' + scale + ', minimum-scale=' + scale + ', user-scalable=no');
}
})(document, window);
设置动态缩放视口后,在iPhone6上,缩放为0.5,即CSS像素2px最终显示效果为1px,而在scale=1的设备,CSS像素1px显示效果为1px,那么,为了达到显示效果一致,以px为单位的元素(比如字体大小),其样式应有适配不同dpr的版本,因此,在动态设置viewport: scale
的时候,同时在html根元素上加上data-dpr=[dpr]
,但是这种方式还是不够,如果dpr为2,3之外的其他数值,px就没办法适配到。因此我会选择都用rem为单位进行适配。
样式示例:
.p {
font-size: 14px;
[data-dpr="2"] & {
font-size: 14px * 2;
}
[data-dpr="3"] & {
font-size: 14px * 3;
}
}
为写样式方便,可以借助sass的mixin写代码片段:
// 适配dpr的字体大小
@mixin font-dpr($font-size){
font-size: $font-size;
[data-dpr="2"] & {
font-size: $font-size * 2;
}
[data-dpr="3"] & {
font-size: $font-size * 3;
}
}
@mixin px-dpr($property, $px) {
#{$property}: $px;
[data-dpr="2"] & {
#{$property}: $px * 2;
}
[data-dpr="3"] & {
#{$property}: $px * 3;
}
}
// 使用
@include font-dpr(14px);
@include px-dpr(width, 40px); @include px-dpr(height, 40px);
问题:viewport设为理想视口(scale=1),基本已经满足适配,为什么要动态设置viewport缩放?
原因:iPhone6为例,dpr为2,缩放设为0.5,则DOM宽度为750,缩放后显示刚好为屏幕宽度375,而总的CSS像素其实是750,与设备像素一致,这样1px的CSS像素,占用的物理像素也是1;而viewport设置缩放为1的理想视口情况下,DOM宽度为375,显示也刚好是屏幕宽度,然而1px的CSS像素,占用的物理像素是2。这样说来,这样设置可以实现1px的线条在二倍屏的显示。因为:CSS像素与设备像素的关系依赖于屏幕缩放。
验证:设备:iPhone6,
通过对比后发现,在scale=0.5时,1px的线比scale=1.0要细,即** 不同dpr的设备用不同伸缩比,可以实现在多倍屏设备上显示1px设备独立像素的线条 **,这也就解决了1px线条的显示问题。
定义:font size of the root element.
这个单位的定义和em类似,不同的是em是相对于父元素,而rem是相对于根元素。rem定义是根元素的font-size, 以rem为单位,其数值与px的关系,需相对于根元素< html>的font-size计算,比如,设置根元素font-size=16px, 则表示1rem=16px。
根据这个特点,可以根据设备宽度动态设置根元素的font-size,使得以rem为单位的元素在不同终端上以相对一致的视觉效果呈现。
选取一个设备宽度作为基准,设置其根元素大小,其他设备根据此比例计算其根元素大小。比如使得iPhone6根元素font-size=16px。
设 备 | 设备宽度 | 根元素font-size/px | 宽度/rem |
---|---|---|---|
iPhone5 | 320 | js计算所得 | – |
iPhone6 | 375 | 16 | 23.4375 |
i6 Plus | 414 | js计算所得 | – |
- | 360 | js计算所得 | – |
根元素fontSize公式:width/fontSize = baseWidth/baseFontSize
其中,baseWidth, baseFontSize
是选为基准的设备宽度及其根元素大小,width, fontSize
为所求设备的宽度及其根元素大小
(function (doc, win) {
var docEl = win.document.documentElement;
var resizeEvt = 'orientationchange' in window ? 'orientationchange' : 'resize';
// 设置根元素font-size
//当设备宽度为375(iPhone6)时,根元素font-size=16px;
var refreshRem = function () {
var clientWidth = win.innerWidth
|| doc.documentElement.clientWidth
|| doc.body.clientWidth;
console.log(clientWidth)
if (!clientWidth) return;
var fz;
var width = clientWidth;
fz = 16 * width / 375;
docEl.style.fontSize = fz + 'px';
};
if (!doc.addEventListener) return;
win.addEventListener(resizeEvt, refreshRem, false);
doc.addEventListener('DOMContentLoaded', refreshRem, false);
refreshRem();
})(document, window);
对于需要使用rem来适配不同屏幕的元素,使用rem来作为CSS单位,为了方便,可以借助sass写一个函数计算px转化为rem, 写样式时不必一直手动计算。
/*
* 此处 $base-font-size 具体数值根据设计图尺寸而定
* flexible中设置的标准是【fontSize=16px, when 屏幕宽度=375】,因此,按此标准进行计算,
* 若设计图为iPhone6(375*667)的二倍稿,则$base-font-size=32px
*
*/
@function px2rem($px, $base-font-size: 32px) {
@if (unitless($px)) {
@warn "Assuming #{$px} to be in pixels, attempting to convert it into pixels for you";
@return px2rem($px + 0px); // That may fail.
} @else if (unit($px) == rem) {
@return $px;
}
@return ($px / $base-font-size) * 1rem;
}
// 使用,eg:
font-size: px2rem(18px);
// 使用sass的混合宏
// 淘宝手淘的方案里,i6(375pt)屏幕宽度为10rem,即font-size=75px, scale=0.5 因设计图为二倍图,$base-font-size=75px
@mixin px2rem($property,$px-values,$baseline-px:16px,$support-for-ie:false){
//Conver the baseline into rems
$baseline-rem: $baseline-px / 1rem * 1;
//Print the first line in pixel values
@if $support-for-ie {
#{$property}: $px-values;
}
//if there is only one (numeric) value, return the property/value line for it.
@if type-of($px-values) == "number"{
#{$property}: $px-values / $baseline-rem;
}
@else {
//Create an empty list that we can dump values into
$rem-values:();
@each $value in $px-values{
// If the value is zero or not a number, return it
@if $value == 0 or type-of($value) != "number"{
$rem-values: append($rem-values, $value / $baseline-rem);
}
}
// Return the property and its list of converted values
#{$property}: $rem-values;
}
}
原理:rem是相对长度单位,rem方案中的样式设计为相对于根元素font-size计算值的倍数。根据 屏幕宽度 设置html标签的font-size,在布局时使用 rem 单位布局,达到自适应的目的,是 弹性布局 的一种实现方式。
实现过程: 首先获取文档根元素和设备dpr,设置 rem,在html文档加载和解析完成后调整body字体大小; 在页面缩放 / 回退 / 前进的时候, 获取元素的内部宽度 (不包括垂直滚动条,边框和外边距),重新调整 rem 大小。
实现方法: 用 css 处理器或 npm 包将页面 css 样式中的px自动转换成 rem。在整个 flexible 适配方案中,文本使用px作为单位,使用[data-dpr]属性来区分不同dpr下的文本字号。由于手机浏览器对字体显示最小是8px,因此对于小尺寸文字需要采用px为单位,防止通过 rem 转化后出现显示问题。手机淘宝 中的字体使用px为单位,腾讯新闻中的字体使用rem为单位。
(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中viewport的标签设置了scale时,将根据scale手动设置dpr
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) {
//当meta中flexible的标签存在时,据此设置dpr
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) {
//根据js获取到的devicePixelRatio设置dpr及scale,scale是dpr的倒数
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和3倍方案
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);
//文本字号不建议使用rem,flexible适配方案中,文本使用px作为单位,使用[data-dpr]属性来区分不同dpr下的文本字号
if (!metaEl) {
//添加meta标签,设置name为viewport,content根据scale设置缩放比(默认、最大、最小缩放比)
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(){
//更新rem值
var width = docEl.getBoundingClientRect().width;
if (width / dpr > 540) {
width = 540 * dpr;
}
var rem = width / 10; //1rem = viewWidth / 10
docEl.style.fontSize = rem + 'px';
flexible.rem = win.rem = rem;
}
//resize与pageshow延时300ms触发refreshRem(),使用防抖函数,防止事件被高频触发可能引起性能问题
win.addEventListener('resize', function() {
clearTimeout(tid);
tid = setTimeout(refreshRem, 300);
}, false);
win.addEventListener('pageshow', function(e) {
//当一条会话历史纪录被执行的时候触发事件,包括后退/前进按钮,同时会在onload页面触发后初始化页面时触发
if (e.persisted) {//表示网页是否来自缓存
clearTimeout(tid);
tid = setTimeout(refreshRem, 300);
}
}, false);
//在html文档加载和解析完成后设置body元素字体大小
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);
}
//浏览器有最小字体限制,css在pc上font-size是12px(移动端最小是8px), 也就是css像素是12,其DPR为1,在移动端dpr有可能为2和3,为了保证字体不变小,需要用12*dpr进行换算。
refreshRem();
//实现rem与px相互转换
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'] = {}));
优势:
不足:
不是纯css移动适配方案,需要引入js脚本 在头部内嵌一段 js脚本 监听分辨率的变化来动态改变根元素的字体大小,css样式和 js 代码有一定 耦合性,并且必须将改变font-size的代码放在 css 样式之前。
小数像素问题,浏览器渲染最小的单位是像素,元素根据屏幕宽度自适应,通过 rem 计算后可能会出现小数像素,浏览器会对这部分小数四舍五入,按照整数渲染。浏览器在渲染时所做的摄入处理只是应用在元素的尺寸渲染上,其真实占据的空间依旧是原始大小。也就是说如果一个元素尺寸是 0.625px,那么其渲染尺寸应该是 1px,空出的 0.375px 空间由其临近的元素填充;同样道理,如果一个元素尺寸是 0.375px,其渲染尺寸就应该是0,但是其会占据临近元素 0.375px 的空间。会导致:缩放到低于1px的元素时隐时现(解决办法:指定最小转换像素,对于比较小的像素,不转换为 rem 或 vw);两个同样宽度的元素因为各自周围的元素宽度不同,导致两元素相差1px;宽高相同的正方形,长宽不等了;border-radius: 50% 画的圆不圆。
Android 浏览器下 line-height 垂直居中偏离的问题。常用的垂直居中方式就是使用line-height,这种方法在Android设备下并不能完全居中。
cursor: pointer 元素点击背景变色的问题,对添加了cursor:pointer属性的元素,在移动端点击时,背景会高亮。为元素添加tag-highlight-color:transparent 属性可以隐藏背景高亮。
原理: 使用 百分比% 定义 宽度,高度 用px固定,根据可视区域实时尺寸进行调整,尽可能适应各种分辨率,通常使用max-width/min-width控制尺寸范围过大或者过小。下表是子元素不同属性设置百分比的依据
属性 | 设置参考 |
---|---|
height/width | 基于子元素的直接父元素,width相对于父元素的width,height相对于父元素的height |
top/bottom 和left/right | 相对于直接非static定位的父元素的height/width |
padding/margin | 不论是垂直方向或者是水平方向,都相对于直接父亲元素的width,与父元素的height无关。 |
border-radius | 相对于自身的宽度 |
优势:
不足:
如果屏幕尺度跨度太大,相对设计稿过大或者过小的屏幕不能正常显示,在大屏手机或横竖屏切换场景下可能会导致页面元素被拉伸变形,字体大小无法随屏幕大小发生变化。
设置盒模型的不同属性时,其百分比设置的参考元素不唯一,容易使布局问题变得复杂
原理: 视口是浏览器中用于呈现网页的区域,移动端的视口通常指的是 布局视口
使用 css 预处理器把设计稿尺寸转换为 vw 单位,包括 文本,布局高宽,间距 等,使得这些元素能够随视口大小自适应调整。以1080px设计稿为基准,转化的计算表示为
// 以1080px作为设计稿基准
$vw_base: 1080
@function vw($px) {
@return($px / 1080) * 100vw
}
优势:
不足:
rem:CSS的长度单位, 根元素字体大小的倍数,只有根元素字体大小有关; html 中的根元素即 html 元素。
大部分浏览器的默认字体大小都是16px,所以1rem = 16px;
rem适配原理:
在制作页面的时候,只考虑跟设计稿相同的屏幕尺寸即可,其他尺寸屏幕自动适配
//对屏幕大小划分了html不同的font-size
@media screen and (min-width: 320px) {html{font-size:50px;}}
@media screen and (min-width: 360px) {html{font-size:56.25px;}}
@media screen and (min-width: 375px) {html{font-size:58.59375px;}}
@media screen and (min-width: 400px) {html{font-size:62.5px;}}
@media screen and (min-width: 414px) {html{font-size:64.6875px;}}
@media screen and (min-width: 440px) {html{font-size:68.75px;}}
@media screen and (min-width: 480px) {html{font-size:75px;}}
@media screen and (min-width: 520px) {html{font-size:81.25px;}}
@media screen and (min-width: 560px) {html{font-size:87.5px;}}
@media screen and (min-width: 600px) {html{font-size:93.75px;}}
@media screen and (min-width: 640px) {html{font-size:100px;}}
@media screen and (min-width: 680px) {html{font-size:106.25px;}}
@media screen and (min-width: 720px) {html{font-size:112.5px;}}
@media screen and (min-width: 760px) {html{font-size:118.75px;}}
@media screen and (min-width: 800px) {html{font-size:125px;}}
@media screen and (min-width: 960px) {html{font-size:150px;}}
viewport, data-dpr, font-size
的完整js代码见此项目文件flexible.js
, 相关的CSS预处理片段见_mixins.scss