CSS --- 移动端适配方案

1,778 阅读9分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第27天,点击查看活动详情

移动互联网的快速发展,让人们已经越来越习惯于使用手机来完成大部分日常的事务

但是移动端的屏幕大小多种多样,所以需要对移动端的屏幕进行适配

LxqlJu.png

所以我们需要使用某种适配方案,来实现移动端页面的自适应

自适应: 网页会根据不同的设备屏幕大小来自动调整尺寸、大小

响应式: 网页会随着屏幕的实时变动而自动调整,是一种自适应的一种表现形式

视口

在一个浏览器中,我们可以看到的区域就是视口(viewport)

在PC端的页面中,我们是不需要对视口进行区分,因为我们的布局视口和视觉视口是同一个

但是在移动端网页的视口被划分为三种不同的类别:

分类说明
布局视口(layout viewport)移动网页真正用来布局的视口,默认的宽度为980px
会自动等比例进行缩放或拉伸,以使网页适应于视觉视口
视觉视口(visual layout)移动设备真正可见区域
理想视口(ideal layout)当布局视口 等于 视觉视口的时候,该视口就被称之为理想视口

Lxq6zG.png

设置布局视口

如果默认情况下,我们按照980px显示内容,那么右侧有一部分区域 就会无法显示,所以手机端浏览器会默认对页面进行缩放以显示到用 户的可见区域中

事实上这种方式是不利于我们进行移动的开发的,我们希望的是设置100px,那么显示的就是100px

此时我们可以通过设置布局视口,来阻止页面在移动端进行缩放

<!-- content中的多个值之间使用逗号进行分割,也可以使用分号或空格进行分割,推荐使用逗号 -->
<!--
  user-scalable=no 属性可以禁止用户对网页进行自主的缩放
  但是user-scalable这个属性 有一些小部分浏览器是不支持的
  所以我们需要将最大缩放比和最小缩放比都设置1.0来兼容一些小部分浏览器
-->
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, maximum-scale=1.0, minimum-scale=1.0">
属性
width可以是具体的值,如980px, 375px等
也可以是关键字device-width, 表示的是设备宽度
initial-scale设置网页的默认缩放比为1.0
其值可以是一个 0.0 和 10.0 之间的正数
user-scalable避免用户通过手指或鼠标键盘对网页进行缩放,从而破坏网页的布局
可选值为: yes 或者 no
默认值为yes
也可以设置为1或0
1代表yes,0代表no
但是还是推荐直接使用yes或no
maximum-scale定义缩放的最大值,必须大于等于 minimum-scale,否则表现将不可预测
一个 0.0 和 10.0 之间的正数
minimum-scale定义缩放的最小值,必须小于等于 maximum-scale,否则表现将不可预测
一个 0.0 和 10.0 之间的正数

移动端布局方案

移动端的屏幕尺寸通常是非常繁多的,很多时候我们希望在不同的屏幕尺寸上显示不同的大小

比如我们设置一个100x100的盒子

  • 在375px的屏幕上显示是100x100
  • 在320px的屏幕上显示是90+ x 90+
  • 在414px的屏幕上显示是100+ x 100+

其他尺寸也是类似,比如padding、margin、border、left,甚至是font-size等等

常见的适配方案:

  1. 百分比设置
    • 因为不同属性的百分比值,相对的可能是不同参照物,所以百分比往往很难统一
    • 所以百分比在移动端适配中使用是非常少的
  2. rem单位 + 动态html的font-size
  3. vw单位
  4. flex的弹性布局
  5. Grid布局

rem单位 + 动态html的font-size

rem单位是相对于html元素的font-size来设置的,

那么如果我们需要在不同的屏幕下有不同的尺寸,可以动态的修改html的 font-size尺寸

例如实现如下方案:

LxdlJT.png

所以,在使用rem进行移动端适配的时候,主要需要实现如下问题:

  1. 针对不同的屏幕,设置html不同的font-size
  2. 将原来要设置的尺寸,转化成rem单位

针对不同的屏幕,设置html不同的font-size

方案一: 使用媒体查询

可以通过媒体查询来设置不同尺寸范围内的屏幕html的font-size尺寸

@media screen and (min-width: 320px) {
  :root {
    font-size: 12px;
  }
}

@media screen and (min-width: 375px) {
  :root {
    font-size: 14px;
  }
}

@media screen and (min-width: 414px) {
  :root {
    font-size: 16px;
  }
}

@media screen and (min-width: 480px) {
  :root {
    font-size: 18px;
  }
}


.box {
  width: 10rem;
  height: 10rem;
  background-color: skyblue;
}

媒体查询实现移动端适配的缺点:

  1. 我们需要针对不同的屏幕编写大量的媒体查询,
  2. 如果动态改变尺寸,不会实时的进行更新
    • 因为编写的媒体查询是一个范围值
    • 只有触发范围边界值的时候才会触发元素大小的改变
    • 并不会做到随着屏幕大小的改变,元素进行实时大小的改变

方案二: 编写JavaScript代码

如果希望实时改变屏幕尺寸时,font-size也可以实时更改,可以通过js代码

  1. 根据html的宽度计算出font-size的大小,并且设置到html上
  2. 监听页面的实时改变,并且重新设置font-size的大小到html上

事实上, lib-flexible库 实现了相同的功能,我们可以在开发中,直接使用lib-flexible库来完成相同的功能

// 编写为一个IIFE,避免污染全局变量,并可以在加载的时候自动执行
(function flexible (window, document) {
  // 获取HTML元素
  const htmlEl = document.documentElement
  // 获取当前设备的dpr
  const dpr = window.devicePixelRatio || 1

  // adjust body font size
  /*
    因为font-size是一个可继承的css属性
    所以在html上设置的font-size会依次继承
    导致在默认情况下,默认的字体大小变得很大
    所以需要在body中对字体进行重置,以免默认字体过大
   */
  function setBodyFontSize () {
    // document.body 用于获取body元素
    if (document.body) {
      // body的字体乘以dpr以便于默认字体的大小也可以自适应跳转
      // document.body.style.fontSize = (12 * dpr) + 'px'
      // 上述代码可以写成如下形式
      document.body.style.fontSize = (12 / (htmlEl.clientWidth / 10) * dpr) + 'rem'
    }
    else {
      // 如果在执行setBodyFontSize的时候,混 界面没有加载完毕
      // 那么就等到界面加载完毕以后再次执行setBodyFontSize
      document.addEventListener('DOMContentLoaded', setBodyFontSize)
    }
  }

  // 在执行函数的时候进行调用
  setBodyFontSize();

  // set 1rem = viewWidth / 10
  function setRemUnit () {
    // 获取页面的宽度
    const width = htmlEl.clientWidth

    // 设置rootFontSize
    // 10在这里只是一个参考值,也可以是别的值
    const rem = htmlEl.clientWidth / 10
    htmlEl.style.fontSize = rem + 'px'
  }

  // 初始化的时候,不会触发resize函数,需要手动调用
  setRemUnit()

  // reset rem unit on page resize
  // 监听页面改变事件
  window.addEventListener('resize', setRemUnit)
  // 页面通过前进后退进入的时候,会读取缓存,需要重新设置
  window.addEventListener('pageshow', function (e) {
    if (e.persisted) {
      setRemUnit()
    }
  })

  // detect 0.5px supports
  // 只有dpr大于等于2的时候才有可能支持0.5px
  // 因为此时0.5px的逻辑像素可以对应1px的物理像素
  if (dpr >= 2) {
    // create fake body
    const fakeBody = document.createElement('body')
    const testElement = document.createElement('div')
    testElement.style.border = '.5px solid transparent'
    fakeBody.appendChild(testElement)
    htmlEl.appendChild(fakeBody)
    // 如果 0.5px border-top + 0.5px border-bottom = 1px 成立
    // 就表示当前浏览器支持0.5px
    if (testElement.offsetHeight === 1) {
      // 如果支持0.5px,就在html元素上添加一个名为hairlines的特殊样式
      htmlEl.classList.add('hairlines')
    }
    // remove fake body
    htmlEl.removeChild(fakeBody)
  }
}(window, document))

rem单位的换算

  1. 手动换算

比如有一个在375px屏幕上,100px宽度和高度的盒子

我们需要将100px转成对应的rem值

100/37.5=2.6667,其他也是相同的方法计算即可

  1. 使用less/scss函数
// px -> rem
.px2rem(@px) {
  // 推荐将 1rem 写在最前边 是因为less会以第一个单位作为最终单位
  res: 1rem * (@px / 37.5)
}

.box {
  width: .px2rem(100)[res];
  height: .px2rem(100)[res];
  background-color: gray;
}

.lorem {
  font-size: .px2rem(18)[res];
}
  1. 使用postcss-pxtorem

​ 这是一个postcss的插件,通过结合webpack,vite之类的打包工具可以实现自动的px2rem

  1. 使用VScode插件,px to rem 在编写的时候,自动进行转化

​ 在绝大多数情况下,一般是以iPhone6的宽度作为设计稿的设计标准

​ 而iphone6的宽度为750px,又因为iPhone6的dpr是2,所以iphoe6的视口宽度是375px

​ 但是px to rem 默认的rootFontSize为16px, 所以我们需要对该插件进行对应的配置

LxdNzO.png

​ 如图,点击配置按钮,进入插件的配置页面

Lxdk6e.png

vw单位

rem事实上是作为一种过渡的方案,它利用的也是vw的思想

前面不管是我们自己编写的js,还是flexible的源码,都是将1rem等同于设计稿的1/10,在利用1rem计算相对于整个屏幕的尺寸大小

但是1vw本质上,就是视口宽度的一百分之一,所以使用vw进行移动端适配的时候,不需要自己再去编写对应的适配函数或使用第三方库,因此vw的使用相比rem的使用更为的方便

vw相比rem的好处:

  1. 不需要去计算html的font-size大小,也不需要给html设置这样一个font-size
  2. 因为没有设置html的font-size大小,所以也就不需要给body再设置一个font-size,以防止继承
  3. vw相比于rem更加语义化,1vw刚才是1/100的viewport的大小

但是vw相比rem有一个缺点:

因为vw是相对于视口的,所以如果视口改变,对应的基于vw的值也会相应的发送改变,因此没有办法设置页面的最大宽度

如果我们希望页面放大到一定的宽度,例如600px的时候,页面不在相应的放大,此时依旧需要结合rem来进行使用

:root {
  /* 375 / 10 = 37.5 */
  font-size: 10vw;
}

.box {
  width: 2.6667rem;
  height: 2.6667rem;
  background-color: gray;
}

.lorem {
  font-size: 0.48rem;
}

但是,其实1vw是视口宽度的一百分之一,所以上边的代码可以修改为

.box {
  width: 26.6667vw;
  height: 26.6667vw;
  background-color: gray;
}

.lorem {
  font-size: 4.8vw;
}

vw的单位换算

  1. 手动换算

    比如有一个在375px屏幕上,100px宽度和高度的盒子 -> 1vw === 3.75px

    我们需要将100px转成对应的vw值

    100/3.75=26.667,其他也是相同的方法计算即可

  2. 使用less/scss函数

.px2vw(@px) {
  res: (@px / 3.75) * 1vw
}

.box {
  width: .px2vw(100)[res];
  height: .px2vw(100)[res];
  background-color: gray;
}

.lorem {
  font-size: .px2vw(18)[res];
}
  1. 使用postcss的插件postcss-px-to-viewport-8-plugin在打包的时候进行自动转换

  2. 使用px to rem插件在编写的时候自动进行转换

vw 结合 rem一起使用

vw的好处是计算方便,我们可以不使用JavaScript脚本就可以完成移动端的适配

但是vw没有办法设置最大的宽度,所以在实际开发中,我们一般会将vw和rem结合在一起进行使用

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    :root {
      box-sizing: border-box;
      /*
        在M站的时候,假设设计稿的宽度为375px
        1rem = 4vw = 3.75 * 4 = 15px
        1px = 1 / 15px ~= 0.0667rem(保留小数后 4 位)

        所以当宽度为100%的时候,正好是25rem
      */
      font-size: 4vw;
    }

    @media screen and (min-width: 600px) {
      :root {
        /*
          M站页面在PC端显示的时候,字体大小默认为24px
          viewport >= 600px 时,1rem = 600px / 100 * 4 = 24px
         (因为我们限制了 div.-mobile-wrapper 的 max-width 为 600px)
        */
        font-size: 24px;
      }
    }

    /* 重置样式 */
    body {
      margin: 0;
      padding: 0;
    }

    /* M站页面需要在最外层容器上使用该类进行包裹,以保证页面的最大宽度为600px */
    /* 全局 css 以 - 开头 */
    .-mobile-wrapper {
      max-width: 600px;
      min-height: 100vh;
      background: #f5f5f5;
      
      /* 
      	添加底部安全区的距离,避免iphone手机 页面被底部的圆角和小黑条所遮挡
      	如果设计稿的底部没有任何的内容需要显示的情况下,这个属性可以被移除
      */
      padding-bottom: calc(env(safe-area-inset-bottom));
      
      /* M站在PC显示的时候,页面可以居中显示 */
      margin: 0 auto;

      /* 使 absolute 子元素基于此容器定位 */
      position: relative;

      /* 建立 BFC,防止第一个子元素的 margin-top collapsing */
      display: flow-root;
    }

    /* ----------------------------------- */

    /* 自定义的CSS样式 */
    .wrapper {
      background-color: gray;
    }

    .box {
      width: 6.6667rem;
      height: 6.6667rem;
      background-color: skyblue;
    }

    .lorem {
      font-size: 1.2rem;
    }
  </style>
</head>
<body>
  <div class="-mobile-wrapper wrapper">
    <div class="box"></div>
    <span class="lorem">lorem</span>
  </div>
</body>
</html>