移动端开发的适配方案 | 百分比、视口、dpr、rem、vwvh

移动端适配

所谓移动端适配,其概念就是在任意手机中我们开发的网页显示都是正常的,如下例子所示

html 复制代码
 <div class="container"></div>
css 复制代码
 * {
   margin: 0;
   padding: 0;
 }
 ​
 .container {
   width: 375px;
   height: 50px;
   background-color: red;
 }

上面的页面如果在 iPhone6/7/8 中查看那就一切正常,因为 iPhone6/7/8 的设备宽度就是 375px

如果换到 iPhone6/7/8 Plus 中,由于设备宽度为 414px,就会导致 375px 的宽度并不能占满整个屏幕,这时候就需要进行适配处理了。

因此移动端适配的任务就是要让我们的网页在各个设备中都能显示正常,包括字体、宽高、间距、图片等,常见的适配方案有很多,下面列出常用的 5 种适配方案:

  • 百分比适配
  • viewport 缩放适配
  • DPR缩放适配
  • rem适配
  • vw、vh适配

百分比适配

CSS 中盒子的宽度可以设置为一个百分比值,表示根据父级宽度的百分比来计算宽度。因此我们可以通过百分比的方式让一个盒子在任何设备中宽度占比都是一样的。

html 复制代码
 <div class="container">
   <div></div>
   <div></div>
   <div></div>
   <div></div>
 </div>
css 复制代码
 .container {
   display: flex;
   width: 100%;
   height: 50px;
 }
 ​
 .container div {
   width: 25%;
   height: 50px;
 }
 ​
 .container div:nth-child(1) {
   background-color: red;
 }
 .container div:nth-child(2) {
   background-color: yellow;
 }
 .container div:nth-child(3) {
   background-color: blue;
 }
 .container div:nth-child(4) {
   background-color: green;
 }

在上面代码中,.container 下的四个 <div> 的宽度都为 25%,其相对的是父级宽度的百分比,所以在任何设备下显示都是占同样比例。

注意: 利用百分比布局,不是所有情况都根据父级宽度来进行计算,例如

  • [max/min-]height、top、bottom 等: 取决于父级高度的百分比
  • padding、margin四个方向: 取决于父级宽度的百分比
  • transform: translate()、background-size 等: 取决于自身宽度的百分比

所以百分比布局的适配方案相对标准不统一,在开发时很容易造成意外的问题,这种方案往往需要配合其他适配方案一起使用。

viewport适配

由于在不同的设备上,CSS 像素是不一样的。例如 iPhone 6/7/8375px,而 iPhone 6/7/8 Plus414px。那么,我们可以通过设置 viewport 的缩放,来使页面显示正常。

这种适配方案的原理就是把所有机型的 CSS 像索(设备宽度)设置成一致的

html 复制代码
 <div class="container">
   <div>123</div>
   <div></div>
   <div></div>
   <div></div>
 </div>
css 复制代码
 .container {
   display: flex;
   width: 375px;
   height: 50px;
 }
 ​
 .container div {
   width: 93.75px;
   height: 50px;
 }
 ​
 .container div:nth-child(1) {
   background-color: red;
 }
 .container div:nth-child(2) {
   background-color: yellow;
 }
 .container div:nth-child(3) {
   background-color: blue;
 }
 .container div:nth-child(4) {
   background-color: green;
 }

正常来说,上面的页面在 iPhone 6/7/8 Plus 是无法占满全屏的,因为宽度只有 375px,而设备宽度是 414px

此时,我们就可以通过缩放 viewport 的形式来让网页显示正常,那么问题来了,缩放多少呢?

由于我们在编写网页时,页面的宽度是按照 375px 去开发的,因此缩放比应该按照 375px 去计算。计算缩放比的公式为:设备宽度 / 布局宽度 ,相当于 414 / 375 = 1.104。但设备宽度是不能写死的,而是要根据当前设备去获取,然后再来计算缩放比。

于是我们在设置带有 name='viewport'<meta> 标签,在 <head> 标签中添加如下的 JavaScript 代码:

js 复制代码
 (function () {
   const view = document.querySelector('meta[name="viewport"]');
   const targetWidth = 375;
   // 获取设备宽度
   const curWidth = document.documentElement.clientWidth;
   const targetScale = curWidth / targetWidth;
   view.content = `initial-scale=${targetScale},user-scalable=no,minimum-scale=${targetScale},maximum-scale=${targetScale}`;
 })();

现在页面看起来是解决了问题,在各个设备下都能正常显示。但这种适配方案也有其本身的缺点,主要有三点:

  • 在设置宽度时,要把宽度设置成一个固定值,那么所有手机看上去都是同样的大小,没有分别,不太好。厂商特意做出各种大小的手机,还要弄成一样,那买大屏机有什么意义。
  • 算出的值在一些有小数的情况下可能会出现误差(可忽略),因为设备独立像素不能有小数。
  • 对设计稿的测量存在问题。

DPR缩放适配

首先 DPR 指的是像素比(物理像素 / CSS像素)。以 iPhone6/7/8 来举例,其物理像素为 750pxCSS 像素(设备宽度)为375pxDPR2 。此时 UI 要给我们一张图占满整个屏幕的图,那么他应该按照 750px 起稿还是 350px 起稿?

关于设备像素的文章:一文带你深入了解移动端像素的概念

答案:应该是按照物理像素 750px 进行起稿。

  • 当 DPR = 1 时,使用1个物理像素显示1CSS 像素;
  • 当 DPR = 2 时,使用4个物理像素显示1CSS 像素;

如上图所示,对于 retina 视网膜屏幕(2 倍屏)而言,1 个位图像素对应于 4 个物理像素,由于单个位图像素不能再进一步分割,所以只能就近取色,这也是导致图片模糊的原因。

对于图片高清的问题,比较好的方案就是使用 2 倍图,如 200x300(css 像素) 的 <img> 标签,需要提供 400x600 的图片。

如此一来,位图像素点个数就是原来的4倍,在 retina 视网膜屏幕下,位图像素点个数就可以跟物理像素点个数形成 1:1 的比例,图片自然就清晰了。

问题:如果普通屏幕下也用了 2 倍图,会怎样呢?

在普通屏幕下,200×300(css 像素)img标签,所对应的物理像素个数是 200×300 个,而 2 倍图的位图像素个数则是200×300x4 个,所以会出现一个物理像素点对应4个位图像素点

所以取色过程也只能通过一定的算法(downsampling 算法)进行处理,显示结果就是一张只有原图像素总数四分之一的图片,肉眼看上去图片不会模糊,但是图片会缺少一些锐利度,或者是有色差(在接受范围内)。

那么基于 DPR 的缩放适配方式,其原理就是将 CSS 像素缩放成与设备像素一样大的尺寸

在实际开发中,设计者为了页面的高清,都是采用物理像素的值来进行设计。如 iPhone6/7/8 的设备宽度为 375px,则将其缩放为 750px

那么使用 DPR 计算缩放比就是:缩放比 = 1 / DPR ---> 1 / (物理像素 / CSS像素) ---> 1 / (750 / 375)

js 复制代码
(function () {
  var view = document.querySelector('meta[name="viewport"]');
  // 直接获取设备的DPR
  var targetScale = 1 / window.devicePixelRatio;
  view.content = `initial-scale=${targetScale},user-scalable=no,minimum-scale=${targetScale},maximum-scale=${targetScale}`;
})();

但是按照 DPR 缩放适配后反而占不满了,因为我们给的宽度为 375px,因此只占了一半,那么这种适配方案的意义是什么呢?

实际上这种方案最大的意义就是开发者和设计者的像素都是统一的 ,因为 UI 是按照 750px 来设计的(iPhone6/7/8为例),那么前端在量图的时候,也是以 750px 为基准。通过 DPR 适配,量出来多少写代码时就可以设置多少。

但这种方案并没有真正解决适配问题,因为设计稿是 750px 像素,前端测量出来的也是 750px,但是如果将宽度设置为 750px,在 iPhone 6/7/8 Plus又存在适配问题了,那么就需要配合 rem 进行适配了。

rem适配

rem 适配也是移动端比较主流的一种适配方案

rem 全称 font size of the root element,是 CSS3 新增的一个相对单位,含义是相对于根元素的字体大小的单位,它就是一个相对单位。

如下代码所示:

html 复制代码
 <div class="container"></div>
 <p>Hello Rem</p>
css 复制代码
 :root {
   font-size: 10px;
 }
 .container {
   width: 20rem;
   height: 20rem;
   background-color: blue;
 }
 p {
   font-size: 3rem;
 }

在上面的示例中,将根元素的字体设置为10px,其他元素都设置 rem 单位,表示根据根元素字体大小来缩放。例如 <p> 元素的字体大小是 3rem,也就是 3×10px = 30px;而 .container 的宽高为 20rem,大小为 200x200px

rem适配的原理:

rem 适配的原理就是把设备宽度都分成相同的若干份,然后再计算元素宽度所占的份数。

例如 iPhone5iPhone6 对应的设备宽度分别为 320px375px,现在将其分为100 列,那么每一列为 3.2px3.75px。不同的设备宽度对应的每一列的宽度都不一样。在设置元素的宽度时,以列为媒介即可。

同样一个 <div>,设置它的宽度为10 份,那么在 iPhone5 中该 <div> 的宽就是 32px,而在 iPhone6 中该 <div> 的宽度就是 37.5px。通过这种方式就实现了不同宽度的设备中,一个元素的大小可以等比例的缩放。

但在我们日常开发中,从设计稿量出来都是像素,假设从设计稿量出来是 100px,那怎么知道这是多少列呢?

此时就需要一个转换了,我们需要算出测量出来的宽度在总宽度中究竟占几列。

假如设计稿是按照 iPhone 6/7/8 尺寸(750px)设计的,我们测量出来的是 100px,而换算成设备像素(375px)就是 50px。得知一列的宽度和总宽度,那么可以算出 50/3.75 约等于13.33 列,也就是设置为13.33 * 1列的宽度。

如下面例子所述:

ini 复制代码
 iPhone6 375px
 iPhone6 Plus 414px
 ​
 现在设计师按照 750px 起稿,有一个元素测量出来是 375px(物理像素)
 设备宽度是375px,该元素换算成设备宽度: 375 / 2 = 187.5
 接下来计算列数:187.5 / 3.75 = 50
 设置该元素的宽度为:50 * 一列宽度
 ​
 在iPhone6中:50 * 3.75px = 187.5px
 在iPhone6 Plus中:50 * 4.14px = 207px

这个一列的宽度,实际上就是 rem。也就是说 rem 的适配方式,是根据屏幕的宽度配合列数算出一列的宽度,之后设置 HTML 根元素的 font-size 就为一列的宽度即可。

下面这种方案是设计稿尺寸和列数之间的转换,只需要将量出来的尺寸小数点往前移动两位即可。

js 复制代码
 (function (doc, win, designWidth) {
   const html = doc.documentElement; // 获取html根元素
   function refreshRem() {
     const width = html.clientWidth; // 获取CSS像素(设备宽度)
     // 当设备宽度大于设计稿时,测量出来是多少就是多少
     if(width >= designWidth) {
       html.style.fontSize = '100px'
     } else {
       // 否则要算出每列宽度
       // 如iPhone6,每一列的宽度为 375/750 = 0.5px,相当于分成750列
       // 但是 ont-size 不可以设置为0.5px,所以要x100
       // 那么每一列宽度就变成了50px,相当于分成7.5列
       // 在px和rem转换时,设计稿的100px --> 设备上的50px --> 1rem
       // 3.75rem --> 设计稿375px --> 设备显示187.5px
       html.style.fontSize = 100 * (width / designWidth) + 'px';
     }
   }
   doc.addEventListener('DOMContentLoaded', refreshRem);
 })(document, window, 750);

vw、vh适配

CSS3 除了 rem,还有4个和 viewport 相关的单位,分别为 vwvhvminvmax

  • vw 的全称是 Viewport Width,1vw 相当于 window.innerWidth 的 1%
  • vh 的全称是 Viewport Height,1vh 相当于 window.innerHeight 的 1%
  • vmin 的值是当前 vwvh 中较小的值
  • vmax 的值是当前 vwvh 中较大的值

vw 相当于直接将屏幕分为了 100 列,1vw 就是 1 列,那么按照 iPhone 6/7/8 设备宽度为 375px,因此 1vw 就是 3.75px,根据上面的公式:

js 复制代码
 html.style.fontSize = 100 * (width / designWidth) + 'px'

将根元素的字体大小设置为 50px,和设计稿测量出来的尺寸转换会非常方便,只需小数点往前移动 2 位即可。

iPhone6/7/8 为基准,由于 1vw 相当于屏幕的一列,所对应的宽度为 3.75px,所以可计算出 50px 对应的 vw50 / 3.75 = 13.33333333vw

html 复制代码
 <div class="container"></div>
css 复制代码
 :root {
   font-size: 13.3333333vw;
 }
 .container {
   width: 3.75rem;
   height: 3.75rem;
   background-color: blue;
 }

可以看到,使用 vw 的最大优点就是不需要使用 JavaScript 来做处理了,因为分列 vw 已经处理了,唯一需要做的就是计算根元素字体应设置多少 vw,然后从设计稿测量出来的尺寸仍然是小数点前移 2 位即可。

相关推荐
中微子4 分钟前
JavaScript 防抖与节流:从原理到实践的完整指南
前端·javascript
天天向上102419 分钟前
Vue 配置打包后可编辑的变量
前端·javascript·vue.js
芬兰y34 分钟前
VUE 带有搜索功能的穿梭框(简单demo)
前端·javascript·vue.js
好果不榨汁41 分钟前
qiankun 路由选择不同模式如何书写不同的配置
前端·vue.js
小蜜蜂dry41 分钟前
Fetch 笔记
前端·javascript
拾光拾趣录43 分钟前
列表分页中的快速翻页竞态问题
前端·javascript
小old弟43 分钟前
vue3,你看setup设计详解,也是个人才
前端
Lefan1 小时前
一文了解什么是Dart
前端·flutter·dart
Patrick_Wilson1 小时前
青苔漫染待客迟
前端·设计模式·架构
vvilkim1 小时前
Nuxt.js 全面测试指南:从单元测试到E2E测试
开发语言·javascript·ecmascript