肝货!移动端 H5 开发十大技巧

572 阅读2分钟

我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第3篇文章,点击查看活动详情

移动端网页H5开发和PC端是有很多区别的,本文从自己开发过的H5项目中总结出移动端开发十大技巧,希望能对你有所启发。

使用几倍图 @2x @3x

移动端开发过程中,因为手机的dpr(设备像素比不同),需要根据dpr来修改图标的大小,判断使用@2x 图 还是 @3x 图,解决高清的适配。

sass为例:

// @mixin
@mixin bg-image($url) {    
      background-image: url($url + "@2x.png");    
      @media (-webkit-min-device-pixel-ratio: 3),(min-device-pixel-ratio: 3){        
            background-image: url($url + "@3x.png");    
      }
}

// @include
div{
  width:30px;
  height:20px;
  background-size:30px  20px;
  background-repeat:no-repeat;
  @include bg-image('../../../../static/image/map_loading');     
}

扩展点击区域

手机屏幕区域比PC小很多,因此手机上点击按钮可能较小,用户使用的时候很可能点一次触发不了点击事件,所以为了提高用户的点击效率,对点击按钮需要扩展一下。

这里有两种方式进行扩展:

  1. 使用 padding 属性扩大可点击区域
<button class="btn">点击</button>
.btn { padding: 20px; }
  1. 使用伪元素扩大可点击区域
.btn {
  position: relative;
}

.btn::before {
  content: '';
  position: absolute;
  top: -10px;
  right: -10px;
  bottom: -10px;
  left: -10px;
}

移动端滚动: 让滚动更流畅

在移动端,如果你使用过 overflow: scroll 生成一个滚动容器,会发现它的滚动是比较卡顿,呆滞的。为什么会出现这种情况呢?

因为我们早已习惯了目前的主流操作系统和浏览器视窗的滚动体验,比如滚动到边缘会有回弹,手指停止滑动以后还会按惯性继续滚动一会,手指快速滑动时页面也会快速滚动。而这种原生滚动容器(某个div容器内)却没有,就会让人感到卡顿。比如,在一个div容器内滚动是没有在浏览器窗口滚动的回弹等效果的。

为了达到这种边沿回弹,惯性滚动,上拉刷新,下拉加载等特性,就不能使用默认的滚动机制,better-scroll采用通过监听touch事件,改变transform:translate()的值来实现这些效果的

对于better-scroll的具体使用可以参考BetterScroll:可能是目前最好用的移动端滚动插件

better-scroll不仅能做垂直方向的滚动,better-scroll还提供了slide配置来实现一个slider轮播图,这在h5项目中就能非常快速搞定所有关于滚动的功能,而不用去为了实现不同功能去加载其他杂七杂八的第三库。

图片懒加载

我们使用手机的时候,很多是在没有wifi的情况下,如果一个页面中列表含有比较多的图片,如果全部加载的话会非常浪费流量,同时在滚动条下面的图片你都不打算往下看,这些图片也加载了。为了解决上面的问题可以使用图片懒加载。

这里推荐使用vue-lazyload插件,使用方式如下:

// main.js
import VueLazyload from 'vue-lazyload'
// 准备一种默认的图片
Vue.use(VueLazyload, {
  loading: require('common/image/default.png')
})

// 采用指令的方式使用
<ul>
  <li v-for="img in list">
    <img v-lazy="img.src" >
  </li>
</ul>

路由动画

在开发 H5 页面时,比如有一个 lists.vue 列表页面,现在点击其中一个查看详情页,那么这个详情页以什么样的方式出现了屏幕中呢?

一般是从右往左的滑入方式进入,这需要使用 Vue 的内置组件transition,同时需要添加appear,这个属性的作用是一进入就能触发动画,而不是需要人为触发。

伪代码如下:

// lists.vue
<template>
  <div class="recommend" ref="recommend">
    <ul>
        <li @click="toDetail"></li>
        <li></li>
        <li></li>
    </ul>
    // 承载子路由
    <router-view></router-view>
  </div>
</template>

<script>
toDetail(item) {
    this.$router.push({
        path: `/list/${item.id}`
    })
}
</script>

// detail.vue
<template>
  <transition appear name="slide">
    <div>....</div>
  </transition>
</template>

<style scoped lang="stylus" rel="stylesheet/stylus">
.slide-enter-active,
.slide-leave-active
  transition: all 0.3s

.slide-enter,
.slide-leave-to
  transform: translate3d(100%, 0, 0)
</style>

keep-alive的使用

现在有个场景:你在A页面滑动了好久终于找到了你想找的信息,但是这个时候因突然情况你需要跳转到B页面,当处理好B页面的事情之后需要跳转回A页面原来的位置,此时你发现A页面又是从头开始的,原先找的信息不见了。这样体验就不太好,有没有解决方案呢?

答案是keep-alive

只要组件会经历创建和销毁(v-if v-show)的时候,都可以使用keep-alive,如果没有缓存,每点击一次导航,内容区就会创建一个组件,该组件会经历整个生命周期,每点击一次,就会创建一个组件,比较浪费性能。

这时,我们就要考虑到是否能将点击过的已创建的组件件进行缓存,当再次点击已访问过的组件时直接就会从缓存中获取该组件,而不会重新创建,这就是keep-alive

<keep-alive include="bookLists">
  <router-view></router-view>
</keep-alive>
<keep-alive exclude="indexLists">
  <router-view></router-view>
</keep-alive>

还可以动态改变include
<keep-alive :include="keepAliveNames">
  <router-view />
</keep-alive>

include,exclude属性

  • include属性表示只有组件name属性为bookLists的组件会被缓存。

  • exclude属性表示除了name属性为indexLists的组件不会被缓存,其它组件都会被缓存。

另一种用法:利用路由的meta属性:

export default[
 {
  path:'/',
  name:'home',
  components:Home,
  meta:{
    keepAlive: true //需要被缓存的组件
  }
]

activated, deactivated钩子

keep-alive包裹的组件,除了第一次创建的时候会执行mounted钩子外,其他时机进入到该页面都不会执行mounted,如果这是页面有些接口有变化,怎么办呢?可以使用activated这个钩子,表示这个页面被激活。

padding-top在移动端中的使用

在 H5 页面开发中,页面经常是上面一张图片,下面是一些信息,图片需要通过网络请求才能获得。

如果没有给图片设置一个高度,就会导致一进来会先看到下面的信息,等图片请求回来之后再显示图片,这样会造成页面有一个闪动,所以我们需要给图片设置一个默认高度,先把图片的位置占住,但是这个高度不能设置死,因为不同屏幕手机的大小不一致。

所以我们可以利用padding-top: 50%; height: 0;,值为百分比,这个百分比是当前手机屏幕的宽度,这样完美解决上面的问题。

.bg-image {
    position: relative
    width: 100%
    height: 0
    padding-top: 70%
    background-size: cover
}

搜索框

在H5页面开发中,经常用到搜索框去搜索内容,这里展示下如何封装一个搜索框组件。

  1. template模板
<template>
  <div class="search-box">
    // 左边的搜索图标
    <i class="icon-search"></i>
    <input ref="query" v-model="query" class="box" :placeholder="placeholder" />
    // 右边的清空input框里面的内容
    <i @click="clear" v-show="query" class="icon-dismiss"></i>
  </div>
</template>

image.png

  1. 逻辑部分

这里我们没有处理搜索的具体逻辑,因为这个是父组件的业务逻辑,我们只需要把用户输入的最新值值传递给父组件即可。

<script>
import { debounce } from 'common/js/util'

export default {
  props: {
    placeholder: {
      type: String,
      default: '搜索歌曲、歌手'
    }
  },
  data() {
    return {
      query: ''
    }
  },
  methods: {
    clear() {
      this.query = ''
    },
    setQuery(query) {
      this.query = query
    },
    // 当我们滚动列表的时候,让input框失去焦点,这样文本框就会收起来,不影响滚动
    blur() {
      this.$refs.query.blur()
    }
  },
  created() {
    this.$watch(
      'query',
      debounce(newQuery => {
        this.$emit('query', newQuery)
      }, 200)
    )
  }
}
</script>
  • 增加了一个防抖功能
function debounce(func, delay) {
  let timer

  return function(...args) {
    if (timer) {
      clearTimeout(timer)
    }
    timer = setTimeout(() => {
      func.apply(this, args)
    }, delay)
  }
}
  • 增加了一个setQuery方法,这是当父组件调用的时候可以设置搜索值,比如当用户有搜索历史,当点击一个搜索历史时,需要把这个历史值设置进来。
<search-box ref="searchBox" @query="onQueryChange"></search-box>
// 往搜索框设置值
addQuery(key) {
    this.$refs.searchBox.setQuery(key)
}
  • 增加了有个blur函数,当用户进行其他操作的时候,需要把input框焦点去除,这样手机上的键盘就会消息。

搜索历史

现在App都有一个搜索历史的功能,当你下次想找上次搜索的内容的时候就非常方便。

搜索历史的保存位置一般放在localStorage里面,对于localStorage的操作可以采用storage这个第三方库。

因为localStorage里面的数据可能在多个地方使用,所以需要把数据放在vue-store中进行统一管理。

// store
const state = {
    searchHistory: loadSearch()
}

对于localStorage的操作如下:

import storage from 'good-storage'

// 取一个特殊的key,以免冲突
const SEARCH_KEY = '__search__'
// 最大存储的数量
const SEARCH_MAX_LEN = 15

// 往数组中插入数据
function insertArray(arr, val, compare, maxLen) {
  const index = arr.findIndex(compare)
  // 如果已经存在,并且放在第一的位置
  if (index === 0) {
    return
  }
  // 如果存在,且不是第一的位置,先删除,然后放在最前面
  if (index > 0) {
    arr.splice(index, 1)
  }
  arr.unshift(val)
  
  // 如果超过存储的最大数量,则删除最后的数据
  if (maxLen && arr.length > maxLen) {
    arr.pop()
  }
}

function deleteFromArray(arr, compare) {
  const index = arr.findIndex(compare)
  if (index > -1) {
    arr.splice(index, 1)
  }
}

// 保存搜索历史
export function saveSearch(query) {
  let searches = storage.get(SEARCH_KEY, [])
  // 往搜索历史中加入数据
  insertArray(
    searches,
    query,
    item => {
      return item === query
    },
    SEARCH_MAX_LEN
  )
  storage.set(SEARCH_KEY, searches)
  return searches
}

export function deleteSearch(query) {
  let searches = storage.get(SEARCH_KEY, [])
  deleteFromArray(searches, item => {
    return item === query
  })
  storage.set(SEARCH_KEY, searches)
  return searches
}

export function clearSearch() {
  storage.remove(SEARCH_KEY)
  return []
}

export function loadSearch() {
  return storage.get(SEARCH_KEY, [])
}

确认框

在当用户删除一个比较重要的数据时,需要一个确认框让用户再次确认。在开发 PC 项目的时候,一般都是用的第三方组件库,但是在 H5 开发中往往找不到满足要求的确认框,那么就要求自己封装一个确认框组件,这里就是想通过确认框的例子来看看如何封装这一类弹框的组件。

效果如下:

image.png

  1. template模板
<template>
  <transition name="confirm-fade">
    // showFlag控制弹框的显示与否
    <div class="confirm" v-show="showFlag" @click.stop>
      <div class="confirm-wrapper">
        <div class="confirm-content">
          <p class="text">{{ text }}</p>
          <div class="operate">
            <div @click="cancel" class="operate-btn left">
              {{ cancelBtnText }}
            </div>
            <div @click="confirm" class="operate-btn">
              {{ confirmBtnText }}
            </div>
          </div>
        </div>
      </div>
    </div>
  </transition>
</template>
  1. 添加确认框的显示与隐藏动画
.confirm
  position: fixed
  z-index: 998
  top: 0
  right: 0
  bottom: 0
  left: 0
  background-color: $color-background-d
  &.confirm-fade-enter-active
    animation: confirm-fadein 0.3s
    .confirm-content
      animation: confirm-zoom 0.3s

@keyframes confirm-fadein
  0%
    opacity: 0
  100%
    opacity: 1

@keyframes confirm-zoom
  0%
    transform: scale(0)
  50%
    transform: scale(1.1)
  100%
    transform: scale(1)
  1. 逻辑部分
export default {
  props: {
    text: {
        type: String,
        default: ''
    },
    confirmBtnText: {
        type: String,
        default: '确定'
    },
    cancelBtnText: {
        type: String,
        default: '取消'
    }
  },
  data () {
    return {
      showFlag: false
    }
  },
  methods: {
    show () {
      this.showFlag = true
    },
    hide () {
      this.showFlag = false
    },
    cancel () {
      this.hide()
      this.$emit('cancel')
    },
    confirm () {
      this.hide()
      this.$emit('confirm')
    }
  }
}
  1. 父组件调用
<confirm
  ref="confirm"
  text="是否清空所有搜索历史"
  confirmBtnText="清空"
  @confirm="clearSearchHistory"
></confirm>

// 打开确认框
showConfirm() {
  this.$refs.confirm.show()
}

H5中的事件

H5中常用的事件除了click外,还有touchstart, touchmove, touchend,下面以一个例子来说明下这三个事件的使用。

这个例子的功能是当手指向左右滑动的时候,屏幕中的内容进行切换。比如当手指向左侧滑动时,那么右侧的内容将显示在屏幕中。

  1. template模板
<div
  class="wrapper"
  @touchstart.prevent="touchStart"
  @touchmove.prevent="middleTouchMove"
  @touchend.prevent="middleTouchEnd"
>
   <div class="content-left" ref="contentLeft">左边内容</div>
   <div class="content-right" ref="contentRight">右边内容</div>
</div>
  1. 左右两边的内容排列在一行
.content-left, .content-right {
   display: inline-block
}
  1. 逻辑部分
created() {
  this.touch = {}
}

touchStart(e) {
  // 增加一个初始化的标志位
  this.touch.initiated = true
  this.touch.directionLocked = ''
  // 用来判断是否是一次移动
  this.touch.moved = false
  this.touch.startX = e.touches[0].pageX
  this.touch.startY = e.touches[0].pageY
}

touchMove(e) {
  if (!this.touch.initiated) {
    return
  }
  const touch = e.touches[0]
  const deltaX = touch.pageX - this.touch.startX
  const deltaY = touch.pageY - this.touch.startY

  const absDeltaX = Math.abs(deltaX)
  const absDeltaY = Math.abs(deltaY)
  // 当纵向滚动的距离大于横向滚动的距离的时候,就什么都不做
  if (!this.touch.directionLocked) {
    if (absDeltaX > absDeltaY) {
      this.touch.directionLocked = 'h' // lock horizontally
    } else if (absDeltaY >= absDeltaX) {
      this.touch.directionLocked = 'v' // lock vertically
    }
  }
  if (this.touch.directionLocked === 'v') {
    return
  }
  if (!this.touch.moved) {
    this.touch.moved = true
  }
  const left = -window.innerWidth
  
  const offsetWidth = Math.min(
    0,
    Math.max(-window.innerWidth, left + deltaX)
  )
  this.touch.percent = Math.abs(offsetWidth / window.innerWidth)
  
  // 改变右侧内容是transform,让右侧的内容滑入
  this.$refs.contentRigth.style[
    transform
  ] = `translate3d(${offsetWidth}px, 0, 0)`
  this.$refs.contentRigth.style[transitionDuration] = 0
  
  // 把左侧的内容影藏
  this.$refs.contentLeft.style.opacity = 1 - this.touch.percent
  this.$refs.middleL.style[transitionDuration] = 0
},

touchEnd() {
  // 首先要判断moved是否true,如果为true才表明是一个有效的滑动,比如垂直滑动就是无效的滑动,垂直滑动只会滑动歌词
  if (!this.touch.moved) {
    return
  }
  let offsetWidth
  let opacity
  // 从右像左滑动
  if (this.right) {
    // 如果滑动的距离超过一定的值,那么就把右侧内容整体滑入,就不用一点点的滑入了
    if (this.touch.percent > 0.1) {
      offsetWidth = -window.innerWidth
      this.right = false
      opacity = 0
    } else {
      offsetWidth = 0
      opacity = 1
    }
  } else {
    // 从左像右滑动
    if (this.touch.percent < 0.9) {
      offsetWidth = 0
      this.right = true
      opacity = 1
    } else {
      offsetWidth = -window.innerWidth
      opacity = 0
    }
  }
  this.$refs.contentRigth.style[
    transform
  ] = `translate3d(${offsetWidth}px, 0, 0)`
  
  const time = 300
  // 此时就需要增加一个缓动的过程
  this.$refs.contentRigth.style[transitionDuration] = `${time}ms`
  this.$refs.contentLeft.style.opacity = opacity
  this.$refs.contentLeft.style[transitionDuration] = `${time}ms`
},