开发手势体验媲美原生的移动Web应用

得益于移动设备性能越来越强劲,Web不再成为性能瓶颈。各种hybrid方案以及大量webview内嵌页面以及纯H5应用在用户体验上愈发显得重要。本文将介绍基于Vue2.X、BetterScroll开发的***TabScroll***在处理原生应用中常用的手势操作的案例。阅读本文将快速习得如何借助***TabScroll***开发拥有媲美原生应用手势操作的移动Web应用

TabScroll 简介

TabScroll是一个基于Vue2.X以及BetterScroll的手势库,宗旨是赋予WebApp***复合型***的手势操作 以及 相关的极富设计感的***视差体验***。与此同时,TabScroll希望做到语义清晰 开箱即用,让使用者仅需对BetterScroll有简单的了解和认识即可上手TabScroll。

GitHub地址 github.com/a62527776a/…

案例展示

demo地址 dscsdoj.top/public/unsp…

开始开发!

准备工作

TabScroll依赖BetterScroll 如未安装过BetterScroll的需要同时安装BetterScroll

# shell
$ yarn add better-scroll tab-scroll

or
# 如果安装过better-scroll 仅需安装tab-scroll
$ yarn add tab-scroll
复制代码
# main.js
import Vue from 'vue'
import tabScroll from 'tab-scroll'

Vue.use(tabScroll)

new Vue({
  render: h => h(App)
}).$mount('#app')
复制代码


起步

TabScroll的特点即是开箱即用,仅用短短几行代码即可完成大部分效果 下面的片段展示了一个具有基本的复合型手势的应用,但它还不能满足正式的需求 还需添加更多的代码才能展示完整的应用

实例预览(注意,请使用手机模式打开)

<template>
  <div>
    <vue-horizontal-scroll offsetY="10vh">
      <div slot="header" style="height: 20vh;background: green"></div>
      <vue-vertical-scroll>
        <div v-for="(i, idx) in 20" :key="idx" class="item-block item-block-red"></div>
      </vue-vertical-scroll>
      <vue-vertical-scroll>
        <div v-for="(i, idx) in 20" :key="idx" class="item-block item-block-yellow"></div>
      </vue-vertical-scroll>
      <vue-vertical-scroll>
        <div v-for="(i, idx) in 20" :key="idx" class="item-block item-block-green"></div>
      </vue-vertical-scroll>
    </vue-horizontal-scroll>
  </div>
</template>

<script>
export default {
  name: 'demo2'
}
</script>

<style>
.item-block {
  border-bottom: 15px solid #EEE;
  height: 30vh;
}
.item-block-green {
  background: green
}
.item-block-red {
  background: red
}
.item-block-yellow {
  background: yellow
}
</style>
复制代码


对顶部header栏没有特殊要求的,可以直接忽略header的slot 将顶部的节点直接放置 <vue-horizontal-scroll>上方 TabScroll将自动计算自身高度,以适配界面的高度

不使用slot header实例预览(注意,请使用手机模式打开)

<template>
  <div>
    <div style="height: 20vh;background: green"></div>
    <vue-horizontal-scroll>
      <vue-vertical-scroll>
        <div v-for="(i, idx) in 20" :key="idx" class="item-block item-block-red"></div>
      </vue-vertical-scroll>
      <vue-vertical-scroll>
        <div v-for="(i, idx) in 20" :key="idx" class="item-block item-block-yellow"></div>
      </vue-vertical-scroll>
      <vue-vertical-scroll>
        <div v-for="(i, idx) in 20" :key="idx" class="item-block item-block-green"></div>
      </vue-vertical-scroll>
    </vue-horizontal-scroll>
  </div>
</template>
复制代码


如果底部还有菜单栏,或者遇到自动计算无法满足实际需求的情况 TabScroll也提供了height属性用以计算高度 height属性可以为以各种css单位的String型('80vh', '10.5rem', '650px')等 具体为屏幕高度减去界面上放在TabScroll外的各种元素的高度(比如顶部菜单栏、底部Tab栏)

<template>
  <div>
    <div style="height: 3rem;background: green"></div>
    <vue-horizontal-scroll height="calc(100vh - 3rem - 5rem)">
    ...
    <tab-bar style="height: 5rem">
    ... 
  </div>
</template>
复制代码


开发界面

我们将使用pug以及less快速的开发出一个带有搜索栏(当然,我们不会给这个搜索栏加上任何功能)和一个菜单栏的header-bar

我们还将添加一个根据获取豆瓣关键词获取剧信息的接口,这个接口将作为我们应用的真实数据

<template lang="pug">
  .wrapper
    .header-bar
      .search-bar 写给你爱的人的情书
      .tab-bar(v-if="menus")
        .tab-item(v-for="(value, key, idx) in menus" :class="{'tab-item-active' : idx === currentPageIdx }") {{key}}
    .content-wrapper(v-if="menus")
      .movie-card(v-for="(item, idx) in menus['日剧'].data")
        .movie-cover
          // img(:src="item.images.medium")
          // 豆瓣的图片做了防盗链,暂时用一张假的图片
          img(src="https://i.loli.net/2019/03/12/5c87b97a0b2ce.jpg")
        .movie-title {{item.title}}
</template>

<script>
import axios from 'axios'

export default {
  name: 'douban-demo',
  data () {
    return {
      menus: null,
      // 给mock数据准备一些结构
      mockMenusData: {
        '日剧': {data: null},
        '泰剧': {data: null},
        '韩剧': {data: null},
        '美剧': {data: null},
        '英剧': {data: null}
      },
      // 作为滚动的下标
      currentPageIdx: 0
    }
  },
  methods: {
    /**
     * @method mockMenus mock菜单数据 为模拟真实环境中从后端获取菜单栏 取800ms延迟
     * @return { Promise }
     */
    mockMenus: function () {
      return new Promise(resolve => {
        setTimeout(() => {
          resolve(this.mockMenusData)
        }, 800)
      })
    },
    // 获取菜单栏
    initMenus: async function () {
      this.menus = await this.mockMenus()
      // 菜单栏获取之后开始调用数据
      this.api('日剧', 1)
    },
    /**
     * @method api 从后端获取接口
     */
    api: async function (key, page) {
      let baseUrl = process.env.NODE_ENV === 'development' ? 'http://localhost:7001' : 'https://dscsdoj.top'
      let result = await axios.get(`${baseUrl}/api/douban?key=${key}&page=${page}&size=18`)
      if (!this.menus[key].data) this.menus[key].data = []
      this.menus[key].data = this.menus[key].data.concat(result.data.data.subjects)
    }
  },
  created () {
    this.initMenus()
  }
}
</script>

<style lang="less">
.wrapper {
  .header-bar {
    padding: 8px 12px 0 12px;
    .search-bar {
      background: #EEE;
      border-radius: 50px;
      font-size: 12px;
      color: #666;
      padding: 6px 12px;
    }
    .tab-bar {
      display: flex;
      justify-content: space-between;
      margin: 0 -12px;
      border-bottom: 1px solid #EFEFEF;
      .tab-item {
        color: #AAA;
        padding: 6px 0;
        margin: 0 12px;
        font-size: 14px;
        margin-bottom: -1px;
      }
      .tab-item-active {
        color: #666;
        font-weight: bold;
        border-bottom: 2px solid #666;
      }
    }
  }
  .content-wrapper {
    padding: 12px;
    display: flex;
    width: calc(100% - 24px);
    justify-content: space-between;
    flex-wrap: wrap;
    .movie-card {
      width: 49%;
    }
    .movie-cover {
      img {
        border-radius: 5px;
      }
    }
    .movie-title {
      font-size: 14px;
      color: #444;
      margin-bottom: 12px;
      height: 2.4em;
      color: #212121;
    }
  }
}
</style>



复制代码

整合TabScroll

<template>
.wrapper
    vue-horizontal-scroll(ref="vue-horizontal-scroll") // add 将内容包裹vue-horizontal-scroll组件中
      .header-bar(slot="header") // modify 将header作为slot为header的tab顶部栏
        .search-bar 写给你爱的人的情书
        .tab-bar(v-if="menus")
          .tab-item(v-for="(value, key, idx) in menus" :class="{'tab-item-active' : idx === currentPageIdx }") {{key}}
      template(v-if="!menus") // add 添加一个当菜单还未加载时的loading占位符
        div loading // add
      template(v-else) // add
        vue-vertical-scroll(v-for="(value, key, idx) in menus" :key="idx") // add 将竖向滚动的内容包裹进vue-vertical-scroll中
          .content-wrapper(v-if="value.data") // modify
            .movie-card(v-for="(item, idx) in value.data") // modify
              .movie-cover
                // img(:src="item.images.medium")
                // 豆瓣的图片做了防盗链,暂时用一张假的图片
                img(src="https://i.loli.net/2019/03/12/5c87b97a0b2ce.jpg")
              .movie-title {{item.title}}
      
</template>
<script>
...
...
...
    // 获取菜单栏

    initMenus: async function () {
      this.menus = await this.mockMenus()
      this.$nextTick(() => { // add 因为initBScroll有操作dom 所以包裹进$nextTick中
        this.$refs['vue-horizontal-scroll'].initBScroll() // add 因为菜单栏是动态生成的 所以需要在菜单生成之后再次调用initBScroll 如果菜单是写死的 比如固定4个 则可以忽略该操作
      }) // add
      // 菜单栏获取之后开始调用数据
      this.api('日剧', 1)
    },
...
...
...
</script>
复制代码

当套入了这些代码之后 即可快速生成一个具有复合操作的列表 需要注意的是 本文中的案例使用了动态的菜单栏,所以需要手动在菜单加载完成后调用$refs['vue-horizontal-scroll'].initBScroll() 如果用户并没有动态菜单栏的需求 就可以忽略该行,<vue-horizontal-scroll>将根据其传入的<vue-vertical-scroll>数量来自动处理

这一步的完整代码可以在/demo/整合tabscroll.vue中找到

横向滚动读取数据以及上拉加载

这一步我们将处理横向滚动的手势 这将使应用的左右滑动变得可用

<template>
...
vue-horizontal-scroll(
  ref="vue-horizontal-scroll" 
  @scrollEnd="handleHorizontalScrollEnd") // modyify 通过监听scrollEnd事件 来处理横向滚动结束的事件
      .header-bar(slot="header")
        .search-bar 写给你爱的人的情书
        .tab-bar(v-if="menus")
          .tab-item(v-for="(value, key, idx) in menus" :class="{'tab-item-active' : idx === currentPageIdx }") {{key}}
...
</template>

<script>
...
    /**
     * @method handleHorizontalScrollEnd 处理横向的滚动事件
     * @param { Number } pageIdx 横向滚动的页数
     */
    handleHorizontalScrollEnd: function (pageIdx) { // add
      this.currentPageIdx = pageIdx // add 每次切换tab页之后 都需要改变菜单栏聚焦的栏目
      let keyCode = Object.keys(this.menus)[pageIdx] // add
      // 如果this.menus[keyCode].data存在则说明这一栏已经被加载过了
      if (this.menus[keyCode].data) return // add
      this.api(keyCode, 1) // add 根据当前滚动到的页来请求接口
    },
...
</script>

复制代码

通过监听scrollEnd事件 我们在横向滚动结束的时候加载不同菜单栏的数据

这一步的完整代码可以在/demo/处理横向滚动事件.vue中找到

上拉加载事件

<template>
...
      vue-vertical-scroll(
          v-for="(value, key, idx) in menus" 
          @pullingUp="pullingUp(key, arguments)" // modify 通过监听pullingUp事件,来操作每一个vue-vertical-scroll组件的上拉加载事件
          :key="idx")
...
</template>

<script>
...
  data () {
    ...
    mockMenusData: {
        '日剧': {data: null, page: 1},  // modify 当我们需要上拉加载后 我们需要增加页数字段
        '泰剧': {data: null, page: 1},  // modify
        '韩剧': {data: null, page: 1},  // modify
        '美剧': {data: null, page: 1},  // modify
        '英剧': {data: null, page: 1}  // modify
    },
...
...
  methods: {
    ...
    /**
     * @method pullingUp
     * @param { String } key 当前上拉加载数据的类型 为业务层传过来的参数
     * @param { Arguments } _arguments 由当前上拉的vue-vertical-scroll组件传上来的参数 由只包含一个BScroll实例的数组组成
     * 通过_arguments[0] 即可获取BScroll实例以处理上拉加载的操作 下拉刷新同理
     */
    pullingUp: function (key, _arguments) { // add
      this.api(key, _arguments[0]) // add
    }, // add
    ...
    ...
    /**
     * @method api 从后端获取接口
     * @param { BScroll } BScroll 当前操作的vue-vertical-scroll组件中的BScroll实例
     */
    api: async function (key, BScroll = null) { // modify 页数参数将由this.menus[key].page字段来读取
      let baseUrl = process.env.NODE_ENV === 'development' ? 'http://localhost:7001' : 'https://dscsdoj.top'
      let result = await axios.get(`${baseUrl}/api/douban?key=${key}&page=${this.menus[key].page}&size=10`)
      // 当请求完毕后,需要手动调用finishPullUp方法 否则BScroll将无法继续上拉加载
      this.menus[key].page++ // add 请求成功则增加一页
      if (!this.menus[key].data) this.menus[key].data = []
      this.menus[key].data = this.menus[key].data.concat(result.data.data.subjects)
      if (BScroll) { // add 如果上层传入BScroll 则执行以下函数
        this.$nextTick(() => { // add // 由于refresh需要读取dom参数 所以以下操作必须包裹入$nextTick中
          BScroll.refresh() // add 将重新计算最大滚动位置
          BScroll.finishPullUp() // add 如果不手动执行 上拉加载功能将不再回调
        }) // add
      } // add
    }
    ...
  }
</script>
复制代码

通过监听<vue-vertical-scroll>组件的pullingUp事件,我们将可以操作每一个vue-vertical-scroll组件的上拉加载事件 pullingUp事件向上传递了当前组件的BScroll实例。由于我们需要key参数,使用arguments参数,我们就可以通过arguments[0]来同时拿到业务参数key以及<vue-vertical-scroll>组件提供的BScroll来操作 关于BScroll.refresh() 以及 BScroll.finishPullUp() 通过阅读 https://ustbhuangyi.github.io/better-scroll/doc/zh-hans/api-specific.html#finishpullup https://ustbhuangyi.github.io/better-scroll/doc/zh-hans/api.html#refresh 来了解两个函数做了什么 为什么这么做

这一步的完整代码可以在/demo/处理上拉加载事件.vue中找到

优化展示效果

TabScroll暴露了多个参数来满足不同的展示需求

比如 offsetY:该参数将产生一定的视差 默认情况下上划 顶部header栏将会隐藏 当填写不同css单位的参数后 将会只隐藏一部分 而这一部分 就是你填入的offsetY的高度部分 在本例中希望通过搜索栏的高度来在下滑的情况下只展示菜单栏

<template>
...
vue-horizontal-scroll(
  ref="vue-horizontal-scroll" 
  offsetY="-33px" // add 差值为菜单栏的高度
  @scrollEnd="handleHorizontalScrollEnd")
</template>
复制代码

比如 lock: 如果希望菜单栏的展示隐藏由是否滚动到顶部决定 而不是由手势决定(默认) 则给vue-horizontal-scroll增加一个lock属性

<template>
...
vue-horizontal-scroll(
  ref="vue-horizontal-scroll" 
  offsetY="-33px" 
  lock // add 增加lock 往上划时将根据是否滚动到顶部来判断是否打开
  @scrollEnd="handleHorizontalScrollEnd")
</template>
复制代码

转载于:https://juejin.im/post/5c87e5306fb9a049ba426c8d

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值