H5移动端采坑指南

1、1PX 问题;

问题来源:

由于不同的手机有不同的像素密度导致的。如果移动显示屏的分辨率始终是普通屏幕的2倍,1px设备独立像素的边框在DPR=2的移动显示屏下会显示成2px的设备像素,所以在高清屏幕下看着1px的视觉效果是变胖了而且颜色更深........

一些概念:

像素:
指在由一个数字序列表示的图像中的一个最小单元,单位是 px,不可再次分割了。
设备像素比(DPR):
设备像素比 = 设备像素 / 设备独立像素。
复制代码

**像素:**即css中采用的逻辑像素; **设备像素:**即物理像素,和屏幕尺寸有关,固定不变,最终显示的单位,屏幕上的一个个点;

**设备独立像素:**计算机坐标系统中得一个点;这个点代表一个可以由程序使用的虚拟像素(比如: CSS 像素),这个点是没有固定大小的,越小越清晰,然后由相关系统转换为物理像素。

解决方案:

采用 css 的伪元素 + transform: scale() 缩放实现;因为伪元素::after或::before是独立于当前元素,可以单独对其缩放而不影响元素本身的缩放;

方案一:
<!--全部边框-->
.border-1px:after {
    content: '';
    position: absolute;
    box-sizing: border-box;
    top: 0;
    left: 0;
    width: 200%;
    height: 200%;
    border: 1px solid #000;
    border-radius: 4px;
    -webkit-transform: scale(0.5);
    transform: scale(0.5);
    -webkit-transform-origin: top left;
}
<!--单边框,以上边框为例-->
.border-1px-top:before {
    content: "";
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    border-top: 1PX solid red;
    transform: scaleY(.5);
    transform-origin: left top;
}
方案二:封装方案一,并针对不同的DPR进行媒体查询来优化缩放;
.border(
    @borderWidth: 1PX;
    @borderStyle: solid; 
    @borderColor: @lignt-gray-color; 
    @borderRadius: 0) {
    position: relative;
    &:before {
        content: '';
        position: absolute;
        width: 98%;
        height: 98%;
        top: 0;
        left: 0;
        transform-origin: left top;
        -webkit-transform-origin: left top;
        box-sizing: border-box;
        pointer-events: none;
    }
    @media (-webkit-min-device-pixel-ratio: 2) {
        &:before {
            width: 200%;
            height: 200%;
            -webkit-transform: scale(.5);
        }
    }
    @media (-webkit-min-device-pixel-ratio: 2.5) {
        &:before {
            width: 250%;
            height: 250%;
            -webkit-transform: scale(.4);
        }
    }
    @media (-webkit-min-device-pixel-ratio: 2.75) {
        &:before {
            width: 275%;
            height: 275%;
            -webkit-transform: scale(1 / 2.75);
        }
    }
    @media (-webkit-min-device-pixel-ratio: 3) {
        &:before {
            width: 300%;
            height: 300%;
            transform: scale(1 / 3);
            -webkit-transform: scale(1 / 3);
        }
    }
    .border-radius(@borderRadius);
    &:before {
        border-width: @borderWidth;
        border-style: @borderStyle;
        border-color: @borderColor;
    }
}

.border-all(
	@borderWidth: 1PX@borderStyle: solid; 
	@borderColor: @lignt-gray-color@borderRadius: 0) {
    .border(@borderWidth; @borderStyle; @borderColor; @borderRadius);
}
注意:这里 PX 大写,为了防止插件将 px 转成 rem 等单位
UI框架 vant-uicube-ui 采用的都是第一种方案;
方案三:box-shadow模拟边框;
.box-shadow-1px {
  box-shadow: inset 0px -1px 1px -1px #c8c7cc;
}
复制代码

2、input 相关问题

(1)type=number 兼容性及数字键盘唤醒;

<input class="num_input" type='number' pattern="[0-9]*"/>在移动端,还要区分是安卓用户,还是ios用户,所以这样写:
在安卓端设置input类型为number,可限制键盘只输入数字;而在ios端,要加入pattern验证输入字段的模式,才能限制数字输入。
复制代码
<input type="tel">
复制代码
<input pattern="\d*">
复制代码

(2)input 去除默认样式的问题:

input { 
  outline: none;
  border: none;
  background: none;
  -webkit-appearance: none; // 解决 ios 上的阴影
}
复制代码

3、input 聚焦(focus)之后页面自动放大问题;

MDN介绍:

meta 元标签标准中的 viewport 属性,用来控制页面的缩放:

图片

一般情况:

// 设置meta 信息 user-scalable=0 禁止用户缩放页面
<meta name="viewport" 
content="initial-scale=1.0,maximum-scale=1.0,minimum-scale=1.0,user-scalable=0,width=device-width" />
// 苹果手机设置
<meta name="apple-mobile-web-app-capable" content="yes" />
复制代码

字体小于16px情况

导致页面自动缩放的问题来源于,ios在小屏设备(如iphone 5s/6/6s/7/8...)上点击输入框的时候,如果input字体小于16px的时候会自动放大页面,提升阅读性。但实际不同场景下,我们的ui设计字体可能低于16px。

解决方案:
监听input聚焦/失焦事件,把input的字体设置为16px,或者''切换;
复制代码

4、经典sticky footer布局方式;

需求:

整体布局为header(页头)部分,content(内容区)部分和footer(页脚)部分。当header和content的内容较少时,footer不是挨着content排布而是始终显示在屏幕的最下方。当content的内容较多时,footer能随着content区文档流撑开始终显示在页面的最下方。

实现方案

(1)负margin布局方式
.wrapper{
  min-height:100%;
  width:100%;
}
.content{
  margin-top:64px;
  padding-bottom:64px;
}
.footer{
  margin:-64px auto 0 auto;
  font-size:32px;
}
精髓:main里的 padding-bottomfooter里的负margin值要保持一致;或者更大;且父盒子最小高度设置为100%或者100vh;
(2)flex 布局
.wrapper{
  display: flex; 
  flex-flow: column; 
  min-height: 100vh; 
  overflow:auto;
}
.content{ flex:1; }
.footer{ flex: 0; }
复制代码

5、微信页面软键盘弹起后,页面整体被顶上去,然而键盘收起后,页面无法自动回落到底部;

问题复现:

我问题复现环境:IOS v13.3.1 微信 v7.0.12;安卓微信及浏览器均没发现此问题;

图片图片

解决方案:

解决方案一:

微信开发者回复: 在点击按钮那里加下面其中一个即可解决

// 滚动到顶部

window.scrollTo(0, 0); // 可以加上 setTimeout
复制代码

// 滚动到底部

setTimeout(() => {
  window.scrollTo(0document.documentElement.clientHeight);
}, 100);
复制代码

解决方案二:

在input失去焦点的时候用代码控制页面滑动一下可以修复此问题;

document.body.scrollTop = 0;
复制代码

6、iOS 滑动不流畅

问题复现:

上下滑动页面会产生卡顿,手指离开页面,页面立即停止运动。整体表现就是滑动不流畅,没有滑动惯性。

定位问题:

iOS 5.0 + 版本,滑动有定义有两个值 auto 和 touch,默认值为 auto;

-webkit-overflow-scrolling: touch; /* 当手指从触摸屏上移开,会保持一段时间的滚动 */
-webkit-overflow-scrolling: auto; /* 当手指从触摸屏上移开,滚动会立即停止 */
复制代码

解决方案:

// 外部 overflow 为 hidden,设置内容元素 overflow 为 auto且-webkit-overflow-scrolling 值设置为 touch;
body {
  overflow-y: hidden;
}
.wrapper {
  -webkit-overflow-scrolling: touch;
  overflow-y: auto;
}
.wrapper::-webkit-scrollbar { display: none; } // 隐藏滚动条
复制代码

7、click 点击事件延时与穿透

定位问题:

为什么会产生 click 延时?

iOS 中的 safari,为了实现双击缩放操作,在单击 300ms 之后,如果未进行第二次点击,则执行 click 单击操作。也就是说来判断用户行为是否为双击产生的。但是,在 App 中,无论是否需要双击缩放这种行为,click 单击都会产生 300ms 延迟。

为什么会产生 click 点击穿透?

双层元素叠加时,在上层元素上绑定 touch 事件,下层元素绑定 click 事件。由于 click 发生在 touch 之后(事件触发顺序: touchstart, touchmove, touchend, click。),点击上层元素,元素消失,下层元素会触发 click 事件,由此产生了点击穿透的效果。

解决方案:

使用 touchstart 替换 click;注意:同时需要点击和滑动的场景下,建议还是绑定 click 事件;

8、移动端H5响应式布局

前端多端的分类:

图片

响应式布局的基础——相对单位:

图片

方案一:rem + px => rem

原理:

监听屏幕视窗的宽度,通过一定比例换算赋值给html的font-size。此时,根字体大小就会随屏幕宽度而变化。

将 px 转换成 rem, 常规方案有两种:

  • 一种是利用sass/less中的自定义函数 pxToRem,写px时,利用pxToRem函数转换成 rem。
  • 另外一种是直接写px,编译过程利用插件全部转成rem。这样 dom 中元素的大小,就会随屏幕宽度变化而变化了。

实现:

pxToRem 函数方法:

1.0 动态更新根字体大小

const MAX_FONT_SIZE = 420
// 定义最大的屏幕宽度
document.addEventListener('DOMContentLoaded', () => {
  const html = document.querySelector('html')
  let fontSize = window.innerWidth / 10
  fontSize = fontSize > MAX_FONT_SIZE ? MAX_FONT_SIZE : fontSize
  html.style.fontSize = fontSize + 'px'
})
复制代码

2.0 px 转rem

方式(1):定义转换函数
$rootFontSize: 375 / 10;
// 定义 px 转化为 rem 的函数
@function px2rem ($px) {
    @return $px / $rootFontSize + rem;
}
// 调用
.demo {
    width: px2rem(100);
    height: px2rem(100);
}
方式(2):利用脚手架打包插件
例如 vue-cli3 中的 postcss-pxtorem(https://github.com/cuth/postcss-pxtorem/blob/master/index.js)
const autoprefixer = require('autoprefixer')
const pxtorem = require('postcss-pxtorem')
module.exports = { 
  // ...
  css: {
    sourceMap: true,
    loaderOptions: {
      postcss: {
        plugins: [
          autoprefixer(),
          pxtorem({
            rootValue: 37.5,
            propList: ['*']
          })
        ]
      }
    }
  }
}
注意: 1px 不会转换;
复制代码

构建阶段的插件法:

实际使用参考 vant-UI 的 rem 适配方案:

youzan.github.io/vant/#/zh-C…

  • postcss-pxtorem 是一款 postcss 插件,用于将单位转化为 rem;
  • lib-flexible 用于动态设置 rem 基准值;

兼容性:

移动端兼容性100%完美;

方案二:vh + vw(主流)

原理:

vw 相对于视窗宽度的单位,随宽度变化而变化。由此看来,方案一其实是方案二的一种 "Hack", 通过使用监听实现了方案二的效果。

实现:

与 rem 类似做法,直接使用postcss-px-to-viewport插件进行配置, 配置方式也是和  postcss-pxtorem 大同小异。

兼容性:

iOS Safari 低于5.1 版本兼容性堪忧,其他完美;

9、移动端安卓文字不能垂直居中而始终偏上

图片

问题:

使用 height 和 line-height 相等的值,在 IOS 完美居中,可是在安卓上有的则显示偏上;

方案:

行内弹性布局:

display: inline-flex;
align-items: center;
just-content: center;
复制代码

10、禁止移动端自动识别 电话 及 邮箱 文本

<meta name="format-detection" content="telephone=no">
复制代码

11、打开原生应用

用法

<a href="weixin://">打开微信</a>
<a href="alipays://">打开支付宝</a>
<a href="alipays://platformapi/startapp?saId=10000007">打开支付宝的扫一扫功能</a>
<a href="alipays://platformapi/startapp?appId=60000002">打开支付宝的蚂蚁森林</a>
复制代码

这种方式叫做URL Scheme,是一种协议,一般用来访问APP或者APP中的某个功能/页面(如唤醒APP后打开指定页面或者使用某些功能)😄;

常用 URL Scheme

微信 weixin://

(扫一扫) weixin://scanqrcode


淘宝 taobao://

(搜索) taobao://s.taobao.com/?q=裙子

支付宝 alipay://

(付款) alipayqr://platformapi/startapp?saId=20000056

(扫一扫) alipayqr://platformapi/startapp?saId=10000007


QQ mqq://


打电话tel://110


知乎 zhihu://

(某篇文章)zhihu://articles/27758684

(某个专栏)zhihu://columns/lishuai


高德地图 iosamap://

(公交导航) iosamap://path?sourceApplication=日历&sid=BGVIS1&sname=$我的位置&did=BGVIS2&dname=目的地&dlat=纬度&dlon=经度&dev=0&m=0&t=0

// t为出行方式:0 驾车,1 公交,2 步行 3 骑行

// m为出行要求 当t为驾车时:0速度最快,1费用最少,2距离最短,3不走高速,4躲避拥堵,5不走高速且避免收费,6不走高速且躲避拥堵,7躲避收费和拥堵,8不走高速躲避收费和拥堵

当t为公交时:0最快捷,2最少换乘,3最少步行,5不乘地铁 ,7只坐地铁 ,8时间短


百度地图 baidumap://

(导航)baidumap://map/direction?origin=我的位置&destination=公司&mode=driving

// mode 导航模式,固定为transit、driving、walking、riding


腾讯地图 qqmap://

(导航)qqmap://map/routeplan?from=我的位置&type=drive&tocoord=36.547901,104.258354&to=dest&coord_type=1&policy=0

type 导航模式:公交:bus 驾车:drive 步行:walk(仅适用移动端) 骑行bike;

12、禁止长按默认行为

不同端的长按行为一般有:长按图片保存、长按选择文字、长按链接/手机号/邮箱时呼出菜单;

// 禁止长按图片保存
img {
  -webkit-touch-callout: none;
  pointer-events: none; // 像微信浏览器还是无法禁止,加上这行样式即可
}
// 禁止长按选择文字
div {
  -webkit-user-select: none;
}
// 禁止长按呼出菜单
div {
  -webkit-touch-callout: none;
}
复制代码

13、滑动穿透

问题描述:

弹出遮罩时,禁用 遮罩滚动,但是 在遮罩上 触摸时,行为穿透到了下面引起底层的滚动的问题;

图片

解决方案:

阻止遮罩层 mask 的 默认行为:

// JS 
document.querySelector(".mask").addEventListener("touchmove", event => {
  event.preventDefault();
});
// vue中,你可以这么写:
<div class="mask" @touchumove.prevent>
  <div calss="content">弹层内容</div>
</div>
复制代码

如果弹层内容 .content 自己有滚动条则只需要禁用 mask 默认行为即可:

// JS
document.querySelector(".mask").addEventListener("touchmove", event => {
  if (event.target.classList.contains("mask")) event.preventDefault();
});
// vue
<div class="mask" @touchumove.self.prevent></div>
复制代码

14、移动端容器内点击返回时强制页面刷新方案

图片

场景:

返回后页面不刷新(默认从缓存中读取),一些失效的信息依然显示在页面上;这个问题在 iphone 手机上会出现,所以我们需要监听页面并强制刷新;在Android 手机上返回时会自动刷新。

解决方案:

(1)用onpageshow事件监听在用户浏览网页时触发相关事件;pagehide 是在不显示的时候触发;

window.location.reload 此方法会导致整个页面刷新重新渲染,用户有感知;

window.addEventListener('pageshow', function(event) {
    if(event.persisted){
        window.location.reload(true); // 页面重新初始化加载
    }
});
复制代码

(2)vue 中通过控制路由的显隐实现无感刷新;

App.vue

<router-view v-if="isRouterShow"></router-view>
provide() {
  return {
    reload: this.reload,
  };
},
# data
isRouterShowtrue,
# methods
// 强制刷新路由函数(无感刷新)
  async reload() {
    console.log("---刷新路由---");
    this.isRouterShow = false;
    await this.$nextTick();
    this.isRouterShow = true;
  },
复制代码

子组件中调用:

inject: ["reload"],
# methods
_this.reload(); // 再需要的地方调用 App.vue 中定义的 reload 函数即可
复制代码

15、IOS 及 Android 端判断方法的兼容性写法;

let u = navigator.userAgent;
# 判断 Android
(1)
u.indexOf('Android') > -1 || u.indexOf('Adr') > -1; //android终端
(2)
/(Android)/i.test(navigator.userAgent) // android终端
复制代码

#判断 ios 不要用下面的方法!!!在 ios12.0 以上的系统会有兼容性问题;

var isiOS = !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/); //ios终端
复制代码

而用

var isiOS = /(iPhone|iPad|iPod|iOS)/i.test(u)
复制代码

16、Video 相关问题

Video.js 踩坑

17、一些特殊场景的开发需求

1.0 路由切换动画(转场效果)的实现

可用项目:

1.0vue-page-stack(借鉴了 vue-navigation 项目)

2.0 vue-navigation

以上两个 转场效果都几乎接近移动端原生效果。

2.0 新手引导页的实现

可用项目:

vue-introjs

3.0 开屏引导页(横向轮播)的实现

一般UI框架自带 swiper 组件,如 vant-ui 的 Swipe 轮播,直接使用即可;

也可用vue-fullpage实现。

18、 文档在线预览方案汇总

1.0 纯前端 实现(不是在公网预览):

PDF 预览:Vue-pdf(支持分页,懒加载展示)

Excel:SheetJS

Word:mammoth.js

2.0 后端实现:

2.1 私有域部署方案:

kkFileView解决方案:支持doc,docx,xls,xlsx,ppt,pptx,pdf,txt,zip,rar,图片,视频,音频;

2.2 公网方案:

微软提供的在线预览、XDOC文档预览公网预览方案一般过不了安全测试,因为文档是私有域访问的。

https://view.officeapps.live.com/op/view.aspx?src=文件地址
https://view.xdocin.com/xdoc?_xdoc=你的文档地址
复制代码

19、前端接收后端二进制流下载文件的实现

(1)场景:

移动端使用动态创建 a 链接请求文件流并下载文件时,默认是预览而不支持下载,以及下载后的文件无后缀名,无法正常打开的问题。

后端书写接口时候需要注意 HTTP Response Header 中三个属性:Content-type、Content-Disposition、Content-Length;

  • Content-type用于定义网络文件的类型和网页的编码,决定浏览器将以什么形式、什么编码读取这个文件;(详见MIME 对照表
  • Content-Disposition 用于控制用户请求所得的内容存为一个文件的时候,提供一个默认的文件名,并且弹窗提示文件直接在浏览器上显示还是下载(保存);
  • Content-Length 用于描述HTTP消息实体的传输长度响应头。

(2)字段介绍如下:

Content-Disposition属性有两种类型:inline 和 attachment:

inline :将文件内容直接显示在页面(预览);
attachment:弹出对话框让用户下载(保存)
复制代码

(3)具体代码实现:

后端代码示例(文件流接口的实现):

// HTTP 请求头查看效果:
Content-Type: image/jpeg  
Content-Disposition: inline;filename=hello.jpg  
Content-Description: just a small picture of me  
复制代码

A.在页面内打开(预览)java 代码:

File file = new File("rfc1806.txt");  
String filename = file.getName();  
response.setHeader("Content-Type","text/plain");  
response.addHeader("Content-Disposition","inline;filename=" + new String(filename.getBytes(),"utf-8"));  
response.addHeader("Content-Length","" + file.length());  
复制代码

B.弹出保存(点击下载)框 java 代码:

File file = new File("rfc1806.txt");  
String filename = file.getName();  
response.setHeader("Content-Type","text/plain");  
response.addHeader("Content-Disposition","attachment;filename=" + new String(filename.getBytes(),"utf-8"));  
response.addHeader("Content-Length","" + file.length()); 
复制代码

核心就是 保证Content-Type****类型与Content-Disposition****中指定的文件名 后缀类型保持一致;

前端代码示例(两种实现方式—创建 a 链接地址):

A. 直接使用 标签来接收后端的get请求文件流:

let link = document.createElement("a");
link.style.display = "none";
// !!!注意 '/api/api.pdf?id=' 是接口地址,必须用 .pdf 作为接口后缀
link.href = `${url}/api/api.pdf?id=${文件存储 id}`;
link.setAttribute("download", "filename.pdf");
document.body.appendChild(link);
link.click();
复制代码

B. 将后端post请求返回的文件二进制流创建 Blob 对象,再用 链接请求 Blob;

  • 当文件流是通过post请求返回时,前端必须创建 blob 对象来进行接收;
  • **前端请求时,请求体最好加上{ responseType:'blob' }**这行代码,防止返回的流乱码;
$axios.post("/api/xxx/xxx/xxx",
this.$qs.stringify({range:0,}), {responseType:'blob'})
.then(msg=>{
  // 表示一个指定的 file 对象或 Blob 对象
  let url = window.URL.createObjectURL(msg.data); 
  let a = document.createElement("a"); 
  document.body.appendChild(a);
  let fileName=msg.headers["content-disposition"].split(";")[1].split("=")[1];  
  //filename名称截取
  a.href = url;
  a.download = fileName; //命名下载名称
  a.click(); //点击触发下载  
  window.URL.revokeObjectURL(url);  //下载完成进行释放
})
复制代码
分类:
前端
标签: