通过这篇文章你将收获什么?
- 明晰移动端三种视口存在的意义。
- 对移动端适配的核心标签
<meta name="viewport">
中的width
与initial-scale
配置有一个清晰的认知,并且能够通过配置initial-scale
解决实际问题 - 对于
flexible.js
中设置initial-scale
的“迷惑行为”拥有清晰的理解——解决“1px问题” - 理解移动端高倍屏的图片模糊问题并解决。
meta标签中Initial-scale与width的含义
对移动端的三种视口简单概括:所谓布局视口,就是我们写的dom布局所在的空间,在这个空间上把页面布局渲染完成,然后通过缩放(底层自动代理)把布局好的页面在手机屏幕上一屏展示出来;视觉视口就是指手机屏幕内呈现的空间,即用户所看到的;理想视口可以理解为一个标准,一个参照,它以dip(设备独立像素)
来刻画大小,每种移动设备都有自己的dip
大小,类似于一个写死的软硬件属性(移动端页面不缩放的初始状态下,开发者写的1css像素
等于1dip
)
首先明确三种视口对于我们前端开发者来说存在的意义——为开发者描述多少个css像素铺满手机屏幕(一般以宽度为标准)。
- 如果不写
<meta name="viewport">
标签对视口进行配置,布局视口宽度可能为980px
等一些比较大的值,最终会把布局视口上的页面内容缩小然后放到手机屏幕内,对我们开发者而言,意思就是开发时我们写980px
的盒子就会撑满手机屏宽度。 - 如果我们在
<meta name="viewport">
中设置width
属性为具体的值或者device-width
,其实就是对布局视口的宽度进行修改,最终的效果就是:开发时写我们设置的width
就可以撑满屏幕。因为布局视口还是要缩小到视觉视口内,也就是手机屏幕里面去,这是移动端设备的处理逻辑。 <meta>
标签的另外一个属性initial-scale
,取值为如1、0.5的大于0的一个数字。这个数字表示的意义就是设备的dip
宽度,也就是理想视口宽度(定值)为视觉视口宽度的initial-scale
,也就是设置视觉视口的大小。不管怎样,移动设备的视觉视口最终就是用一整块手机屏幕进行呈现,所以说白了通过initial-scale
调整视觉视口的大小还是在描述对于开发者而言,手机屏幕(宽度)到底由多少个css像素组成。
设置width
相当于修改布局视口大小,最终布局视口缩小扔进手机屏幕里(都是后台软硬件代理完成);设置initial-scale
,相当于以ideal viewport
为参照直接修改视觉视口的大小,视觉视口最终就是由手机屏幕进行展示(后台完成),所以两者似乎会产生冲突:比如我设置布局视口为800,就表示撑满手机屏幕宽度要用800px,同时我又设置了initial-scale
为0.1
(假设设备dip
宽度为300),那么相当于修改视觉视口的宽度为3000px,表示要用3000px才能撑满手机屏幕宽度——发生冲突。
对于width
与initial-scale
对手机屏幕宽度的描述冲突时,屏幕宽度(屏幕内多少个css像素)取width与initial-scale两个属性所描述的宽度的较大值。
举个例子:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
div {
width: 50vw;
height: 50vw;
border: 1px solid red;
}
.child {
width: 380px;
height: 380px;
background: pink;
}
</style>
</head>
<body>
<div>123
<div class="child">456</div>
</div>
</script>
console.log("width=device-width, initial-scale=1.0");
console.log(window.devicePixelRatio);
console.log("layout viewport:", document.documentElement.clientWidth);
console.log("visual viewport:", window.innerWidth);
console.log("ideal viewport:", window.screen.width);
</script>
</body>
</html>
这段代码用Pixel 5
机型(dip大小/理想视口大小: 393 * 851
)打开,代码暂时只需要关注<meta>
里,以及下面我描述的456盒子的大小,width
与initial-scale
所描述的屏幕宽度都是等于设备的dip
宽度,也就是393px
效果如下(380px的456盒子基本占满了屏幕,也就是屏幕宽度就是393px):
修改<meta>
标签的initial-scale
为0.5
,并修改456盒子的宽度为760px,代码与效果如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=0.5">
<title>Document</title>
<style>
div {
width: 50vw;
height: 50vw;
border: 1px solid red;
}
.child {
width: 760px;
height: 760px;
background: pink;
}
</style>
</head>
<body>
<div>
123
<div class="child">456</div>
</div>
</script>
console.log("width=device-width, initial-scale=0.5");
console.log(window.devicePixelRatio);
console.log("layout viewport:", document.documentElement.clientWidth);
console.log("visual viewport:", window.innerWidth);
console.log("ideal viewport:", window.screen.width);
</script>
</body>
</html>
通过对比就可以印证我所说的:width
与initial-scale
所描述的屏幕大小(多少css像素撑满屏幕)冲突时取较大值。plus:仔细观察也会发现相同font-size
的文字,1px
的边框在上面呈现的效果都不一样,印证了屏幕大小变了。
width与initial-scale的取值能为移动端适配带来什么
细心的读者会发现,上面的两个html
例子中,都有一个123盒子,他的大小是50vw * 50vw
,在两个例子中屏幕大小(css多少)不同的情况下,呈现的效果是一致的(都占了屏幕宽度的一半)。所以理论上讲:如果我们在移动端开发过程中所有的大小单位都采用vw
或者rem(本质还是占屏幕的百分比)
,那么具体屏幕有多少css像素我们毫不关心,换句话说,width
与initial-scale
的取值开发者完全不关心。?
但是这两个属性因为修改了移动端屏幕内的css像素多少,所以当我们在开发中使用px
这种绝对单位时,手机屏幕具体有多少css像素就与最终的呈现效果息息相关了。如上两个html例子,粉色盒子border
都是1px,但是上面的html例子中因为手机屏幕css大小比较小,1px就比较粗。
总结:width与initial-scale的取值影响了手机屏幕里的css像素多少,决定了项目中绝对单位(px
)的呈现效果。
解读 flexible.js 适配方案的 标签
下面是flexible.js
中的一段源码,总体流程就是获取设备的dpr
,然后设置meta
标签的initial-scale
为1/dpr
:
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);
}
}
理解上面我们所说的移动端适配方案思想的同学其实应该意识到了,flexible.js
方案做到不同设备等比缩放其实如下几行代码就做到了:
function setRemUnit () {
var rem = docEl.clientWidth / 10
docEl.style.fontSize = rem + 'px'
}
setRemUnit();
之所以还要将initial-scale
设置为1/dpr
这个“多此一举”的操作其实是为了解决移动端高倍屏的“1px”问题。
1px问题
我来说一下我理解的何为“1px问题”:
设计稿上一些线条非常细,就比如设计师的要求就是我们需要一条1px物理像素的线条,那么对于高倍屏,比如dpr=2
(两个物理像素绘制一个dip
像素,可以简单理解为css像素),我们是不是就要写0.5px(我们开发时写的是dip
像素,css像素),但是各种浏览器对于这个极小的px(小于1的px)处理逻辑是不统一的:
- chrome:把小于0.5px的当成0,大于等于0.5px的当作1px
- firefox:会把大于等于0.55px的当作1px
- safiri:把大于等于0.75px的当作1px 进一步在手机上观察iOS的Chrome会画出0.5px的边,而安卓(5.0)原生浏览器是不行的。所以直接设置0.5px不同浏览器的差异比较大。
总之我们达不到设计的要求,换句话说浏览器的差异导致我们没办法在编码时统一1px(物理像素)的呈现效果。
回归<meta>
标签对于initial-scale
的设置问题,举个例子来说比较容易理解:设备dpr=2
,编码开发时我们写的0.5px的绘制效果其实就是1个物理像素进行渲染,这里的0.5因为浏览器的处理逻辑不同就会出现各种不符合预期的情况,那么设置initial-scale=1/2
之后手机屏幕内的css像素就变成了原本的两倍,此时一个css像素就由一个物理像素进行渲染,所以我们想要1px的物理像素,就直接写1px即可,因为各种浏览器对于1px的处理逻辑是统一的,所以就规避了“1px问题”。
如果你明白了上面的思想,那么网上各种对于“1px”的解决方法其实都不难理解了,就比如下面针对dpr=2
的设备的解决方法:
.scale-1px{
position: relative;
border:none;
}
.scale-1px:after{
content: '';
position: absolute;
bottom: 0;
background: #000;
width: 100%;
height: 1px;
-webkit-transform: scaleY(0.5);
transform: scaleY(0.5);
-webkit-transform-origin: 0 0;
transform-origin: 0 0;
}
解决方案的核心是什么:就是在开发时,代码里写1px而不是写0.5px,这样就统一了各浏览器的绘制逻辑,然后通过scale
缩放将其缩小至原来的一半。
移动端图片模糊问题
其实只要是理解了上面的思想,图片模糊问题就不难理解了,首先我们要知道我们平时使用的图片大都是位图(png、jpg…),位图是由一个个像素点组成的,每个像素都有具体的颜色,理论上,位图的每个像素对应在屏幕上使用一个物理像素来渲染,才能达到最佳的显示效果。 而在dpr > 1
的屏幕上,位图的一个像素可能由多个物理像素来渲染,然而这些物理像素点并不能被准确的分配上对应位图像素的颜色,只能取近似值,所以相同的图片在dpr > 1
的屏幕上就会模糊。
解决方案:
比如我们项目的<meta>
标签中initial-scale=1
,也就是说屏幕内的css像素数量就等于dip
像素数量,也就是一个css像素由dpr
个物理像素来渲染,那么我们就可以在不同dpr
的设备上展示不同分辨率的图片来解决这个问题。
比如:在dpr=2
的屏幕上展示两倍图(@2x)
,在dpr=3
的屏幕上展示三倍图(@3x)
,本质上就是让位图的一个小格子由一个物理像素去渲染从而有最好的渲染效果。
提供一种实施方案:
- srcset 使用
img
标签的srcset
属性,浏览器会自动根据像素密度匹配最佳显示图片:
<img src="conardLi_1x.png" srcset=" conardLi_2x.png 2x, conardLi_3x.png 3x">