Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

移动端适配总结(长文) - 掘金 #27

Open
xiaodongxier opened this issue Jun 15, 2020 · 0 comments
Open

移动端适配总结(长文) - 掘金 #27

xiaodongxier opened this issue Jun 15, 2020 · 0 comments

Comments

@xiaodongxier
Copy link
Owner

布局

  • 静态布局: 定宽定高布局, 不随设备和视口改变【定宽居中, 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);
    

    复制代码

  2. 使用 flexible.js
    github.com/amfe/lib-fl…

缺点:

  1. iframe 问题

  2. 富文本问题

  3. 高清方案

  4. 部分 android 机型不兼容

  5. 系统字体缩放时, 发生变化, 导致页面错乱,因为和根元素字体紧密联系;解决方案: 缩放还原
    1> juejin.im/post/59f678…
    2> 基于 flexible.js 添加以下代码: juejin.im/post/5b9cb9…

    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.im/post/5c807802f265da2de33f4e1f
    // 页面从浏览器的缓存中读取该属性返回 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;
    }
    }
    复制代码

  2. 计算使用 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 
}

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.im/post/5c0357…

rem + js

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

$design_width: 750
@function px2rem($dom) {
    @return 
}
复制代码

防止使用 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 
  // 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…
https://juejin.im/post/5ee5e5cd51882557525a8d69?utm_source=gold_browser_extension

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant