移动端适配总结(长文)

7,832 阅读12分钟

布局

  • 静态布局: 定宽定高布局, 不随设备和视口改变【定宽居中, pc】
  • 流式布局: 大小改变,布局不变,宽度使用百分比和min/max,高固定 【栅栏系统(网格系统)】
  • 自适应布局: 大小改变,主体内容和布局没变,缺点,太小屏幕内容过于拥挤 (大小不变,位置改变)
  • 弹性布局: 大小位置都会变化(flex/rem/em/vw/vh)
  • 响应式布局: 展示的内容,大小位置都会变化, 媒体查询+流式布局 (媒体查询, vw, rem/em)

相关概念

设备像素(Device Pixels)

设备屏幕的物理像素,表示屏幕上可以铺多少个点点,而不是一个绝对长度单位(例如in,mm); 单位是px,比如iPhone6的 (750 x 1334px)

分辨率(Resolution)

一个物理概念
对于屏幕,分辨率一般表示屏幕上显示的物理像素总和。比如,我们说iPhone6屏幕分辨率是(750 x 1334px)
对于图像,概念等同于图像尺寸、图像大小、像素尺寸等等。比如,我们说(20 x 20px)的icon

CSS像素(CSS Pixels)

是Web编程的概念,指的是CSS样式代码中使用的逻辑像素, 或者称为设备独立像素, 因为只与设备相关;
1个CSS像素在不同设备上可能对应不同的物理像素数,这个比值是设备的属性(Device Pixel Ratio,设备像素比),比如,iPhone6:375 x 667px

通过 document.documentElement.clientWidth/clientHeight / document.documentElement.getBoundingClientRect().width 获取

在CSS规范中,长度单位可以分为绝对单位和相对单位。px是一个相对单位,相对的是设备像素(Device Pixels)

设备独立像素(device-independent pixels (DIP) / Density-independent Pixels (DP))

Android设备的特点是屏幕尺寸很多,因此为了显示能尽量和设备无关,提出了dip,参照的density是160。

// 当屏幕密度density为160(单位是ppi或者dpi,一个意思)时,px === dip
px = dip * density / 160 
// 所以
dip = px * 160 / density

注: 此处只针对于Android, windows 也有 DIP 概念, 含义不同, IOS貌似不存在

设备像素比 (Device Pixel Ratio (DPR))

Device pixel ratio, the ratio between physical pixels and logical pixels used by cascading style sheets (CSS): other names for it are “CSS Pixel Ratio” and “dppx” 表示1个CSS像素(宽度)等于几个物理像素(宽度)

DPR = 物理像素(设备像素) / 逻辑像素(css像素/设备独立像素) // [未缩放]

像素密度

像素密度也叫显示密度或者屏幕密度,缩写为DPI(Dots Per Inch)或者PPI(Pixel Per Inch)

// 屏幕对角线的像素尺寸 / 物理尺寸(inch 英寸)
Math.sqrt(750*750 + 1334*1334) / 4.7 = 326ppi

视口(viewport)

桌面上视口宽度等于浏览器宽度,但在手机上有所不同。

布局视口(layout viewport)

手机上为了容纳为桌面浏览器设计的网站,默认布局视口宽度远大于屏幕宽度,为了让用户看到网站全貌,它会缩小网站 document.documentElement.clientWidth

视觉视口(Visual viewport)

屏幕的可视区域,即物理像素尺寸, 可变, 与当前缩放值和设备的屏幕宽度有关 visual viewport宽度 = ideal viewport宽度 / 当前缩放值 可以通过window.innerWidth来获取,但在Android 2, Oprea mini 和 UC 8中无法正确获取。

理想视口(ideal viewport)

ideal viewport是最适合移动设备的viewport,ideal viewport的宽度等于移动设备的屏幕宽度 在移动开发时, 在meta[name='viewport']中, 通过width = device-width把当前的viewport宽度设置为理想视口, 否则宽度将默认为布局视口980

ideal viewport并没有一个固定的尺寸,不同的设备拥有有不同的ideal viewport。早期所有iPhone理想视口为320x480px

所以,在没有缩放的情况下,屏幕的CSS像素宽度其实是指理想视口的宽度,而meta标签:

<meta name="viewport" content="width=device-width, inital-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"/>

指定了布局视口=理想视口,并且禁止缩放。所以添上width=device-width的viewport meta后页面变大了(一开始页面内容小得看不清),实际上是布局视口变小了

initial-scale=1 解决了 iphone、ipad 无论横竖屏都把宽度设为竖屏时 ideal viewport 的宽度 width=device-width 解决了IE 无论横竖屏都把宽度设为竖屏时 ideal viewport 的宽度

视觉视口与理想视口关系:

visual viewport宽度 = ideal viewport宽度 / 当前缩放值
当前缩放值 = ideal viewport宽度 / visual viewport宽度

参考: www.ayqy.net/blog/完全理解px… github.com/jawil/blog/…

rem (Font size of the root element)

一个相对长度单位。作用于根元素,相对于初始值/默认值大小;作用域非根元素,相对于根元素html字体大小(常用)

em

Font size of the parent, in the case of typographical properties like font-size, and font size of the element itself, in the case of other properties like width.
作用于font-size属性,相对于父元素字体大小(常用);作用于非font-size属性,相对于自身字体大小

布局方案

百分比+媒体查询

媒体查询 前提: <meta name="viewport" content="width=device-width"/> mate 中的 device-width / width:

  • device-width 是设备实际的宽度,只和设备的分辨率有关,一般是设备物理像素 / 设备像素比,且不会随着手机旋转而改变其值, 因此并不适合开发响应式网站 (css逻辑像素)
  • width 指的是可视区域的宽度, 会和 viewport 的 scale 属性相关,为页面的可视区域的宽度
    可理解为,把布局视口设置为理想视口

最常见的方式,通过屏幕宽度(用CSS像素描述的宽度)来区分各种设备,如下:

@media (min-width:320px) { /* smartphones, portrait iPhone, portrait 480x320 phones (Android) */ }
@media (min-width:480px) { /* smartphones, Android phones, landscape iPhone */ }
@media (min-width:600px) { /* portrait tablets, portrait iPad, e-readers (Nook/Kindle), landscape 800x480 phones (Android) */ }
@media (min-width:801px) { /* tablet, landscape iPad, lo-res laptops ands desktops */ }
@media (min-width:1025px) { /* big landscape tablets, laptops, and desktops */ }
@media (min-width:1281px) { /* hi-res laptops and desktops */ }


min-width: 480px: Will target mobile devices in landscape mode and up
// 
@media screen and (min-width: 320px) {
    html {
        font-size: 50px;
    }
}
@media screen and (min-width: 360px) {
    html {
        font-size: 56px;
    }
}
@media screen and (min-width: 414px) {
    html {
        font-size: 63px;
    }
}

rem布局

rem: 根元素(html)的字体大小. 即 1rem = html中设置的font-size

获取设备宽度 document.documentElement.getBoundingClientRect().width / document.documentElement.clientWidth

原理: 实质时比例问题, 根据设计图比例计算出固定 rem 值 实现: 主要通过修改 html 的 fontSize

  1. 直接引入 [www.jianshu.com/p/b00cd3506…]
   (function (doc, win) {
        var docEl = doc.documentElement,
            resizeEvt = 'orientationchange' in window ? 'orientationchange' : 'resize',
            recalc = function () {
                var clientWidth = docEl.clientWidth;
                if (!clientWidth) return;
                if(clientWidth>=640){
                    docEl.style.fontSize = '100px';
                }else{
                    docEl.style.fontSize = 100 * (clientWidth / 640) + 'px'; // 640: 可根据设计图来定
                }
            };

        if (!doc.addEventListener) return;
        win.addEventListener(resizeEvt, recalc, false);
        doc.addEventListener('DOMContentLoaded', recalc, false);
    })(document, window);
  1. 使用flexible.js
    github.com/amfe/lib-fl…

缺点:

  1. iframe 问题
  2. 富文本问题
  3. 高清方案
  4. 部分android机型不兼容
  5. 系统字体缩放时, 发生变化, 导致页面错乱,因为和根元素字体紧密联系;解决方案: 缩放还原
    1> juejin.cn/post/684490…
    2> 基于 flexible.js 添加以下代码: juejin.cn/post/684490…
var root = window.document.documentElement;
var fontSize = parseFloat(root.style.fontSize);
// html最终的font-size大小 
var finalFontSize = parseFloat(window.getComputedStyle(root).getPropertyValue("font-size"));
if(finalFontSize !== fontSize) {
    root.style.fontSize = fontSize * fontSize / finalFontSize + "px";
}
  • getComputedStyle Window.getComputedStyle()方法返回一个对象,该对象在应用活动样式表并解析这些值可能包含的任何基本计算后报告元素的所有CSS属性的值。 私有的CSS属性值可以通过对象提供的API或通过简单地使用CSS属性名称进行索引来访问。

  • getPropertyValue 此 CSSStyleDeclaration.getPropertyValue() 接口会返回一个 DOMString ,这个返回值将会包含预请求的CSS属性信息。

lib-flexible

旧版本

旧版本#17 代码

例如 ios
scale=0.5
innerWidth=750
device-width=375
innerWidth * scale = (device-width = layout-viewport-width)

处理过程如下:

  1. 先取 dpr (dpr = window.devicePixelRatio)
  2. 再设置 scale = 1/dpr
  3. 然后就有 innerWidth 了, innerWidth = 375 / scale = 750px (device-width = document.documentElement.clientWidth)
  4. 最后将 innerWidth 分成 10rem, font-size = innerWidth / 10 = 75px
;(function(win, lib) {
    var doc = win.document;
    var docEl = doc.documentElement;
    var metaEl = doc.querySelector('meta[name="viewport"]');
    var flexibleEl = doc.querySelector('meta[name="flexible"]');
    var dpr = 0;
    var scale = 0;
    var tid;
    var flexible = lib.flexible || (lib.flexible = {});
    
    if (metaEl) {
        console.warn('将根据已有的meta标签来设置缩放比例');
        var match = metaEl.getAttribute('content').match(/initial\-scale=([\d\.]+)/);
        if (match) {
            scale = parseFloat(match[1]);
            dpr = parseInt(1 / scale);
        }
    } else if (flexibleEl) {
        var content = flexibleEl.getAttribute('content');
        if (content) {
            var initialDpr = content.match(/initial\-dpr=([\d\.]+)/);
            var maximumDpr = content.match(/maximum\-dpr=([\d\.]+)/);
            if (initialDpr) {
                dpr = parseFloat(initialDpr[1]);
                scale = parseFloat((1 / dpr).toFixed(2));    
            }
            if (maximumDpr) {
                dpr = parseFloat(maximumDpr[1]);
                scale = parseFloat((1 / dpr).toFixed(2));    
            }
        }
    }

    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);
        }
    }

    function refreshRem(){
        // 动态设置的缩放大小会影响布局视口的尺寸, 设备物理像素大小
        // 设备逻辑像素 device-width = 设备物理像素 /(devicePixelRatio * scale)
        var width = docEl.getBoundingClientRect().width; // iphone6 => 750
        if (width / dpr > 540) {
            width = 540 * dpr;
        }
        var rem = width / 10;
        docEl.style.fontSize = rem + 'px';
        flexible.rem = win.rem = rem;
    }

    win.addEventListener('resize', function() {
        clearTimeout(tid);
        tid = setTimeout(refreshRem, 300);
    }, false);
    win.addEventListener('pageshow', function(e) {
        if (e.persisted) {
            clearTimeout(tid);
            tid = setTimeout(refreshRem, 300);
        }
    }, false);

    if (doc.readyState === 'complete') {
        doc.body.style.fontSize = 12 * dpr + 'px';
    } else {
        doc.addEventListener('DOMContentLoaded', function(e) {
            doc.body.style.fontSize = 12 * dpr + 'px';
        }, false);
    }
    

    refreshRem();

    flexible.dpr = win.dpr = dpr;
    flexible.refreshRem = refreshRem;
    flexible.rem2px = function(d) {
        var val = parseFloat(d) * this.rem;
        if (typeof d === 'string' && d.match(/rem$/)) {
            val += 'px';
        }
        return val;
    }
    flexible.px2rem = function(d) {
        var val = parseFloat(d) / this.rem;
        if (typeof d === 'string' && d.match(/px$/)) {
            val += 'rem';
        }
        return val;
    }

})(window, window['lib'] || (window['lib'] = {}));

新版本

新版本 2.0代码

meta 标签固定为

<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no”>

代码逻辑为: 取 width 分为 10rem, font-size = width / 10

: 手机淘宝首页 2019-01-16, 也是这个方案, 但是是 width = 3.75rem, font-size = 100px, 估计是为了方便除

代码, 添加部分注释

(function flexible (window, document) {
  var docEl = document.documentElement
  var dpr = window.devicePixelRatio || 1

  // adjust body font size
  function setBodyFontSize () {
    if (document.body) {
      document.body.style.fontSize = (12 * dpr) + 'px' 
    }
    else {
      document.addEventListener('DOMContentLoaded', setBodyFontSize)
    }
  }
  setBodyFontSize();

  // set 1rem = viewWidth / 3.6, 当设计稿360px, 更好计算
  function setRemUnit () {
    // var rem = docEl.clientWidth / 10 // 原始代码
    var rem = docEl.clientWidth / 3.6  // 修改后代码
    docEl.style.fontSize = rem + 'px'
  }

  setRemUnit()

  // reset rem unit on page resize
  window.addEventListener('resize', setRemUnit)
  window.addEventListener('pageshow', function (e) {
    // 参考: https://juejin.cn/post/6844904002103033870
    // 页面从浏览器的缓存中读取该属性返回 ture, 优化方案 window.performance.navigation.type
    // if (e.persisted) {  // 原始代码
    //   setRemUnit()
    // }
    if (e.persisted || (window.performance && window.performance.navigation.type === 2)) { // 修改后代码
      setRemUnit()
    }
  })

  // detect 0.5px supports 检测是否支持0.5px, 用于1px问题
  if (dpr >= 2) {
    var fakeBody = document.createElement('body')
    var testElement = document.createElement('div')
    testElement.style.border = '.5px solid transparent'
    fakeBody.appendChild(testElement)
    docEl.appendChild(fakeBody)
    if (testElement.offsetHeight === 1) {
      docEl.classList.add('hairlines')
    }
    docEl.removeChild(fakeBody)
  }
}(window, document))

vw/vh (css3新增属性)

  • Viewport Height (vh): This unit is based on the height of the viewport.
  • Viewport Width (vw): This unit is based on the width of the viewport
  • 1vw: 1% of the viewport's width,即可视窗口宽度1%
  • 1vh: 1% of the viewport's height,即可视窗口高度1%
  • vmin:1% of the viewport's smaller dimension,即从vw和vh中取最小Math.min(vw, vh)
  • vmax: 1% of the viewport's larger dimension,即从vw和vh中取最大Math.max(vw, vh)

实现:

<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no" />
设计图尺寸:设计图元素尺寸 = 100vw:Y // Y 表示css样式中设计图某元素的大小, 单位vw

缺点:

  • 没有最大/最小限制, 当屏幕过于小,字体或内容太小,看不清,过大同理
  • 兼容性: ios8/andorid4.4 以上(ios6/7部分支持) 参考https://caniuse.com/#feat=viewport-units

vw + rem

优点:

  1. 实现简单,不依赖插件及第三方库,几行css代码就可以实现
  2. 开发方便, 方便换算
  3. 不影响px使用,完美兼容第三方组件库
  4. 不存在富文本和iframe等兼容问题

实现: 以下实现把屏幕平均分成10份,即屏幕宽10rem, 以rem与vw关系: rem:vw = 10:1 或者 1rem = 10vw为依据 100vw === 10 rem === document.documentElement 前提: <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no" />

  1. html 的 font-size 等于 1/10 的视口宽度 (即: 1rem = 1 / 10 * 100vw => 等同于lib-flexible 中 document.documentElement.style.fontSize = clientWidth / 10) [此处取1/10, 因为在淘宝方案也是取这个值, 为了更好计算可以去其他值]
// $vw_design: 设计图尺寸
// $vw_fontsize: 设计图尺寸 / 10  假设把设计图分为10份, 每份的大小(设计图的1rem), 并以此为基数
html {
  font-size: ($vw_fontsize / $vw_design) * 100vw; // 直接写 10vw

  // 同时,通过Media Queries 限制根元素最大最小值
  @media screen and (max-width: 320px) {
    font-size: 32px;
  }
  @media screen and (min-width: 540px) {
    font-size: 54px;
  }
}
  1. 计算使用scss函数: 设计图元素尺寸 / (设计图尺寸 / 10) * 1rem
// $basesize: 设计图元素尺寸
@function rem($basesize) {
  @return ($basesize / $vw_fontsize) * 1rem; 
}

//简化
@function rem($basesize) {
  @return ($basesize / $vw_design) * 10rem; 
}

计算原理:

假设设计图某元素所占视口大小为 X, 单位为: vw

设计图某元素尺寸 / 设计图尺寸 = (X)vw / 100vw 
=> (X)vw = (设计图元素尺寸 / 设计图尺寸) * 100vw 
// 转化为以rem单位的数值Y
由于 (Y)rem = (X)vw / 10 (平局分成10份,vw:rem = 10:1)
=> (Y)rem = (设计图元素尺寸 / 设计图尺寸) * 10vw 

观察最后推论,于是也可以按照以下理解:

$design_width: 750
$divideNum: 10 //屏幕平均分成份数
@function px2rem($design_dom) {
    @return #{$design_dom/$design_width * $divideNum}rem
}

html {
    font-size: (100 / $divideNum)vw;
    //同时,通过Media Queries 限制根元素最大最小值
    @media screen and (max-width: 320 ) {
        font-size: 32;
    }
    @media screen and (min-width: 640 ) {
        font-size: 64;
    }
}

参考: juejin.cn/post/684490…

rem + js

js 动态设置 html {font-size: (clientWidth / 屏幕平均分成份数)px}

$design_width: 750
@function px2rem($dom) {
    @return #{$dom/$design_width * 屏幕分成份数}rem
}

防止使用rem后,未设置font-size的元素继承使用根元素的font-zsize,重置body的font-size 为默认值(一般16px)或使用媒体查询字体响应式(320, 480, 640移动端尺寸)

body {
    font-size: 16px;
}

总结

响应式布局方案

缩放布局 用户体验 兼容性 依赖js 支持超大屏幕 需要修正字体
rem+media-query IOS4.1 AN2.1 ×
rem+js IOS4.1 AN2.1
rem+vw IOS6.1 AN4.4 ×
vw IOS6.1 AN4.4 × × ×

总结:

  1. rem兼容性:ios4.1/android2.1
  2. vw兼容性:ios6.1/android4.4
  3. 超大屏幕时,rem可通过media query控制font-size 最大/小值
  4. rem方案都需要修正body字体

其他

1px 实现

.hairlines li{
  height: 50px;
  line-height: 50px;
  border:none;
  text-align: center;
  position: relative;
  margin-top: 10px;
}
.hairlines li:after{
  content: '';
  position: absolute;
  left: 0;
  top: 0;
  border: 1px solid #cccccc;
  // border-radius: 26px;
  width: 200%;
  height: 200%;
  -webkit-transform: scale(0.5);
  transform: scale(0.5);
  -webkit-transform-origin: left top;
  transform-origin: left top;
}

百分比计算

  1. 子元素的height或width中使用百分比,是相对于子元素的直接父元素
  2. 子元素的top/bottom|left/right中使用百分比,是相对于直接非static定位(默认定位)的父元素的高度|宽度
  3. 子元素的padding/margin中使用百分比,是相对于子元素的直接父元素的宽度
  4. 子元素的border-radius/translate/background-size中使用百分比,是相对于自身的宽|高

参考: github.com/sunmaobin/s…