vue使用better-scroll制作左右菜单联动以及vant框架导致滚动失效的解决办法

vue使用better-scroll制作左右菜单联动

better-scroll是一个纯js的插件,官方推荐配合vue这种纯前端开发使用,在vue中使用需要注意一些地方,比如初始化的时机,html的布局,事件的触发机制

安装

我现在都是用yarn 方便一些,未来的安装包管理器

yarn add better-scroll

yarn不写--save默认就是save,如果要--save-dev 如下写法

yarn add better-scroll --dev

安装完毕后可以在main.js文件中作为全局调用,也可以在对于的组件内调用,我是在组件中的,所以在组件的script元素中import

<script>
  import BScroll from "better-scroll";
</script>

BScroll将作为这个js插件的变量名使用。

引入完毕后,我们先了解下html的结构,我们可以先去看下官方文档的原理示意图:

官方文档

原理图

html结构

可以看到,一个父元素,这父元素限制高度,并且overflow: hidden,里面的子元素则由内容撑开。

所以一个基本结构如下:

<div class="container" ref="container">
  <ul>
    <li>子元素内容</li>
    <li>子元素内容</li>
    <li>子元素内容</li>
    <li>子元素内容</li>
    <li>子元素内容</li>
    <li>子元素内容</li>
    <li>很多个</li>
  </ul>
</div>

container容器为父元素,ul为子元素,li则是内容撑开,我们可以限制container容器的高度,利用插件来滑动ul元素。

如果说你的布局很复杂,建议就是先用一个div布好位置,然后把容器丢进去,高度100%。

初始化的时机

结构搭好后我们就需要对这个插件功能进行初始化。

在vue的created生命周期里:

created(){
   this.$nextTick(() => {
    //这里放初始化函数,我还没写,后面补充
  });
}

这里我们用到了一个$nextTick;原因是因为我们的li元素,很有可能是for循环遍历出来的,如果直接初始化,容易造成for循环渲染还没有完毕,我就初始化完了,此时的高度是错误的,使用就会出现问题,所以要在$nextTick的回调里进行初始化,此时元素都已经渲染好了的。

如果我们的元素是一个弹窗,那么在created初始化就不正确,因为此时的元素可能没有创建出来,或者是display:none;这个时候初始化就会报错,因为获取不到dom元素或者元素高度计算不出来。

这种情况:

我推荐就是监听元素用于显示隐藏的变量,比如这个变量名为:show,show默认是false,元素就没有,为true就弹窗显示,我们就监听这个变量,当他为true的时候,我们再进行初始化。

但是这个弹窗用户可能打开了又关闭了,弹窗这个东西是需要复用的,所以只监听为true是片面的,我们要创建一个变量用于判断是否已经初始化了,在data里面创建一个变量名为initScroll:false

 watch: {
    show(val) {
      if (val && !this.initScroll) {
        this.$nextTick(() => {
          //这里放初始化函数,我还没写,后面补充
        });
      }
    }
  }

初始化函数

在methods中创建一个初始化函数

methods:{
  initScroll() {
    new BScroll(this.$refs.container, {
      click: true
    });
  }
}

将容器dom元素丢入BScroll得第一个参数,第二个参数是一个对象option,click为true表示开启子元素点击事件,否则默认是阻止的。

此时子元素就可以滑动了,但是还没有达到我们的要求:两个菜单联动

菜单联动

两个菜单联动他其实是有要求的,不是什么东西随便一搭他就能成。

首先我们需要两个菜单进行对应,比如左边的菜单有:一,二,三,四,五;5个菜单选项,那么右边的菜单,第一个li对应一,第二个li对应二,以此类推。

<div class="tab-nav" ref="tabNav">
  <ul>
    <li>一</li>
    <li>二</li>
    <li>三</li>
    <li>四</li>
    <li>五</li>
  </ul>
</div>
<div class="tab-content" ref="tabContent">
  <ul>
    <li ref="sli">
      <p>小标题</p>
      <div>内容</div> 
    </li>
    <li ref="sli">
      <p>小标题</p>
      <div>内容</div> 
    </li>
    <li ref="sli">
      <p>小标题</p>
      <div>内容</div> 
    </li>
    <li ref="sli">
      <p>小标题</p>
      <div>内容</div> 
    </li>
    <li ref="sli">
      <p>小标题</p>
      <div>内容</div> 
    </li>
  </ul>
</div>

左右两个菜单,我要求:

  1. 当我点击左边的菜单,右边的菜单要自动滚动到对应的li
  2. 当我右边的菜单滚动的时候,左边的菜单要即使反馈,比如我右边滚动到第三个li,左边菜单三要高亮显示

因为要判断滚动的位置,所以需要计算出每个li的高度值,将每个li的高度保存在数组,然后在通过对比数组的下标调出高度值进行判断,从而判断是是哪个li,从而可以让左边的菜单高亮。

data(){
  return {
    scrollY: 0, //实时滚动高度
    heightList: [], //右边li元素高度数组
    leftScroll:null,
    rightScroll:null,
    initScroll:false
  }
},
methods:{
  initScrollFn(){
    //左边
    this.leftScroll = new BScroll(this.$refs.tabNav, {
      click: true
    });
    //右边
    this.rightScroll = new BScroll(this.refs.tabContent,{
      probeType: 3,
      click: true
    });
    //监听右边scroll事件
    this.rightScroll.on("scroll",(pos)=>{
      //pos包含x,y轴两个属性,我们要取正值
      this.scrollY = Math.abs(Math.round(pos.y));
    });
  },
  getHeight(){ //获取高度
    const lis = this.$refs.sli;
    //高度从0开始
    let sh = 0;
    this.heightList.push(sh);
    //遍历
    lis.forEach((item,i)=>{
      height += item.clientHeight;
      this.heightList.push(height);
    });
  }
},
watch:{
  show(val){
    if(val && this.initScroll){
      this.$nextTick(() => {
        this.initScrollFn();
        this.getHeight();
      });
    }
  }
}

点击左边菜单右边滚动对应位置

此时初始化就完毕了,下面处理点击左边,右边进行自动滚动到对应位置

创建一个方法:

data(){
  return {
    activeTabNav:0 //左边菜单点击选中的元素下标
  }
},
methods : {
  selectTabNav(index){
    //获取到左边菜单下标对应的右边li元素
    const lis = this.$refs.sli;
    const el = lis[index];
    //更新选中的下标
    this.activeTabNav = index;
    //运行右边菜单的跳转方法,传入li元素和执行时间
    this.rightScroll.scrollToElement(el, 300);
  }
}

html绑定

<div class="tab-nav" ref="tabNav">
  <ul>
    <li v-for="(item,i) in 6" :key="i" @click="selectTabNav(i)" :class="{'active': activeTabNav === i}">{{i}}</li>
  </ul>
</div>

点击事件绑定,clss判断,如果activeTabNav选中的下标和遍历的i相同,就显示active的class

右边菜单滚动左边菜单自动高亮

创建一个计算属性,用于实时计算滚动到哪里

computed:{
  currentIndex(){
    const index = this.heightList.findIndex((item, index) => {
      return (
        this.scrollY >= this.heightList[index] &&
        this.scrollY < this.heightList[index + 1]
      );
    });
    return index > 0 ? index : 0;
  }
}

currentIndex判断,当前heightList高度数组里面,大于或者等于当前滚动的高度,但又小于下一个高度的数组下标。

左边菜单绑定

<li v-for="(item,i) in 6" :key="i" @click="selectTabNav(i)" :class="{'active': activeTabNav === i || currentIndex === i}">{{i}}</li>

这样写虽然能用,但是会有一个问题,就是左边的activeTabNav和currentIndex会有冲突,因为currentInde是即时的,activeTabNav固定的,可能我已经滚动到菜单二,菜单一被activeTabNav影响而高亮显示。

为此我们要对这个两个业务逻辑分开,点击的时候不触发即时计算

data(){
  return {
    tabNavClick: false  //用于判断是否在点击滚动
  }
},
methods:{
  initScrollFn(){
    //左边
    this.leftScroll = new BScroll(this.$refs.tabNav, {
      click: true
    });
    //右边
    this.rightScroll = new BScroll(this.refs.tabContent,{
      probeType: 3,
      click: true
    });
    //监听右边scroll事件
    this.rightScroll.on("scroll",(pos)=>{
      //pos包含x,y轴两个属性,我们要取正值
      this.scrollY = Math.abs(Math.round(pos.y));
    });
    //监听右边滚动结束事件
    this.rightScroll.on("scrollEnd", pos => {
      //如果是点击滚动,滚动结束后恢复tabNavClick为false
      if (this.tabNavClick) {
        this.tabNavClick = false;
      }
    });
  }
},
computed:{
  currentIndex(){
    if(!this.tabNavClick){
      const index = this.heightList.findIndex((item, index) => {
      return (
        this.scrollY >= this.heightList[index] &&
        this.scrollY < this.heightList[index + 1]
      );
      const active = index > 0 ? index : 0;
      if (this.activeTabNav !== active) {
        this.activeTabNav = active;
      }
      return active;
    });
    }
  }
}

即时计算的时候,如果是点击触发的scrollY变化,不做处理。

这样,左右菜单联动就完成了。

vant框架popup组件导致滚动失效

我使用该框架的时候遇到一个问题,就是组件在浏览器pc模式,是可以滚动的,但是手机端不行,滚动无反应,想来想去我只想到一个地方。

better-scroll是在dom上的滚动事件,我记得vant在弹出popup组件的时候默认是有一个防止滑动的属性的,于是我尝试关闭了这个功能,问题解决。

<van-popup :lock-scroll="false"></van-popup>

由于关闭了防止滚动,事件是不会阻止了,但是body不会默认添加clss="van-overflow-hidden"了,这就导致滚动穿透,解决这个问题简单点,我就是在监听这个弹窗的时候,手动添加class了,利用原生js方法。

watch: {
    countryPopup(val) {
      if (val && !this.initScr) {
        this.$nextTick(() => {
          this.initScroll();
          this.getHeight();
        });
      }
      //防止滚动穿透
      if (val) {
        document.body.className = "van-overflow-hidden";
      } else {
        document.body.className = "";
      }
    }
  }
0
微信收款码
微信收款码