likes
comments
collection
share

移动端H5开发-常用技巧篇

作者站长头像
站长
· 阅读数 38

引言

笔者是一名前端开发工程师,大部分开发工作都是在 H5 的世界里遨游。多年的 H5 开发经验总结了一些常用技巧,在这里分享给大家,希望在遇到同类问题时能给你提供思路和灵感,保护日益稀疏的头发。

HTML 相关

meta 标签

meta 元素可提供有关页面的元信息。下面是一些与移动端 H5 相关比较特殊的 meta 标签

<meta name="screen-orientation" content="portrait" />            // 禁止屏幕旋转
<meta name="full-screen" content="yes" />                        // 全屏显示
<meta name="browsermode" content="application" />                // UC应用模式,页面默认全屏
<meta name="x5-orientation" content="portrait" />                // QQ强制竖屏
<meta name="x5-fullscreen" content="true" />                     // QQ强制全屏
<meta name="x5-page-mode" content="app" />                       // QQ应用模式
<meta name="renderer" content="webkit" />                        // 启用360浏览器的极速模式
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" /> // 优先使用 IE 最新版本和 Chrome

语义化标签

HTML5 语义化标签能够让页面的结构更清晰,有利于 SEO,并且能够帮助辅助技术更好的阅读和转译你的网页。推荐使用语义化标签编写 HTML

<header></header>         // 定义文档的头部区域
<nav></nav>               // 定义导航链接的部分
<main></main>             // 定义文档的主体部分
<section></section>       // 定义文档中的节
<footer></footer>         // 定义页面的页脚
<aside></aside>           // 定义页面的侧边栏内容
<article></article>       // 定义一个文章区域

电话号码拨号与识别

使用 a 标签实现点击手机号码唤起拨号页面

<a href="tel:4009161060" class="phone">400-916-1060</a>

在 iOS Safari 浏览器上会将类似为电话号码的数字识别为电话链接,比如:

7 位数字,形如:1234567 带括号及加号的数字,形如:(+86)123456789 双连接线的数字,形如:00-00-00111 11 位数字,形如:13800138000

关闭识别

<meta name="format-detection" content="telephone=no" />

邮箱识别

使用 a 标签实现点击邮箱地址弹出邮件发送的功能

<a href="mailto:8888@qq.com" class="email">8888@qq.com</a>

Android 手机上会对符合邮箱格式的字符串进行识别,我们可以通过如下的 meta 来关闭邮箱的自动识别

<meta name="format-detection" content="email=no" />

CSS 相关

1px 边框

css 中的 1px 并不等于移动设备的 1px,是因为不同的手机有不同的像素密度。在 window 对象中有一个 devicePixelRatio 属性,他可以反应 css 中的像素与设备的像素比

devicePixelRatio 的官方的定义为:设备物理像素和设备独立像素的比例

完美实现 1px 的方法如下,兼容 DPR(设备像素比) 1-4 的设备


@line-color: #ccc;

[retina] {
  position: relative;
}

[retina]::before {
  position: absolute;
  box-sizing: border-box;
  width: 100%;
  height: 100%;
  font-size: 20px;
  content: ' ';
  pointer-events: none;
}

/* 设备像素比为 1 */
@media only screen and (-webkit-min-device-pixel-ratio: 1), only screen and (min-device-pixel-ratio: 1) {
  [retina]::before {
    width: 100%;
    height: 100%;
    transform: scale(1);
    transform-origin: 0 0;
  }

  [retina='line-bottom']::before {
    height: 1px;
  }

  [retina='line-top']::before {
    height: 1px;
  }

  [retina='line-left']::before {
    width: 1px;
  }

  [retina='rect-input']::before {
    border-width: 1px;
  }
}

/* 设备像素比为 1.5 */
@media only screen and (-webkit-min-device-pixel-ratio: 1.5), only screen and (min-device-pixel-ratio: 1.5) {
  [retina]::before {
    width: 150%;
    height: 150%;
    font-size: 30px;
    transform: scale(0.666667);
    transform-origin: 0 0;
  }

  [retina='line-bottom']::before {
    height: 1px;
  }

  [retina='line-top']::before {
    height: 1px;
  }

  [retina='line-left']::before {
    width: 1px;
  }

  [retina='rect-input']::before {
    border-width: 1px;
  }

  [retina='rect-input']::before {
    border-width: 1px;
  }
}

/* 设备像素比为 2 */
@media only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (min-device-pixel-ratio: 2) {
  [retina]::before {
    width: 200%;
    height: 200%;
    font-size: 40px;
    transform: scale(0.5);
    transform-origin: 0 0;
  }

  [retina='line-bottom']::before {
    height: 1px;
  }

  [retina='line-top']::before {
    height: 1px;
  }

  [retina='line-left']::before {
    width: 1px;
  }

  [retina='rect-input']::before {
    border-width: 1px;
  }
}

/* 设备像素比为 3 */
@media only screen and (-webkit-min-device-pixel-ratio: 3), only screen and (min-device-pixel-ratio: 3) {
  [retina]::before {
    width: 300%;
    height: 300%;
    font-size: 60px;
    transform: scale(0.333333);
    transform-origin: 0 0;
  }

  [retina='line-bottom']::before {
    height: 2px;
  }

  [retina='line-top']::before {
    height: 2px;
  }

  [retina='line-left']::before {
    width: 2px;
  }

  [retina='rect-input']::before {
    border-width: 2px;
  }
}

/* 设备像素比为 4 */
@media only screen and (-webkit-min-device-pixel-ratio: 4), only screen and (min-device-pixel-ratio: 4) {
  [retina]::before {
    width: 400%;
    height: 400%;
    font-size: 80px;
    transform: scale(0.25);
    transform-origin: 0 0;
  }

  [retina='line-bottom']::before {
    height: 2px;
  }

  [retina='line-top']::before {
    height: 2px;
  }

  [retina='line-left']::before {
    width: 2px;
  }

  [retina='rect-input']::before {
    border-width: 2px;
  }
}

[retina='line-bottom']::before {
  bottom: 0;
  left: 0;
  background-color: @line-color;
  transform-origin: 0 100%;
}

[retina='line-top']::before {
  top: 0;
  left: 0;
  background-color: @line-color;
}

[retina='line-left']::before {
  top: 0;
  left: 0;
  background-color: @line-color;
}

[retina='rect-input']::before {
  top: 0;
  left: 0;
  box-sizing: border-box;
  border-color: @input-border-color;
  border-style: solid;
  border-radius: 1em;
}

使用方式很简单

上边框

<div class="container" retina="line-top"></div>

右边框

<div class="container" retina="line-right"></div>

下边框

<div class="container" retina="line-bottom"></div>

左边框

<div class="container" retina="line-left"></div>

全边框

<div class="container" retina="rect-input"></div>

绘制三角形

css 绘制三角形

.triangle-up {
  display: inline-block;
  width: 0;
  height: 0;
  border-left: 10px solid transparent;
  border-right: 10px solid transparent;
  border-bottom: 20px solid #409eff;
}

.triangle-right {
  display: inline-block;
  width: 0;
  height: 0;
  border-top: 10px solid transparent;
  border-bottom: 10px solid transparent;
  border-left: 20px solid #409eff;
}

.triangle-down {
  display: inline-block;
  width: 0;
  height: 0;
  border-right: 10px solid transparent;
  border-left: 10px solid transparent;
  border-top: 20px solid #409eff;
}

.triangle-left {
  display: inline-block;
  width: 0;
  height: 0;
  border-right: 20px solid #409eff;
  border-top: 10px solid transparent;
  border-bottom: 10px solid transparent;
}

滚动区域边缘的模糊蒙层

左侧:background: linear-gradient(90deg, #fff, #fff, 50%, hsla(0, 0%, 98%, 0));

右侧:background: linear-gradient(270deg, #fff, #fff, 20%, hsla(0, 0%, 98%, 0));

可换行的文本下划线

移动端H5开发-常用技巧篇

<span>“比赛期间作品许可授权声明”的签署短信,签署后完成参赛报名</span>

span {
    position: relative;
    background: linear-gradient(to bottom, transparent 50%, #FEDC00 50%);
}

音乐唱片旋转与暂停保持动画状态

常常有这样的需求,H5 页面顶部有个播放音乐的图标,点击播放音乐,图标开始旋转,再次点击暂停播放,且图标保持旋转的弧度

@keyframes rotation {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}

.music-icon {
  animation: rotation 5s linear infinite;
}

.music-icon.paused {
  animation-play-state: paused;
}

如上代码,点击暂停时加上样式类 paused,点击播放时去掉样式类 paused

镜像翻转效果

移动端H5开发-常用技巧篇

倾斜-1 并且旋转 180 度

transform: scaleX(-1) rotate(180deg);

12px 以下的字体大小实现

大部分桌面浏览器对于小于 12px 的字体会直接展示为 12px,移动端浏览器不会有这样的问题。为了保持两端同步,可以采用缩放来实现

如果设计稿上字体大小是 10px,css 可以写成 20px,然后缩放 0.5 倍大小。缩放后字体位置可能会有偏移,可以使用 translate 来调整


<div class="10pxfont-wrapper">
    <div class="10pxfont">我是一段文本</div>
</div>

.10pxfont-wrapper {
  display: flex;
  justify-content: center;
  align-items: center;
  width: 375px;
  height : 20px;
}

.10pxfont {
  width: 200%;
  height: 200%;
  font-size: 20px;
  transform: scale(0.5);
}

屏蔽用户选择

禁止用户选择页面中的文本

.container {
  user-select: none;
}

清除输入框内阴影

在 safari 浏览器中,输入框默认有内部阴影,可以这样关闭:

input {
  -webkit-appearance: none;
}

禁止保存或拷贝图像

img {
  -webkit-touch-callout: none;
}

输入框 placeholder 样式设置

设置 input 里面 placeholder 字体的颜色

input::-webkit-input-placeholder,
textarea::-webkit-input-placeholder {
  color: #c2c2c2;
}

用户设置字号放大或者缩小导致页面布局错误

设置字体禁止缩放

body {
  text-size-adjust: 100% !important;
}

android 系统中元素被点击时产生边框

部分 android 系统点击一个链接,会出现一个边框或者半透明灰色遮罩, 不同生产商定义出来额效果不一样。去除代码如下

a, button, input, textarea{
  -webkit-tap-highlight-color: rgba(0,0,0,0);
  -webkit-user-modify:read-write-plaintext-only;
}

iOS 滑动不流畅

ios 手机上下滑动页面会产生卡顿,手指离开页面,页面立即停止运动。整体表现就是滑动不流畅,没有滑动惯性。 iOS 5.0 以及之后的版本,滑动有定义有两个值 auto 和 touch,默认值为 auto

在滚动容器上增加滚动 touch 方法

.scroll {
  -webkit-overflow-scrolling: touch;
}

safari 浏览器会给 disabled 的输入框加上 0.4 的不透明度

input:disabled {
  opacity: 0.4;
}

不想透明度降低可以这样设置

input:disabled {
  color: xxx;
  opacity: 1;
  -webkit-text-fill-color: xxx;
}

动画实现

loading 动画

接口调用时展示加载 loading

<div class="loading"></div>

.loading {
    width: 20px;
    height: 20px;
    background-image: url(xxxxx);
    background-repeat: no-repeat;
    animation: rotation 1s infinite;
}

@keyframes rotation {
    0% {
      transform: rotate(0deg);
    }

    100% {
        transform: rotate(1turn);
    }
}

逐帧动画之音浪实现方法

播放音频时的音浪如何实现? 移动端H5开发-常用技巧篇

<div class="sound-wave">
    <div class="icon"></div>
</div>

.sound-wave {
    width: 21px;
    height: 21px;
}

.sound-wave .icon {
    position: relative;
    width: 42px;
    height: 42px;
    background-image: url(../img/sound_wave.png);
    background-size: 1008px 42px;
    animation: sound_run 1.2s steps(24,start) infinite;
    zoom: .5;
}

@keyframes sound_run {
    0% {
      background-position: 0 0;
    }

    100% {
        background-position: -1008px 0;
    }
}

一起来看流星雨

H5 活动类页面实现流星雨效果? 移动端H5开发-常用技巧篇

<div class="container">
    <div class="flowglow"></div>
</div>

.container {
    width: 100vw;
    height: 100vh;
}

.container .flowglow {
    position: absolute;
    top: 0;
    right: 0;
    width: 22vw;
    height: 22vw;
    animation: flowglow 5s infinite;
    background-image: url(../img/flowglow.png);
    background-repeat: no-repeat;
    background-position: 0 0;
    background-size: contain;
    transform: translate3d(22vw, -22vw, 0);
}

@keyframes flowglow {
    0% {
      transform: translate3d(-22vw, -22vw, 0);
    }

    100% {
        transform: translate3d(-120vw, 76vw, 0);
    }
}

页面切换动画,区分前进和后退

在单页应用中,点击链接进入新页面希望页面从右往左滑入,回退到上个页面希望页面从左往右滑出。

以 vue 应用为例,这里会将最外层容器 app-wrapper 绝对定位,宽高 100%,子路由页面高度 100%,内容自适应滚动。这样设计利于页面切换动画的实现。

App.vue

<template>
  <div class="app-wrapper">
    <transition :name="transitionName">
      <keep-alive>
        <router-view :key="$route.name"></router-view>
      </keep-alive>
    </transition>
  </div>
</template>

<script lang="ts">

...

transitionName = '';

// 监听路由添加页面切换效果
@Watch('$route')
onRouteChanged(to: Route, from: Route) {
    const toDeep = to.meta.deep;
    const fromDeep = from.meta.deep;
    // 初次进入首页不启用滑动效果
    if (from.name === null) {
        this.transitionName = '';
    } else {
        this.transitionName = toDeep === fromDeep ? '' : toDeep < fromDeep ? 'slide-right' : 'slide-left';
    }
}

...

</script>

<style lang="less" scoped>
.app-wrapper {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
}

子页面根样式 {
    overflow: atuo;
    height: 100%;
}

// transition样式类
.slide-left-enter-active,
.slide-left-leave-active,
.slide-right-enter-active,
.slide-right-leave-active,
.slide-top-enter-active,
.slide-top-leave-active,
.slide-bottom-enter-active,
.slide-bottom-leave-active {
  position: absolute;
  width: 100%;
  transition: transform 300ms linear;
}

.slide-left-enter,
.slide-right-leave-to {
  transform: translate3d(100%, 0, 0);
}

.slide-top-enter,
.slide-bottom-leave-to {
  transform: translate3d(0, 100%, 0);
}

.slide-left-enter-to,
.slide-left-leave,
.slide-right-enter-to,
.slide-right-leave,
.slide-top-enter-to,
.slide-top-leave,
.slide-bottom-enter-to,
.slide-bottom-leave {
  transform: translate3d(0, 0, 0);
}

.slide-left-leave-to,
.slide-right-enter {
  transform: translate3d(-100%, 0, 0);
}

.slide-top-leave-to,
.slide-bottom-enter {
  transform: translate3d(0, -100%, 0);
}
</style>

router.ts:根据页面层级深度设置 meta 中的 deep 参数,页面越深,deep 数字越大

import Vue from 'vue';
import Router, { RouteConfig } from 'vue-router';
import { NavTitle } from './extapi';

Vue.use(Router);

const routes: RouteConfig[] = [
  {
    path: '/',
    alias: ['album', '/album', 'home', '/home'],
    name: 'album',
    component: () => import(/* webpackChunkName: "album" */ './views/Album.vue'),
    meta: {
      deep: 0,
      title: NavTitle.Album,
    },
  },
  {
    path: '/login',
    alias: ['login', 'loginPop', '/loginPop'],
    name: 'login',
    component: () => import(/* webpackChunkName: "login" */ './views/Login.vue'),
    meta: {
      deep: 1,
      title: NavTitle.Login,
    },
  },

  ...
]

JS 相关

手机号码输入框禁止输入非数字字符

解决方案:监听输入事件,将非数字字符替换为空字符串

<input
  v-model.trim="phone"
  type="tel"
  placeholder="请输入手机号码"
  :maxlength="11"
  @input="onInputMessage('phone')"
/>

// 解决在微信内IOS粘贴短信验证码,不显示在方框内的问题
onInputMessage(field: string) {
  this[field] = this[field].replace(/[^\d]/g, '');
}

移动端 click 事件会有 300ms 的延时

移动端 click 事件会有 300ms 的延时,原因是移动端屏幕双击会缩放(double tap to zoom) 页面,浏览器需要延时来判断用户操作是点击还是双击事件。解决方案:fastclick

var attachFastClick = require('fastclick');
attachFastClick(document.body);

safari 浏览器日期转换出现 NaN 的问题

将日期字符串的格式符号替换成'/'

'yyyy-MM-dd'.replace(/-/g, '/')