我报名参加金石计划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小很多,因此手机上点击按钮可能较小,用户使用的时候很可能点一次触发不了点击事件,所以为了提高用户的点击效率,对点击按钮需要扩展一下。
这里有两种方式进行扩展:
- 使用
padding属性扩大可点击区域
<button class="btn">点击</button>
.btn { padding: 20px; }
- 使用伪元素扩大可点击区域
.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页面开发中,经常用到搜索框去搜索内容,这里展示下如何封装一个搜索框组件。
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>
- 逻辑部分
这里我们没有处理搜索的具体逻辑,因为这个是父组件的业务逻辑,我们只需要把用户输入的最新值值传递给父组件即可。
<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 开发中往往找不到满足要求的确认框,那么就要求自己封装一个确认框组件,这里就是想通过确认框的例子来看看如何封装这一类弹框的组件。
效果如下:
- 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>
- 添加确认框的显示与隐藏动画
.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)
- 逻辑部分
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')
}
}
}
- 父组件调用
<confirm
ref="confirm"
text="是否清空所有搜索历史"
confirmBtnText="清空"
@confirm="clearSearchHistory"
></confirm>
// 打开确认框
showConfirm() {
this.$refs.confirm.show()
}
H5中的事件
H5中常用的事件除了click外,还有touchstart, touchmove, touchend,下面以一个例子来说明下这三个事件的使用。
这个例子的功能是当手指向左右滑动的时候,屏幕中的内容进行切换。比如当手指向左侧滑动时,那么右侧的内容将显示在屏幕中。
- 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>
- 左右两边的内容排列在一行
.content-left, .content-right {
display: inline-block
}
- 逻辑部分
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`
},