本文介绍移动端的自适应、flexible.js的原理和发丝线的实现
首先我们需要了解相关概念
基本概念
像素
不同场景下像素的含义不同。
设备像素和图像像素
显示设备(显示器)是通过排列的显示器件来显示图像的,一个这样的显示器件称为一个“设备像素”。例如iPhone6横向375个像素,纵向667个像素。每个像素可以独立设置颜色。
图像像素指一个图像(位图)的最小展示单元,每个像素只能有一种颜色 。
物理像素和逻辑像素
物理像素指的就是设备像素,一个设备的物理像素是它的固有属性,显示设备还会提供逻辑像素给应用程序使用,例如我们平时设置显示器的分辨率为1280 × 800或者1440 × 900,就是设置显示器横向和竖向展示逻的辑像素的个数。
我们在CSS中使用的px单位就是逻辑像素(页面缩放的话,CSS的px尺寸和逻辑像素会成一定比例)。
为什么需要逻辑像素呢?因为物理像素因设备而异,不同像素排列方式和密度不同,如果程序设置一个展示元素的尺寸使用物理像素,那么它在不同的设备的尺寸是有很大区别的。因此显示器提供逻辑像素,让程序可以定义“希望实际看到的元素尺寸”。例如,设备的分辨率是375 × 667,逻辑像素1个像素对应物理1个像素;如果设备分辨率是750 × 1334,那么这个屏设置1个逻辑像素对应4个物理像素,这样显示的元素在两个屏上看起来基本一样大了。
一个逻辑像素里可能包含1个或者多个物理像素点,包含的越多则图像看起来越清晰。
设备像素比
设备独立像素指独立于设备的像素,即设备无关的像素,即逻辑像素,设备像素比指
物理像素 / 设备独立像素
设备像素比越大,说明一个逻辑像素对应的物理像素越多,图像就会更清晰。
例如,有些移动设备的设备像素比是2,即2个物理像素的宽度等于一个逻辑像素宽度,这种设备叫“2倍屏”,也有“3倍屏”。
设备的设备像素比可以通过window.devicePixelRatio
来获取。
视网膜屏(retina屏)
所谓“Retina”是一种显示技术,可以将更多的像素点压缩至一块屏幕里,从而达到更高的分辨率并提高屏幕显示的细腻程度。这种分辨率在正常观看距离下足以使人肉眼无法分辨其中的单独像素,也被称为视网膜显示屏。
Retina 既不是指分辨率,也不是单独指PPI,而是指视觉效果。其计算公式为(可以不用了解):
a = acttan( h / 2d )
a 代表人眼视角,h 代表像素间距,d 代表肉眼与屏幕的距离。符合以上条件的屏幕可以使肉眼看不见单个物理像素点。这样的显示屏就可被苹果称作“Retina显示屏”。
简单地说,视网膜屏就是设备的分辨率很高的屏。
meta viewport
name为"viewport"的meta标签,可以设置viewport相关的属性。下面说明一下viewport的概念。
关于viewport,可以参考viewport深入理解,在这只简单介绍一下。
viewport(视口),指移动端设备的可视区域,这包含了两个方面,一个是我们可以在手机上看到的网页的大小(layout viewport),这个可以通过document.documentElement.clientWidth
获取到。另一个是可视区的大小(visual viewport),可以通过window.outerWidth
获取到。
手机visual viewport的大小各不相同(注意,这里的大小指的都是逻辑像素),通常手机默认的layout viewport的大小是980px
或者1024px
(也可能是其它值,这个是由设备自己决定的)。
手机默认的layout一般都和PC近似,而大于手机的可视区。这是为了让移动端显示PC的网页,PC的网页通常较大,超出移动端可视的范围就会产生滚动条。
如果我们设计专门的移动端的网页,不希望很大的layout viewport的尺寸,而是和移动端适配,不产生滚动条。那么可以设置name为"viewport"的meta标签。
width
name为"viewport"的meta标签支持layout viewport的尺寸设置。
这样设置的结果是,layout viewport的宽度等于设备的视口宽度。通常移动端的网页都要这样设置。
scale
viewport meta标签也支持设置缩放比例,效果和用户手动放缩一样,例如缩放比例是2,那么CSS的1px对应逻辑像素就变为2px。
viewport meta标签的scale控制页面的缩放,缩放之后,CSS的1px代表的逻辑像素有所变化,例如scale设置为0.5的话,CSS的1px会对应0.5px的逻辑像素。
viewport的scale相关有几个关键属性
- initial-scale 设置页面的初始缩放值,为一个数字,可以带小数
- minimum-scale 允许用户的最小缩放值,为一个数字,可以带小数
- maximum-scale 允许用户的最大缩放值,为一个数字,可以带小数
- height 设置layout viewport 的高度,这个属性对我们并不重要,很少使用
- user-scalable 是否允许用户进行缩放,值为"no"或"yes", no 代表不允许,yes代表允许
注意,如果设置scale小于1,那么屏幕会缩小,所以视口能够展示的逻辑像素增多,因此视口的尺寸会变大。例如对于一个视口宽度为327的设备,设置scale为0.5后
console.log(document.documentElement.clientWidth); // 654
移动端自适应
前端面试刷题网站:灵题库,收集大厂面试真题,相关知识点详细解析。
简述
移动端自适应的基本思路是,让我们对元素的尺寸定义是和设备尺寸成一定比例的,比如一个盒子在10px的屏幕上展示2px宽度,在20px的屏幕上展示4px,这样在不同的设备上就可以自适应地展示了。
如何实现上述效果呢?首先会想到使用百分比,但是百分比是相对于父元素的比例,如果使用百分比会让元素的展示不符合预期,例如父元素增大时候,子元素也一定跟随增大。因此百分比最好是相对于屏幕的宽度(通常不适用高度作为基准)。
如果有CSS的尺寸单位能够表达相对于屏幕的宽度的百分比,就可以很方便地实现自适应了。
总而言之,移动端自适应的实现原理是,使用一些CSS的长度单位,让我们写的CSS尺寸能够和屏幕宽度保持固定比例。 这个比例是多少?很明显,就是UI图上的元素尺寸 / UI图上的屏幕宽度。
自适应开发流程
通常我们开发页面时候,UI图会提供每个元素的尺寸。如何根据UI图进行自适应开发呢?
我们现在已知的信息包括:UI图的屏幕宽度(UI_screen_width
),UI图的元素的尺寸(UI_element_width
)。那么我们就可以计算出UI图的元素相对于屏幕宽度的百分比,有了这个百分比,再结合CSS的相对于屏幕宽度的比例的长度单位,就可以计算出这个元素的尺寸值了。对于不同的单位,计算方法也是不同的。
所以自适应开发流程就是先根据UI图计算元素的百分比,再根据百分比计算出元素在某个自适应单位下的实际值。
一般在实际项目中,会有一个算法计算从UI_element_width
到使用自适应单位的尺寸值,开发时候就按照UI图的数值来写,在打包构建时候转成自适应单位。
因此在开发自适应页面时候,我们需要:
- 计算方法
- 转换工具
下面介绍两个主流的自适应方案,和对应的计算方法&转换工具。
vm自适应
CSS中有提供viewport单位,vm和vh,100vm = 屏幕宽度,100vh = 屏幕高度。使用vm来实现自适应是很方便的。
下面我们看如何根据元素相对于屏幕尺寸的比例,计算元素尺寸在vm单位下的取值。
比如UI图使用750px的屏幕宽度,一个元素的宽度是75px,那么元素宽度就是10%的屏幕宽度,使用vm单位就是10vm。
计算公式是什么呢?因为不管是UI图,还是实际的开发,元素宽度和屏幕宽度比例一样,设元素的实际值是x(vm)则可以得到:
x(vm) / 100vm = UI_element_width / UI_screen_width
所以x = UI_element_width / UI_screen_width × 100
。
这就是使用vm自适应的计算方法。
通常使用CSS预处理器的话,就可以实现在打包时候对长度进行转换了。例如使用Sass:
//iPhone 6尺寸作为设计稿基准
$vm_base: 750;
@function vw($px) {
@return ($px / $vm_base) * 100vw;
}
.demo-dev {
width: vm(100);
}
rem自适应
**
在一段时间内,由于vm的兼容性不能满足项目需求,因此有一些替代方案,其中最流行的就是rem自适应方案。
rem也是CSS中的一个长度单位,1rem = 16px
。html元素的style.fontSize默认是16px,如果html元素的style.fontSize改为其他的值,则1rem也相应变化,例如修改document.documentElement.style.fontSize=75px
,则1rem = 75px
。
根据rem的这个特性,我们看下如何使用rem来实现自适应。
我们先设定,document.documentElement.style.fontSize
简写为html.fontSize
;屏幕宽度document.documentElement.getBoundingClientRect().width
简写为visual_layout_width
。
我们上面已经提到,自适应方案的本质就是让我们写的CSS的尺寸和屏幕宽度保持固定比例,根据rem的特性(1rem = html.fontSize
),只需要让html.fontSize
和屏幕宽度保持固定比例,我们使用rem来写CSS单位,就可以达到CSS尺寸和屏幕宽度保持固定比例的目的了。例如UI图屏幕宽度是750px,UI图元素尺寸是75px。我们只要设置html.fontSize = 1 / 10 × visual_layout_width
,然后元素的CSS尺寸是1rem,这样元素就会根据屏幕宽度自适应了。
其实html.fontSize
设置的和屏幕宽度成一定比例即可,具体比例是多少都无所谓,只要计算元素的rem尺寸时候使用相同比例计算就可以。
在flexible.js里面,这个比例是1 / 10,即
html.fontSize = 1 / 10 × visual_layout_width。
下面我们看使用rem实现自适应的计算方法,设html的font size和屏幕宽度比例为r,即:
r = html.fontSize / visual_layout_width
所以屏幕宽度的值是
visual_layout_width = html.fontSize / r
设元素实际值是x(rem),那么因为x和visual_layout_width
的比值,等于UI_element_width / UI_screen_width
。所以x(rem) = html.fontSize(px) / r × UI_element_width / UI_screen_width = 1(rem) / r × UI_element_width / UI_screen_width
,所以x = UI_element_width / UI_screen_width / r
。
综上,rem自适应方案实现流程为:
- 首先选定一个比例
r
, - 然后确定UI图的屏幕宽度
UI_screen_width
。 - 然后js动态修改
document.documentElement.style.fontSize = document.documentElement.getBoundingClientRect().width × r
。 - 通过公式
x = UI_element_width / UI_screen_width / r
根据UI图元素的尺寸计算实际CSS代码的rem值。
类似vm,rem方案也可以通过预编译器来实现转换工作
//iPhone 6尺寸作为设计稿基准
$rem_rate: 0.1;
$ui_screen_width: 750;
@function rem($px) {
@return ($px / $ui_screen_width / $rem_rate);
}
.demo-dev {
width: rem(75);
}
也可以使用px2rem-loader工具进行转换。
0.5px实现
参考文章
简介
在上面“物理像素和逻辑像素”部分介绍了,通常我们使用CSS设置的是逻辑像素,不能直接设置物理像素。有时候前端需要实现比较细的线,例如0.5px的逻辑像素(或者说1px的物理像素),这种效果成为“发丝线”。按照正常的思路,我们直接设置0.5px就可以,但是不同浏览器的处理不同,因此0.5px并不是在每个浏览器都能达到预期效果。
有几个方案都能实现0.5px的效果,下面简单介绍一下这些方案。
更详细的说明,请阅读 怎么画一条0.5px的边
**
方案
**
直接设置0.5px
**
.half-px {
height: 0.5px;
}
**
transform
**
.half-px {
height: 1px;
transform: scaleY(0.5);
transform-origin: 50% 100%;
}
**
线性渐变
**
.half-px {
height: 1px;
background: linear-gradient(0deg, #fff, #000);
}
**
box-shadow
.half-px {
height: 1px;
background: none;
box-shadow: 0 0.5px 0 #000;
}
**
svg
.half-px {
background: none;
height: 1px;
background: url("data:image/svg+xml;utf-8,");
}
**
viewport scale缩放
先设置viewport的缩放比例
设置完viewport的scale之后,通过上面“meta viewport”部分的说明,CSS的1px会对应0.5px的逻辑像素。因此直接设置元素的尺寸为1px即可实现0.5px的效果。
.half-px {
height: 1px;
}
为了实现一个物理像素的细线,通常scale设置成设备像素比的倒数:1 / window.devicePixelRatio
。
flexible.js
**
简述
flexible.js主要实现了两个功能:
- 基于rem的自适应
- 基于viewport scale的发丝线
我们在项目中引入flexible.js(通过script标签引入或者内联到html中都可以)之后,就可以按照上面介绍的rem自适应方案实现自适应效果了。
我们上面已经提到了基于rem的自适应方案,flexible.js固定document.documentElement.style.fontSize
和document.documentElement.getBoundingClientRect().width
的比例为1 / 10。flexible.js实现了根据屏幕宽度计算html的fontSize并动态设置。
我们使用flexible.js时候,只需要实现px到rem的转换即可:
$rem_rate: 0.1; // flexible.js固定的比例,这样设定是为了方便计算
$ui_screen_width: 750; // UI图的屏幕宽度尺寸
@function rem($px) {
@return ($px / $ui_screen_width / $rem_rate);
}
.demo-dev {
width: rem(75); // 1rem
}
另外,引入flexible.js之后,由于它动态改了viewport的scale,所以可以直接写1px来实现发丝线的效果。
对于rem自适应的功能,需要注意的是,在flexible.js仓库中作者有说明:
由于viewport单位得到众多浏览器的兼容,lib-flexible这个过渡方案已经可以放弃使用,不管是现在的版本还是以前的版本,都存有一定的问题。建议大家开始使用viewport来替代此方案。
viewport方案对代码侵入性小,不需要引入额外脚本,而且在主流浏览器中的兼容性越来越好,所以是更优的方案。
原理
**
源码
;(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.js实现rem自适应方案的一个主要操作是根据设备宽度设置了html的fontSize,比例固定为 1/ 10。
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;
}
设置好html的fontSize后,我们就可以根据UI图计算元素尺寸了。
发丝线
**
flexible.js设置了viewport的scale为设备像素比的倒数
scale = 1 / dpr;
这样加载了flexible.js之后,CSS的1px对应的是1个物理像素,因此就可以实现发丝线了。