移动端适配

@wenye  March 20, 2022

基础概念

先看看一张图😅

iphone

尺寸和分辨率

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人眼就已经分辨不出像素点了,所以又称为视网膜屏幕

在打印领域不叫ppidpi,其实这是一个东西

视口

布局视口(layout viewport)

由于手机尺寸比较小,如果在手机显示pc网站那只能显示一部分,更长的部分得通过滑动来查看。为了能默认显示更大的区域,这个值一般比较大,常见的就是980px,如果你的pc网站长度为980px,那默认就能显示完全啦,虽然字体小到都要看不清了😅

这个值可以通过document.documentElement.clientWidth来获取。

举个例子,读者们可以用手机打开这个 链接,网站是1200px的,在ios中,所以我们一开始只能看到盒子左边绿色的980px,剩下黄色的220px的就得横向拉滚动条了,虽然能将这个pc网站看的差不多了,但是字体也比较小。

ios布局视口

需要注意的是安卓打开默认会自动缩放到显示网站的全部,但此时布局视口也是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)

viewport设置

设置布局视口为理想视口
<meta name="viewport" content="width=device-width" />

这句话在ios上是没有问题的,但是在安卓上虽然布局视口改成功了,但依然会自动缩放到看得到整个网页,所以这并不是我们想要的结果啊😅

ios android

那尝尝这个呢?

设置缩放值
<meta name="viewport" content="initial-scale=1.0" />

无论是ios还是安卓都成功了,神奇的是布局视口的值也变成了理想视口的值,而且安卓因为设置了缩放值就不会出现缩放得太小的问题啦,一切都是如此美好😅

ios android

这里有个现象值得注意,为啥我设置的是缩放的值,布局视口的值也改变了?经过实际测试,安卓和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,不至于全屏缩放导致有很严重的拉伸效果,而是以一种两边留白保留了实体不至于变形。下面给个例子截图

ios布局视口

有个朋友可能要说,这样子也很丑啊,有更好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,办法大概有四种

  1. 直接设置小数的px的,测试发现ios支持,安卓不支持,太拉闸不推荐
  2. 用阴影代替边框,能正常显示圆角
  3. 给容器设置伪元素,设置绝对定位,长度或高度设置为1px,然后设置线性渐变背景,部分背景宽度/高度设置为透明,从视觉看就细了很多,这种方法只适合一条边框切无法展示圆角
  4. 给容器设置伪元素,设置绝对定位,设置好宽高后通过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上截图

ios布局视口

最后,文章终于结束了,写文章真累,各种读者们,下次再见😅


添加新评论