移动端常见问题

移动端常见问题

Posted by limantang on November 24, 2019

移动端常见问题

设备像素、设备独立像素、CSS像素、分辨率、PPI、devicePixelRatio

物理像素(设备像素)(device pixels)

设备像素也称为物理像素, 是显示器的最小单位, 这个像素不一定是一个小圆点或者小正方形, 只是用于显示丰富色彩的一个点, 一个物理像素有三原色(红,蓝,绿)不同变换显示不同的色彩

设备独立像素(device independent pixels)

平时所说的2560 * 1600的分辨率不适合玩游戏, 需要改成1080 * 720, 这里的分辨率(不严谨)就是指设备独立像素, 这个可以通过window.screen.width window.screen.height查看, 一个设备独立像素可能包含多了物理像素点, 包含的越多看起来越清晰

像素分辨率

以手机屏幕为例, IPhoneX像素分辨率是1125 * 2436, 是指屏幕横向能显示1125个物理像素点, 纵向能显示2436个物理像素点 通常所说的4K屏幕指的是4096 * 2160

PPI(pix per inch)

每英寸的物理像素数, 以5.8英寸为例(屏幕对角线长度), 分辨率为1125*2436的iPhone X为例, ppi = Math.sqrt(1125*1125 + 2436*2436) / 5.8

CSS像素

浏览器使用的单位, 用来衡量网页的内容, 比如div{width: 100px}, 一般情况下(缩放比为1), 一个CSS像素等于一个设备独立像素, 假如屏幕的设备独立像素是1440*960, 如果页面的宽度是720ox, 则视觉上页面的宽度是屏幕的一半 这也解释了为什么把设备独立像素调高之后感觉网页变小了 当页面的缩放比不是1的时候, CSS像素设备独立像素不在以1比1对应, 比如当页面放大200%的时候(浏览器页面缩放), 1个CSS像素对应4个设备独立像素, 因为长宽都放大了2倍, 面积放大了4倍

devicePixelRatio

window.devicePixelRatio是指物理像素设备独立像素的比例 window.devicePixelRatio = 物理像素 / 设备独立像素 iPhone X的是3

viewport

尺寸区别

  • 屏幕尺寸

这里获取到的是设备独立像素 window.screen.widthwindow.screen.height

  • 窗口尺寸(包含横向竖向滚动条)

window.innerWidth

window.innerHeight 包含了横向竖向的滚动条

  • viewport(不包含横向竖向滚动条)

document.documentElement.clientWidth

document.documentElement.clientHeight 获取视口宽高 不包含横向竖向的滚动条

  • HTML尺寸

document.documentElement.offsetWidth

document.documentElement.offsetHeight 整个页面内容的宽高

  • 整个浏览器应用尺寸

window.outerHeight

window.outerWidth 包含了标签栏书签栏的整个浏览器的尺寸

visual viewport & layout viewport

移动端的visual viewport和layout viewport

手机屏幕一般都很窄, 一般不会超过400px 比如网站的侧边栏宽度都设置为10%, 这在PC浏览器(假设宽度为1000px)看起来没什么问题, 但是在手机上(假设为400px)只有40px

可以把layout viewport想象成一个很大的照片, visual viewport想象成一个小窗口, 小窗口在照片上移动时, 可以透过小窗口visual viewport看到照片 layout view的一部分

  • layout viewport是页面渲染时的参考宽高
  • visual viewport是移动端视口的设备独立像素

在移动端渲染时,layout viewport默认就是980px, 所以页面元素渲染时就认为窗口(vasul viewport)的宽度也是980px

layout viewport

document.documentElement.clientWidth

document.documentElement.clientHeight

vasul viewport

window.screen.width

window.screen.height

viewport设置

假设页面没有设置viewport, 对于大部分浏览器来说, 页面会以layout viewport宽度去渲染, 然后页面就会被缩放到正好被屏幕容纳

部分浏览器也有可能不去自动缩放, 用户需要左滑右滑才能看到所有内容

这时添加一句 <meta name="viewport” content=“width=device-width”> 其中device-width就是设备独立像素 其实这句话的作用就是让 layout viewport 等于 vasul viewport 也就是 文档的宽度等于设备屏幕宽度

完整的viewport标签 <meta name="viewport” content=“width=device-width, minimum-scale=1, initial-scale=1, maximum-scale=1”>

含义: 手机设备宽度(设备独立像素window.screen.width)等于文档宽度(document.documentElement.clientWidth)

移动端的几种适配方案

适配原则

  1. 开发时方便
  2. 适配的设备类型多
  3. 让用户无不适感

思路1

以原型稿的宽度和标准开发页面,在手机上整个页面内容等比放大或者缩小填充手机屏幕宽度

思路2

以原型稿的宽度和标准开发页面,在手机上部分内容根据屏幕宽度等比放大或者缩小,而部分内容不变或者按需受控变化 需要随屏幕宽度等比缩放的元素用相对单位 rem或者vw,不需要随屏幕宽度缩放的元素用固定单位px

思路3

固定尺寸+弹性布局,无需放大缩小

viewport适配方案

前面说过了如果不设置viewport 移动端设备会默认页面的宽度是980px 而不是认为页面宽度是设备屏幕宽度

假设设计稿的宽度是750px 以IPhone X的屏幕宽度375px为准

可以以这样的思路做适配 就按照设计搞的标注的像素大小去开发页面 然后将页面整体缩小一倍 就满足了要求 为什么缩小一倍呢因为,我们按照750px的设计标准去开发 但是屏幕的实际宽度是375px, 750/375=2
所以要缩小一倍 在html添加这句即可 <meta name="viewport” content=“width=750, initial-scale=0.5”>

这是以IPhone X为例, 如果要适配其他机型, 需要动态计算, 因为不同的移动设备,屏幕宽度可能不同

const WIDTH = 750;
const mobileScale = () => {
	let scale = window.screen/WIDTH;
	let content = `width=${WIDTH}, initial-scal=${scale}`;
	let meta = document.querySelector(`meta[name=viewport]`);
	if (!meta) {
		meta = document.createElement(`meta`);
		meta.setAttribute(`name`m `viewport`);
		ducument.head.appendChild(meta);
	}
}
//		动态执行这个mobileScale函数即可

这样做的优点: 可以满足大部分要求, 原理简单, 开发方便

缺点: 会导致整个页面全部元素的缩小, 这不是我们想要的, 我们可能想要一部分元素是不缩放的(border边框)

vm适配方案

前置知识: 100vw对应屏幕宽度

假设设计稿尺寸为750px 首先要对设计稿的标准进行转换 按照750px对应100vw来转换 那么75px也就是对应10vw

首先还是需要设置viewport <meta name="viewport” content=“width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1”>

然后使用JS设置自定义属性--width, 对应的是把设计稿分成100份之后每一份的像素大小

const WIDTH = 750;
document.documentElement.style.setAttribute(`—widht`, (100/WIDTH))

再根据设计稿的标记进行开发, 比如设计稿显示的一个大小是20px 这里就可以写成calc(20px * var(—width)) 如果是不需要缩放的元素, 比如一个元素的border边框, 那么久直接使用px单位即可, 不需要转换border-bottom: 1px solid #ddd

如果使用了less或者sass这样的css预处理器 则可以定义这样的全局转换函数

@function px2vw($px) {
  @return $px * 1vw / 100;
}

这样做的优点: 可以任意控制元素的缩放或者不缩放, 而不是所有的元素都缩放 缺点: 兼容性可能不好, 书写麻烦(有插件可以解决)

动态rem适配方案

首先设置 <meta name="viewport” content=“width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1”> 把layout viewport的宽度设置为设备宽度

假设设计稿的宽度等于设备宽度, 当我们设置html的fontSize为100px时 那么对应设计稿的20px也就等同于0.2rem 屏幕宽度/设备宽度 * 20px = rem * 0.2 等同于 1/1 * 20px = 100 * 0.2

如果屏幕宽度和设计稿宽度不相同,我们还想要0.2rm代表20px 那么我们只需要让html的fontSize为100*屏幕宽度/设计稿宽度 其实结果很容易按照上面的公式推算出来 屏幕宽度/设备宽度 * 20 * 5 = rem * 0.2 * 5 等同于 屏幕宽度/设备宽度 * 100 = rem rem就算出来了了

为什么选用100这个数值 首先是因为计算方法, 方便记忆 对于浏览器来说fontSize是有大小要求的, 一般情况最小是12px

使用sass等css预处理器可以使用函数转换一下

@function px2rem($px) {
  @return $px * 1rem / 100;
}

其实vm适配方案动态rem适配方案原理很像, 都是通过找到一个基准值, 按照一定的比例计算出新的计算像素的单位

Flex弹性盒子适配方案

具体就不描述了, 可以看看flex布局的使用, 大概就能理解这种适配方案了

以上前三种适配方案都是基于宽度来适配的

300ms延迟

首先看如下代码, 建议复制到浏览器,打开开发者工具, 设置成移动端调试模式测试一下

<!DOCTYPE html>
<html>
<head>
  <meta name="description" content="300ms延迟演示,手机上打开连接,注释掉viewport" />
  <meta charset="utf-8">
  <title>300ms延迟</title>
<!--  <meta name="viewport" content="width=device-width">-->
</head>
<body>
  <div id="delay">click有延迟 </div>
  <div id="no-delay">touchstart无延迟</div>
  <div> <a id="link1" href="#1">链接1</a> <a id="link2" href="#2">链接1</a></div>
  <div id="log"></div>

  <style>
    body {
      font-size: 60px;
    }
  </style>

  <script>
    const $ = s => document.querySelector(s)
    const log = str => $('#log').innerText = str;

    let t1, t2

    $('#delay').ontouchstart = e => {
      t1 = Date.now()
    }

    $('#delay').onclick = e => {
      log(Date.now() - t1)
    }

    $('#no-delay').ontouchstart = e => {
       e.preventDefault()
       log('touchstart无延迟')
    }

    $('#link1').ontouchstart = e => {
      t2 = Date.now()
    }

    $('#link2').ontouchstart = e => {
      t2 = Date.now()
    }

    window.onhashchange = () => {
      log(`link: ${Date.now() - t2}ms`)
    }
  </script>
</body>
</html>

通过onclick绑定的事件,触发时大约都有300ms左右的延迟, 其实我们点击的时候肯定是没有这么长的延迟时间的, 那么长达300ms的延迟是如何产生的呢 当在移动端点击一个元素的时候首先触发touchstart然后可能会触发touchmove然后触发touchend最后触发click这时移动端一次点击事件大概的流程 由于在移动端阅读网页时, 可能会出现放大页面这种需求, 所以移动端的浏览器都添加了双击放大的功能 当手指在屏幕上快速的点击两次,页面就会放大 那么如何判定是快速连续点击了两次了 假定用户点击300ms内又有第二次点击, 那么就认为这两次点击是快速的点击两次而不是普通的点击, 就会放大页面 所以当用户点击一个跳转连接的时候, 浏览器就会在300ms之后决定是否要跳转连接还是缩放页面

如果解决这种问题呢 两种办法

  1. 设置meta

添加<meta name="viewport” content=“width=device-width”> IOS, Android都生效

  1. fastclick库, 或者使用touchstart

touchstart不推荐使用, 因为很可能带来一些不想要的效果

实现一个简单的fastclick 原理: 当检测到touchend事件的时候, 就模拟一个click事件派发出去, 然后将原本的click禁止掉, 代码如下:

const fastClick = (function () {
  function attach(root) {
    let targetEle = null;
    root.addEventListener('touchstart', (e) => {
      targetEle = e.target;
    });
    root.addEventListener('touchend', (e) => {
      e.preventDefault();
      let touch = e.changedTouches[0];
      let clickEvent = document.createEvent('MouseEvents');
      clickEvent.initMouseEvent('click', true, window, 1, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false, false, false, false, 0, null)
      clickEvent.forwardedTouchEvent = true
      targetElement.dispatchEvent(clickEvent)
    })
  }
  return { attach }
})();
fastClick.attach(document.body);

源码地址

blog-source-code/index.html at master · echoheart/blog-source-code · GitHub

点击穿透现象及规避方式

什么是点击穿透现象 在移动端, 当用户通过touchstart事件监听函数让浮层关闭, 关闭浮层后对应位置的页面其他元素也被点击了, 比如浮层下面是一个跳转链接, 当用户点击浮层关闭按钮之后大约300ms页面同时发生了跳转

代码如下

<!DOCTYPE html>
<html>
<head>
  <meta charset=“utf-8”>
  <title>tap击穿</title>

  <style>
    body {
      margin: 0;
      padding: 30px;
      font-size: 60px;
    }
    .mask {
      position: absolute;
      top: 0;
      right: 0;
      bottom: 0;
      left: 0;
      background: rgba(0,0,0,0.8);
      color: #fff;
      padding: 30px;

    }
    .close {
      background: red;
      padding: 10px;
    }
  </style>
</head>
<body>
<!--  <a href=“https://baidu.com”>百度</a>-->
  <span class=“bg”>背景</span>
  <p class=“log”>0</p>
  <div class=“mask”>
    <span class=“close”>X</span>
  </div>

  <script>
    const $ = s => document.querySelector(s)
    const log = str => $('.log').innerText = str

    let i = 1

    $('.close').ontouchstart = (e) => {
      $('.mask').style.display = 'none';
      log('touched' + (++i))
    }

    $('.bg').onclick = () => {
      log('点击了背景')
    }
  </script>

</body>
</html>

原因分析: - 手指触摸屏幕发生了touchstart事件 - 手指在屏幕短暂的停留,如果移动了触发touchmove事件 - 手指离开屏幕触发touchend - 等待约300ms,浏览器判断有没有第二次点击, 如果有就缩放页面 - 最后触发click事件 点击穿透的根源在于第四点, 当用户手指离开屏幕,大约300ms左右, 这时遮罩已经关闭,这时触发click事件, 但是此时遮罩已经关闭了, 自然就会点击了后面的元素了

解决办法: 设置<meta name="viewport” content=“device-width”>是不行的, 就算不是300ms以后, 就算是100ms以后遮罩还是不在了, 还是会在当前位置触发click, 自然还是会点击到后面的元素

  1. 不用touchstart, 而是改用click事件来监听浮层关闭
  2. 在touchstart中阻止默认事件, 组织click触发
  3. 添加延时以及动画, 在300ms后在真正关闭浮层(效果一般, 最好不要用)

1px实现

当我们在css里写 border: 1px solid #000的时候,用户会觉得边线依然很粗不美观

解决方案:

  • 0.5px IOS有效, Andriod无效
  • 伪元素设置:transform: scaleY(0.5), 只适合的border一条边
  • 使用背景渐变,一半是正常border颜色, 另一半透明颜色, 看起来边框做没有那么粗了 background: linear-gradient(to bottom, #ccc .5px, transparent 0)

以上方案还是无法解决圆角问题…

  • 使用viewport缩放, viewport缩小整个页面, 自然也包括border

参考:

A tale of two viewports

在移动浏览器中使用viewport元标签控制布局