基础概念
先看看一张图?
尺寸和分辨率
以iphone6
为例子,iphone6
的尺寸是4.7英寸(一英寸=2.54cm),这个值其实并不是手机的长或者宽,而是对角线的长度,知道长宽情况下通过勾股定理计算出来
手机的分辨率则是750*1334,如果设计师根据iphone6
进行设计的话,给到的设计稿也就会这个尺寸?
DP(device pixels)
设备像素,又称物理像素,可以理解为一个像素就是一个发光单位,在iphone6
中这个的数量就是750*1334
DIP(device indepent pixels)
设备独立像素,又称逻辑像素或者css像素,是一种抽象出来的像素,我们平时写的width: 100px
指的就是这个
DPR(device pixles ratio)
设备像素比,计算方法为DP/DIP
,如果dpr为2,意味着一个css像素需要用2*2个物理像素来绘制,在js中可以通过window.devicePixelRatio
来获取DPR
这个东西有什么指导性意义呢?有的,那就是图片的尺寸,简单说就是在dpr为1的设备中用1倍稿,在2的时候用2倍稿。
举个例子,iPhone6
的逻辑尺寸虽然是375*667
,但是如果你用这个尺寸的图片的话,由于实际1个像素对应这4(2*2)个物理像素,所以4个物理像素为了能呈现一个像素的颜色就会通过算法强行去计算,这样子出来的效果就是很模糊。但是也不是说用像素越高的图片就越好,比如整个一亿像素的图片,图片的像素数量如果比手机物理像素多,其实就会出现模糊的逆向过程,就是一个物理像素要对应多个图片像素,也是通过算法强行计算的,这个时候呈现的效果看起来不会模糊,但是会有一些色差。
对了,如果设计师给的设计稿本身就是750*1334
,那其实这个尺寸其实就已经是二倍稿了,直接下载图片用即可。
当然安卓上的dpr可谓是千奇百怪小数点都有,比如2.75,这个时候可能就要做点牺牲了,比如2.几的dpr都采用2倍稿,不然整那么多尺寸的图片属实麻烦?
PPI(pixel per inch)
沿着对角线,每英寸所拥有的像素数目,比如iphone6
就是Math.sqrt(Math.pow(750, 2) + Math.pow(1334, 2)) / 4.7 = 326
, 一般超过300的ppi人眼就已经分辨不出像素点了,所以又称为视网膜屏幕
在打印领域不叫ppi
叫dpi
,其实这是一个东西
视口
布局视口(layout viewport)
由于手机尺寸比较小,如果在手机显示pc网站那只能显示一部分,更长的部分得通过滑动来查看。为了能默认显示更大的区域,这个值一般比较大,常见的就是980px,如果你的pc网站长度为980px,那默认就能显示完全啦,虽然字体小到都要看不清了?
这个值可以通过document.documentElement.clientWidth
来获取。
举个例子,读者们可以用手机打开这个 链接,网站是1200px的,在ios中,所以我们一开始只能看到盒子左边绿色的980px,剩下黄色的220px的就得横向拉滚动条了,虽然能将这个pc网站看的差不多了,但是字体也比较小。
需要注意的是安卓打开默认会自动缩放到显示网站的全部,但此时布局视口也是980px,只不过缩放了
视觉视口(visual viewport )
这个指的就是浏览器的可视区域的大小了。举个例子,有个长度为1000px的网站,如果你缩放到网站可以看全的程度,那这个值就是1000px,如果你只看到了网站的一半,那就是500px,你看到多大这个值就是多大。和布局视口区别就是布局视口的值是固定的。
可以通过window.visualViewport.width
来获取这个值(PS: 有兼容性问题,但是在手机上问题不大)
PS: 在pc上布局视口和视觉视口是一致的,就是浏览器的内容宽度。手机之所以要拆开那不是因为手机比较小嘛,如果布局视口整成和手机屏幕一样宽,默认看到的内容就比较少嘛,所以就整了个980px,默认看到更多的内容。
理想视口(ideal viewport)
就是手机上最理想的视口,当然就是手机本身的宽度了,用户不需要缩放也不需要横向滚动条就能看完所有内容,而且字体大小也比较合适
这个值可以通过window.outerWidth
得到(在pc端这个值可能会包含各种乱七八糟如侧边栏的宽度,手机则是干净的可用)
那我们怎么设置将布局视口设置为理想视口呢?答案就是meta
标签?
meta标签设置viewport
关于meta设置viewport想必大家都很熟悉了,以下是它的6个属性(PS: 图片的width-device写错了是device-width)
设置布局视口为理想视口
<meta name="viewport" content="width=device-width" />
这句话在ios上是没有问题的,但是在安卓上虽然布局视口改成功了,但依然会自动缩放到看得到整个网页,所以这并不是我们想要的结果啊?
那尝尝这个呢?
设置缩放值
<meta name="viewport" content="initial-scale=1.0" />
无论是ios还是安卓都成功了,神奇的是布局视口的值也变成了理想视口的值,而且安卓因为设置了缩放值就不会出现缩放得太小的问题啦,一切都是如此美好?
这里有个现象值得注意,为啥我设置的是缩放的值,布局视口的值也改变了?经过实际测试,安卓和ios的表现是有差异的,在没有设置width
的情况下,ios中布局视口的值会和视觉视口的相同,而安卓中只要initial-scale
的值不为1,那布局视口的值又会回到980px了
那有朋友要问了,这个缩放是啥东西,怎么算出来的,这里直接给结论
缩放值 = 理想视口 / 视觉视口
因为理想视口是固定的,就是手机的宽度,所以缩放值和视觉视口成反比,视觉视口越大,缩放值越小。对应的就是用手指缩小屏幕,看到的内容就会更多啦。我们通过控制缩放值来控制视觉视口的大小?
同时这个缩放值也可以通过window.visualViewport.scale
来获取到
最优解
除了上面提到的各种问题之外,还有横屏导致视觉视口显示竖屏的值等各种问题,所以最好的做法就是将两者结合起来
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
再加上不允许缩放等设置,最终的就是大家最常用的这个啦?
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
适配方案
viewport缩放方案
简单说比如设计稿为750px的网站,直接通过缩放怼到所有尺寸设备都能看得清的地步。具体做法就是将布局视口设置为750,通过设置缩放值将视觉视口的长度也设置为750?
优点是单位可以用px,直接按照设计稿去写就完事了,简单明了。缺点就是无论什么设备他们的布局视口都设置为设计稿的长度,这样子就会导致小屏幕下字体特别小,而且在大屏幕如ipad下就会有拉伸效果,导致网页比较难看
以750的设计稿尺寸,适配代码如下
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>移动端适配-viewport缩放</title>
<style>
body {
width: 750px;
margin: 0 auto;
}
.box {
width: 750px;
height: 200px;
background-color: green;
font-size: 16px;
}
</style>
</head>
<body>
<div class="box"></div>
</body>
<script>
// 缩放: 设置布局视口为设计稿尺寸 然后将缩放
const designWidth = 750;
const adapter = () => {
let meta = document.querySelector("meta[name=viewport]");
const scale = window.outerWidth / designWidth;
const content = `width=${designWidth}, initial-scale=${scale}, maximum-scale=${scale}, minimum-scale=${scale}, user-scalable=no`;
if (!meta) {
meta = document.createElement("meta");
meta.setAttribute("name", "viewport");
meta.setAttribute("content", content);
document.head.appendChild(meta);
} else {
meta.setAttribute("contnet", content);
}
};
adapter();
window.onresize = adapter;
</script>
</html>
动态rem方案
这个也是大家最常用的方案,简单说就是根据不同手机尺寸,按照设计稿和屏幕宽度的比例来设置各种元素大小。举个例子,在375px宽度的屏幕,设计稿是750px的,那两者的比例就是0.5,设计稿中100px的长度对应过来就是50px(100px * 0.5)?
具体做法就是用根font-size
保存比例值,然后通过rem
单位进行设计稿的还原,那具体是怎么算的呢?我们知道1rem
就等于根font-size
的大小,那我们只需要将设备长度和设计稿尺寸的比例保存为font-size
,那这个时候1rem
就相当于设计稿的1px
,设计稿750px
就对应750rem
1rem = 根font-size = 设备长度 / 设计稿长度 = 设备元素长度 / 设计稿元素长度
这里有个点需要注意,如果font-size直接保存比例值,这个比例值一般都是小数(比如0.5),一些手机不支持小数会出问题,所以一般会乘以一个倍数,为了方便计算这个倍数设置成100,那设计稿的100px对应的就是0.01rem,也很方便书写。还有就是需要设提前设置好视口的内容。以750px的设计稿为例子,详细代码如下?
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>移动端适配-动态rem方案</title>
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"
/>
<style>
body {
min-width: 320px;
max-width: 540px;
margin: 0 auto;
font-size: 0.16rem;
}
.box {
width: 7.5rem;
height: 2rem;
background-color: green;
}
</style>
</head>
<body>
<div class="box"></div>
</body>
<script>
/** 防抖 */
function debounce(fn, wait) {
let timer;
return (...args) => {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
fn.call(this, ...args);
}, wait);
};
}
/** 初始化rem */
function initRem() {
function initFontSize() {
const designWidth = 750;
const htmlWidth =
document.documentElement.clientWidth || document.body.clientWidth;
const width = htmlWidth > 540 ? 540 : htmlWidth;
document.getElementsByTagName("html")[0].style.fontSize =
(width / designWidth) * 100 + "px";
}
initFontSize();
const debounceInitFontSize = debounce(initFontSize, 300);
window.addEventListener("resize", debounceInitFontSize, false);
}
initRem();
</script>
</html>
那rem方案的有啥优点呢,相比较viewport缩放的方案,rem真正实现了按照屏幕比例来显示元素大小,不会出现在小屏幕中字体太小的情况。还有个优点就是既然font-size
是自己设置的,我们就能给他设置一个边界值,细心的朋友应该能发现上面代码中是设置了宽度最大为540的,相应的body也设置了max-width: 540px
,这么做的好处是适配ipad,不至于全屏缩放导致有很严重的拉伸效果,而是以一种两边留白保留了实体不至于变形。下面给个例子截图
有个朋友可能要说,这样子也很丑啊,有更好ipad适配方案的朋友请在评论区告诉我,真的想知道
vw方案
这个方案其实和rem的差不多,都是按照屏幕长度进行比例适配,区别在于用的是vw单位还是rem单位,比如750px的设计稿中一个200*300的元素,可以这样写
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>移动端适配-vw方案</title>
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"
/>
<style>
.box {
width: calc(100vw / 750 * 200);
height: calc(100vw / 750 * 300);
}
</style>
</head>
<body>
<div class="box"></div>
</body>
</html>
当然你也可以用预处理器函数来做,这个就不多赘述了
具体来说,vw方案和rem方案哪个好呢?从兼容性角度其实vw的支持度也很广了,但是rem能做到一个vw做不到的地方,那就是前面提到的边界问题,rem可以设置宽度最大显示为540px以至于在ipad不至于拉伸,这点vw是做不到的?
对了,还有个问题,既然vw是相对视口的单位,100vw就能铺满整个视口,那这个视口是理想视口,视觉视口,布局视口中的哪一个呢?答案是布局视口,猜对的小伙伴把"就这"打在公屏上?
淘宝flexible方案
flexible方案其实有两个版本,2.0的就是rem的方案。这里我们主要讨论已经被抛弃的1.0的方案?
先说结论,”1.0的方法其实就是rem的方案+viewport缩放“。
有个朋友可能要说了为啥用了rem还整个viewport缩放呢?其实这个viewport缩放和我们上面那个还不一样,他主要的目的不是为了做适配,而是为了实现大名鼎鼎的"一像素边框"。提到这里我们得先来解释下1像素问题,很多人都说1像素其实就是对应手机的1物理像素,其实这是个不对的说法。站在设计师的角度,他想要的是手机上的1个物理像素吗,不是的,其实设计师想要的750px设计稿上的1像素,你在代码中写的border: 0.01rem solid #ccc
就已经达成了这个效果,但是问题就出在0.01rem
计算的结果往往是小数,不是所有设备都支持小数px的(ios是支持的),导致了当做了1px来处理,这样子在手机上看就显得比较粗,不细腻。
回到flexible,首先我们要知道flexible1.0实现的是1物理像素,而且是有限的实现,源码中只对iphone进行实现安卓是不管的。那flexible是怎么实现1物理像素的呢?答案就是按照设备像素比将网页缩放到1css像素等于1个物理像素。举个例子,dpr为2的机子就设置缩放值为0.5(0.5=1/2),dpr为3就设置为0.33(0.33=1/3)。但是如果实际布局还是采用的rem单位,只有当需要一像素边框时候整个border: 1px
,这就很蛋疼了,为了实现1像素边框整这活,且不说这个1物理像素是不是设计师想要的,弄这么复杂其实是没必要的,关键还不支持安卓(安卓的dpr统统算成1),这大概就是flexible1.0被废弃的原因吧。
总结下,其实flexible方案就是我们上面提到的rem方案,只不过1.0多了个没啥卵用的viewport缩放,2.0已经去掉专心用rem了
其他问题
1px问题
首先这个问题得先分成两种情况去看待,第一种就是750设计稿上1像素对应到手机上的显示,第二种是实现真正的1物理像素?
由于部分手机px不支持小数,0.01rem转换成px基本都是小数,所以就当成了1px看待,最终导致边框看起来很粗,其实这个所谓的1px最终就是手机不支持0.几像素的问题,所以我们的重点就来到了如何让手机支持小数的px,办法大概有四种
- 直接设置小数的px的,测试发现ios支持,安卓不支持,太拉闸不推荐
- 用阴影代替边框,能正常显示圆角
- 给容器设置伪元素,设置绝对定位,长度或高度设置为1px,然后设置线性渐变背景,部分背景宽度/高度设置为透明,从视觉看就细了很多,这种方法只适合一条边框切无法展示圆角
- 给容器设置伪元素,设置绝对定位,设置好宽高后通过
transform: scale()
进行缩放,能正常显示圆角
那问题来了,750设计稿上1像素对应到手机是0.几像素?其实前面已经提到过,答案就是设备长度/设计稿长度
,而1物理像素则是1/dpr
。网上很多都是讲的0.5px的实现,其实这是一种偷懒的行为,每台手机尺寸不一样,750设计稿上1像素转换到手机上是0.几像素当然也就不一样,当然区别不会很大就是了,其实也够用了,所以这里我也偷个懒,直接上0.5像素的实现,读者看完0.5像素怎么实现的自然也就明白其他0.几像素怎么实现的啦?
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover"
/>
<title>0.5像素</title>
<style>
* {
margin: 0;
padding: 0;
}
body {
padding-left: 10px;
font-size: 14px;
}
.box {
width: 250px;
height: 70px;
border-radius: 10px;
margin-top: 10px;
}
.border {
border: 1px solid #ccc;
}
.border1 {
border: 0.5px solid #ccc;
/* border: 0.01rem solid #ccc; */
}
.border2 {
box-shadow: 0 0 0 0.5px #ccc;
}
.border3 {
position: relative;
}
.border3::after {
content: "";
position: absolute;
top: 0;
right: 0;
width: 1px;
height: 100%;
background-image: linear-gradient(90deg, #ccc 50%, transparent 50%);
}
.border4 {
position: relative;
}
.border4::after {
content: "";
position: absolute;
top: 0;
left: 0;
width: 200%;
height: 200%;
display: block;
border: 1px solid #ccc;
border-radius: 20px;
transform: scale(0.5);
transform-origin: left top;
}
</style>
</head>
<body>
<div id="app">
<p>设备dpr: {{dpr}}</p>
<p>设稿比: {{rem}} = 设备长度/设计稿长度</p>
<p>1物理像素缩放比例: {{scale}} =1/dpr</p>
<br />
<p>
1像素是个伪命题 设计师想要的只是750设计稿上的1像素
由于px不支持小数制作不出来那种效果 并不是真的想要一个物理像素
</p>
<div class="box border">1px边框 作为对比</div>
<div class="box border1">方案1 直接设置0.5px 兼容性差</div>
<div class="box border2">方案2 设置0.5px阴影扩散半径 能正常展示圆角</div>
<div class="box border3">
方案3 使用伪元素背景渐变实现1px背景的一半 只能显示一条边框 不能展示圆角
</div>
<div class="box border4">方案4 2倍的伪元素1px边框scale0.5倍</div>
</div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
new Vue({
el: "#app",
data() {
return {
dpr: window.devicePixelRatio,
rem: document.documentElement.clientWidth / 750,
scale: 1 / window.devicePixelRatio, // 1物理像素缩放比例
};
},
});
</script>
</html>
可以访问这个 链接 查看效果,下面再给张ios上截图
最后,文章终于结束了,写文章真累,各种读者们,下次再见?