Vue, App与我(三)

  • index.html文件的书写完成之后,也就意味着我们的首页展示框已经写好了,接下来需要写一下app.vue的内容。这部分也就是<div id="app"></div>中展示的内容。

  • app.vue: 如果你想简单的进行实现这部分代码,可以写一个测试代码。
<template>
    <div>
        <div class="page-topnav">
            <div class="com-top-nav ">
                <p>首页</p>
                <div class="rigth-btn">
                    <router-link :to="{path:'/search/1/',query:{tab:'hide'}}" class="ico sea"></router-link>
                    <router-link :to="{path:'/user/'}" class="ico user"></router-link>
                </div>
            </div>
            <div class="h15 dn"></div>
            <div class="hbot dn"></div>
        </div>
        <div class="banner pt45">
            <mt-swipe :auto = "3000" class="swipe" v-if="ads">
                <template v-for="ad in ads">
                    <mt-swipe-item class="swipe-item">
                        <a v-on:click = "changeClick(ad['id'], ad['url'])">
                            <img v-if="ad['picture']" : src="fullUrl(ad['picture']) + '!640.301'" />
                        </a>
                    </mt-swipe-item>
                </template>
            </mt-swipe>
            <div class="icon-mess"><div class="red"></div></div>
        </div>
    </div>
</template>

  • 代码解析:
    • <template></template> 单文件中用template标签包含html模板内容。但是很多时候会出现这样的误解:
<template>
    <template></template>
</template>
  • 这样的书写会存在异常报错,ERROR in ./~/vue-loader/lib/template-compiler.js?id=data-v-556c4717!./~/vue-loader/lib/selector.js?type=template&index=0!./src/components/xxx.vue
    template syntax error Cannot use <template> as component root element because it may contain multiple nodes:
  • 组件只能有一个根元素,就是你这了第一个下面必须只有一个根元素, 但是可以用容器元素将它包括起来。
<template>
    <div>
        <template></template>
    </div>
</template>

  • <router-link></router-link>: 组件支持用户在具有路由功能的应用中进行导航。通过to属性指定目标地址,页面加载时默认渲染为<a>标签元素, 如果你使用过react进行开发的话, react-router中的<Link>也就类似于它。但是router-link可以通过配置tag将它转换为其他标签的。另外,当目标路由成功激活时,链接元素自动设置一个表示激活的CSS类名
  • <router-link>比起<a>的好处是什么?
    • 无论是Html5 history模式还是hash模式,它的表现行为一致,所以,当你要切换路由模式,或者在IE9以下降级使用hash模式,无须做任何变动。
    • 在Html5 history模式下,router-link会拦截点击事件, 让浏览器不在重新加载页面。
    • 当你在Html5 history模式下使用base选项之后, 所有的to属性都不需要写(基路经)了。
  • 至于<router-link>props: 请参看vue-router-link的论述。

  • query: {tab : 'hide'} : query 后的键值被放在url中,形式类似以get,明文可以。params 的键值对在请求头header中可以查看到,不放在url中。

  • <mt-swipe></mt-swipe>:

  • mint-ui组件库中swipe组件,目的为了实现常见的轮播图效果。但是它的实现与常见的实现不同。常见的实现方式:通过轮播图的wrapper来实现item的切换效果,(也就是修改wrappertranslate3d属性来进行实现)。如果支持循环播放,需要在首部插入一个最后一个轮播轮播图的itemclone版, 以及在尾部插入一个第一个轮播图的clone版。
  • swipe组件实现的方式: 只显示当前显示的轮播图item, 当切换的时候, 显示处当前item的前后相邻的两个item; 通过设置三个item的translate3d来实现切换的效果。
  • 这两种方式对比:
  • 第一种方式, 初始会渲染出来所有的item, 通过translate3d来实现切换和滑动, 这种方式会启动硬件加速提升性能。但是毕竟是在所有轮播图上进行渲染的。
  • 第二种方式,通过切换item的display属性来实现对应item的显示和隐藏,虽然会引起回流和重绘,但是每个item的position为absolute,脱离文档流,所以并不会引起其他dom的回流和重绘。每个item的translate3d引发的渲染只是在当前item的基础上。
  • 通过上面分析,可以得出: 如果轮播图的数量不多,第一种方式不会引起回流和重绘,并且translate引发渲染的item不多,性能相对好;但是轮播图的数量比较多的话,第二种性能相对比较好。

  • swipe接入实例:
<div class="app">
    <div class="swipe-wrapper">
        <mt-swipe : auto="0" ref="swipeWrapper">
            <mt-swipe-item class="swipe-item-1 item">1</mt-swipe-item>
            <mt-swipe-item class="swipe-item-2 item">2</mt-swipe-item>
            <mt-swipe-item class="swipe-item-3 item">3</mt-swipe-item>
        </mt-swipe>
    </div>
    <div class="button-wrappper">
        <button class="prev-button flex-item" @click="prev">prev</button>
        <button class="prev-button flex-item" @click="next">next</button>
    </div>
</div>
  • css代码:
<link rel="stylesheet" href="../css/mint-style.css">
<style>
html, body {
    width: 100%;
    height: 100%;
    margin: 0;
    padding: 0;
}
#app {
    width: 100%;
    height: 100%;
}
.swipe-wrapper {
    width: 100%;
    height: 100%;
}
.swipe-item-1 {
    background: #ff0000;
}
.swipe-item-2 {
    background: #0000ff;
}
.swipe-item-3 {
    background: #347C17;
}
.item {
    text-align: center;
    font-size: 40px;
    color: #ffffff;
}
.button-wrapper {
    display: flex;
    height: 100px;
}
.flex-item {
    flex: 1;
    display: inline-block;
    text-align: center;
    height: 100%;
    line-height: 100%;
    font-size: 40px;
}
.prev-button {
    background: #000000;
}
.next-button {
    background: #347C17;
}
</style>
  • js代码:
<!-- 先引入vue -->
<script src="../js/vue.js"></script>
<!--- 引入组件库 -->
<script src="../js/index.js"></script>
<script>
    new Vue ({
        el: '#app',
        methods: {
            prev: function () {
                this.$refs.swipeWrapper.prev();
                console.log(this.$children);
            },
            next: function () {
                this.$refs.swipeWrapper.next();
            }
        }
    });
</script>
  • 原理解析

  • 初始只显示选中的indexitem, 将其他item隐藏。
  • 当拖动开始的时候, 显示当前的index的相邻两个item
  • 当拖动的时候,计算出手指滑动的距离, 通过设置当前的item和其相关的两个item的translate3d来改变他们位置的方式, 来实现切换的效果。
  • 自动播放:通过设置定时器,触发上面拖动相同的切换代码,来实现切换。

  • 源码解析

  • 首先需要查看子组件的swipe-item组件, 代码阅读起来倒不是很难,如下:
<template>
    <div class="mint-swipe-item">
        <slot></slot>
    </div>
</template>
<script>
    export default {
        name: 'mt-swipe-item',
        mounted () { //页面显示时触发 
            this.$parent && this.$parent.swipeItemCreated(this); // 内部实现: 调用父组件的`reinit()`业务
        },
        destroyed() { // item隐藏时触发
            this.$parent && this.$parent.swipeItemDestroyed(this); // 内部实现同上
        }
    };
</script>
  • 上面的这一部分源码,需要进行解析一下,其实就是分为mounteddestroyed, 并且这两部分都存在一个共同点,那就都是调用的父组件的实现。实现如下:
swipeItemCreated () {
    if (!this.ready) return;

    clearTimeout(this.reInitTimer);
    this.reInitTimer = setTimeout (()=>{
        this.reInitPages();
    }, 100); 
},

swipeItemDestroyed() {
    if (!this.ready) return;

    clearTimeout(this.reInitTimer);
    this.reInitTimer = setTimeout (() =>{
        this.reInitPages();
    }, 100);
},
  • 父组件swipeprops, 这一类的数据都是允许从外部导入的,而对于数据部分,这里就不再就行阐述了。详细swipe

  • swipedata说明:
data() {
    return {
        ready: false, //当前组件是否mounted
        dragging: false, //当前是否在拖动
        userScrolling: false, //判定当前用户在上下滚动,就不执行dragg。
        animation: false, //当前是否执行动画(也就是自动切换动画)。
        index: 0, // 当前所在的item的index
        pages: [], // 存储当前child的dom
        timer: null, // 自动播放的定时器timerid
        reInitTimer: null, // item组件自动触发reInit触发定时器id
        noDrag: false, // 存储是否运动拖动的标识
        isDone: false, // 当前动画是否执行完成
    }
}
  • 上面源码中的不存在注释, 注释的内容为分析源码之后,它的功能说明。

  • swipe组件的入口函数mounted回调的实现:
mounted () {
    this.ready = true;

    this.initTimer(); //初始化自动化播放的timer
    this.reInitPages(); //初始化drag状态,以及dom节点的样式信息

    var element = this.$el;
    // 为当前的dom节点,注册touch时间
    element.addEventListener('touchstart', (event) => {
        if (this.prevent) {
            event.preventDefault();
        }
        if (this.stopPropagation) {
            event.stopPropagation;
        }
        if (this.animation) return; // 如果当前在执行移动动画,直接返回。
        this.dragging = true; //设置dragging的状态意识
        this.userScrolling = false; //重置userScrolling的状态
        this.doOnTouchStart(event);
    });
    element.addEventListener('touchmove', (event) => {
        if(this.userScrolling) {//纵向滚动
            this.dragging = false;
            this.State = {};
            return;
        }
        if (!this.dragging) {
            return;
        }
        this.initTimer; // 启动自动播放定时器
        this.doOnTouchEnd(event);
        this.dragging = false; //重置拖动状态
    });
}
  • 关于初始化自动播放器的定时器代码,最后会进行分析。现在需要先来看一下dom样式的reInitPages函数实现如下:
 reInitPages() {
    var children = this.$children;
    // 设置拖动状态
    this.noDrag = children.length === 1 && this.noDragWhenSingle; // 当前只有一个item,并且设置了只有一个不支持拖动
    var pages = [];
    var intDefaultIndex = Math.floor(this.defaultIndex);
    var defaultIndex = (intDefaultIndex >= 0 && intDefaultIndex < children.length) ? intDefaultIndex : 0;
    this.index = defaultIndex; // 设置当前显示的索引值
    //初始化显示样式, 将当前index的item显示出来,其他的都隐藏
    children.forEach(function(child, index) {
      pages.push(child.$el);
      removeClass(child.$el, 'is-active');
      if (index === defaultIndex) {
        addClass(child.$el, 'is-active');
      }
    });
    // 设置所有轮播图的item的dom
    this.pages = pages;
  },

  • swipe的touchstart事件回调的处理
  • 上面已经有了回调的代码,主要看处理的核心函数doOnTouchStart的实现如下:
doOnTouchStart(event) { // 创建dragState, 包括touch事件的信息,当前drag item以及它前后两个item,并将其显示出来
    if (this.noDrag) return; // 不支持拖动

    var element = this.$el;
    var dragState = this.dragState;
    var touch = event.touches[0];
    // 设置dragstate的信息(也就是当前滑动的信息数据)
    dragState.startTime = new Date();
    dragState.startLeft = touch.pageX;
    dragState.startTop = touch.pageY;
    dragState.startTopAbsolute = touch.clientY;

    dragState.pageWidth = element.offsetWidth;
    dragState.pageHeight = element.offsetHeight;

    var prevPage = this.$children[this.index - 1];
    var dragPage = this.$children[this.index];
    var nextPage = this.$children[this.index + 1];

    if (this.continuous && this.pages.length > 1) { // 当前支持循环播放, 并且pages的长度大于1
      if (!prevPage) {
        prevPage = this.$children[this.$children.length - 1];
      }
      if (!nextPage) {
        nextPage = this.$children[0];
      }
    }

    dragState.prevPage = prevPage ? prevPage.$el : null;
    dragState.dragPage = dragPage ? dragPage.$el : null;
    dragState.nextPage = nextPage ? nextPage.$el : null;
    // 将当前index下的前后两个item显示出来
    if (dragState.prevPage) {
      dragState.prevPage.style.display = 'block';
    }

    if (dragState.nextPage) {
      dragState.nextPage.style.display = 'block';
    }
  }

  • 获取当前touchstart状态下面的拖动的状态信息(包括touch的信息,页面宽高,prev、current、next三个itemdom)。同时将prev、next显示出来。

  • touchmove事件回调的处理

doOnTouchMove(event) {
    if (this.noDrag) return;

    var dragState = this.dragState;
    var touch = event.touches[0];

    dragState.currentLeft = touch.pageX;
    dragState.currentTop = touch.pageY;
    dragState.currentTopAbsolute = touch.clientY;
    //计算滑动的距离
    var offsetLeft = dragState.currentLeft - dragState.startLeft;
    var offsetTop = dragState.currentTopAbsolute - dragState.startTopAbsolute;

    var distanceX = Math.abs(offsetLeft);
    var distanceY = Math.abs(offsetTop);
    // 判断是 竖向滚动,还是横向滚动
    if (distanceX < 5 || (distanceX >= 5 && distanceY >= 1.73 * distanceX)) {
      this.userScrolling = true; // 判定当前用户在上下滚动,就不执行drag动作
      return;
    } else {
      this.userScrolling = false;
      event.preventDefault(); // 阻止默认事件的触发,也就是点击事件的触发
    }
    // 设置最大的拖拽距离在当前dom里面
    offsetLeft = Math.min(Math.max(-dragState.pageWidth + 1, offsetLeft), dragState.pageWidth - 1);

    var towards = offsetLeft < 0 ? 'next' : 'prev'; // 拖动的方向的确定
    //prev方向: prev dom移动到指定的位置
    if (dragState.prevPage && towards === 'prev') {
      this.translate(dragState.prevPage, offsetLeft - dragState.pageWidth);
    }
    // current dom移动到指定的位置
    this.translate(dragState.dragPage, offsetLeft);
    // next方向: next dom 移动到指定的位置
    if (dragState.nextPage && towards === 'next') {
      this.translate(dragState.nextPage, offsetLeft + dragState.pageWidth);
    }
  }

  • 主要确定当前滚动不是竖向滚动,并确定滚动的方向以确定移动prev还是next。
  • 下面看translate移动dom的核心函数实现:
/**
   * @param element 要移动的dom节点
   * @param offset // dom移动的距离
   * @param speed 如果传递, 执行动画的移动; 没有,则直接translate执行的距离
   * @param callback 处理完成的回调函数
   */
  translate(element, offset, speed, callback) {
    if (speed) {
      this.animating = true; // 当前正在执行动画,此时不能拖拽
      element.style.webkitTransition = '-webkit-transform ' + speed + 'ms ease-in-out'; // transition过渡状态
      setTimeout(() => {
        element.style.webkitTransform = `translate3d(${offset}px, 0, 0)`;
      }, 50);

      var called = false;

      var transitionEndCallback = () => {
        if (called) return;
        called = true;
        this.animating = false; // 停止动画
        element.style.webkitTransition = '';
        element.style.webkitTransform = '';
        if (callback) {
          callback.apply(this, arguments); // 调用回调
        }
      };

      once(element, 'webkitTransitionEnd', transitionEndCallback); // 此事件只执行一次
      // 防止低版本android, 无法触发此事件
      setTimeout(transitionEndCallback, speed + 100); // webkitTransitionEnd maybe not fire on lower version android.
    } else {
      element.style.webkitTransition = '';
      element.style.webkitTransform = `translate3d(${offset}px, 0, 0)`;
    }
  }

  • 如果设置了speed,就会执行平滑的动画切换(speed是动画执行的时间);如果没有设置,直接移动到指定的位置,没有过渡效果。
  • touchend事件回调的实现
  • 分析其中核心代码doTouchEnd函数:
doOnTouchEnd() {
    if (this.noDrag) return;

    var dragState = this.dragState;

    var dragDuration = new Date() - dragState.startTime;
    var towards = null; // 决定下面进入哪个页面, null: 当前页面, prev: 前一个页面, next: 下一个页面

    var offsetLeft = dragState.currentLeft - dragState.startLeft;
    var offsetTop = dragState.currentTop - dragState.startTop;
    var pageWidth = dragState.pageWidth;
    var index = this.index;
    var pageCount = this.pages.length;

    // 判断当前是否是 tap事件(轻触事件)
    if (dragDuration < 300) {
      let fireTap = Math.abs(offsetLeft) < 5 && Math.abs(offsetTop) < 5;
      if (isNaN(offsetLeft) || isNaN(offsetTop)) {
        fireTap = true;
      }
      if (fireTap) {
        this.$children[this.index].$emit('tap'); // 当前轮播图item发送给外部的tab事件
      }
    }
    // 触发时长小于300ms,并且没有执行touchmove事件, 不处理
    if (dragDuration < 300 && dragState.currentLeft === undefined) return;

    if (dragDuration < 300 || Math.abs(offsetLeft) > pageWidth / 2) {
      towards = offsetLeft < 0 ? 'next' : 'prev';
    }

    if (!this.continuous) { // 当前不支持循环, 向前或向后 都回到当前页面
      if ((index === 0 && towards === 'prev') || (index === pageCount - 1 && towards === 'next')) {
        towards = null;
      }
    }

    if (this.$children.length < 2) {
      towards = null;
    }
    // 动画的方式切换到指定的item
    this.doAnimate(towards, {
      offsetLeft: offsetLeft,
      pageWidth: dragState.pageWidth,
      prevPage: dragState.prevPage,
      currentPage: dragState.dragPage,
      nextPage: dragState.nextPage
    });

    this.dragState = {};// 清空dragState
  }

  • 判断当前是否是tap事件,并且确定下面要切换到哪个item。
  • 下面来看doAnimate动画的方式切换的实现:
doAnimate(towards, options) {
    if (this.$children.length === 0) return;
    if (!options && this.$children.length < 2) return;

    var prevPage, nextPage, currentPage, pageWidth, offsetLeft;
    var speed = this.speed || 300;
    var index = this.index;
    var pages = this.pages;
    var pageCount = pages.length;

    if (!options) { // 没有options,是 自动播放或手动触发切换页面的处理
      pageWidth = this.$el.clientWidth;
      currentPage = pages[index];
      prevPage = pages[index - 1];
      nextPage = pages[index + 1];
      if (this.continuous && pages.length > 1) {
        if (!prevPage) {
          prevPage = pages[pages.length - 1];
        }
        if (!nextPage) {
          nextPage = pages[0];
        }
      }
      // 将 prevPage 和 nextPage 定位到应该的位置(也就是开始执行切换页面的位置)
      if (prevPage) {
        prevPage.style.display = 'block'; // 显示出来
        this.translate(prevPage, -pageWidth); // 移到当前index的前面
      }
      if (nextPage) {
        nextPage.style.display = 'block';
        this.translate(nextPage, pageWidth); // 移到当前index的后面
      }
    } else {
      prevPage = options.prevPage;
      currentPage = options.currentPage;
      nextPage = options.nextPage;
      pageWidth = options.pageWidth;
      offsetLeft = options.offsetLeft;
    }
    // 确定 要切换的item的索引
    var newIndex;

    var oldPage = this.$children[index].$el;

    if (towards === 'prev') {
      if (index > 0) {
        newIndex = index - 1;
      }
      if (this.continuous && index === 0) {
        newIndex = pageCount - 1;
      }
    } else if (towards === 'next') {
      if (index < pageCount - 1) {
        newIndex = index + 1;
      }
      if (this.continuous && index === pageCount - 1) {
        newIndex = 0;
      }
    }

    var callback = () => { // 动画完成的回调: 重置dom的样式信息
      if (newIndex !== undefined) {
        // 重置dom的样式信息
        var newPage = this.$children[newIndex].$el;
        removeClass(oldPage, 'is-active'); // is-active 设置当前item的display:block
        addClass(newPage, 'is-active');

        this.index = newIndex;
      }
      if (this.isDone) { // 切换了页面,向外部发送切换页面完成的事件
        this.end();
      }

      // 在touchStart 时设置的style中的display清空, 也就是使用class里面的display:none隐藏属性
      if (prevPage) {
        prevPage.style.display = '';
      }

      if (nextPage) {
        nextPage.style.display = '';
      }
    };

    setTimeout(() => {
      if (towards === 'next') { // 切换到下一页
        this.isDone = true;
        this.before(currentPage); // 执行切换页面之前,向外部发送事件
        this.translate(currentPage, -pageWidth, speed, callback);
        if (nextPage) {
          this.translate(nextPage, 0, speed);
        }
      } else if (towards === 'prev') { // 切换到上一页
        this.isDone = true;
        this.before(currentPage);
        this.translate(currentPage, pageWidth, speed, callback);
        if (prevPage) {
          this.translate(prevPage, 0, speed);
        }
      } else { // 回到当前页面,不切换页面
        this.isDone = false; // 当前没有进入到前一个页面和后一个页面, 还是回到当前页面
        this.translate(currentPage, 0, speed, callback);
        if (typeof offsetLeft !== 'undefined') {
          if (prevPage && offsetLeft > 0) {
            this.translate(prevPage, pageWidth * -1, speed);
          }
          if (nextPage && offsetLeft < 0) {
            this.translate(nextPage, pageWidth, speed);
          }
        } else {
          if (prevPage) {
            this.translate(prevPage, pageWidth * -1, speed);
          }
          if (nextPage) {
            this.translate(nextPage, pageWidth, speed);
          }
        }
      }
    }, 10);
  }

  • 此函数代码比较,但是不难,结合上面的注释,应该很容易读懂。实现:如果没有options(手动触发切换页面),会生成options中的信息(也就是下面处理需要用到的数据),并把prev和next两个dom定位到指定的位置;然后执行切换页面的操作,并且在结束的回调中重置相应dom的样式以及当前选中的index。
  • 下面来看 手动触发的切换页面的代码
  next() { // 切换到下一个页面
    this.doAnimate('next');
  },

  prev() { // 切换到上一个页面
    this.doAnimate('prev');
  }

  • 最后,来看自动播放的代码:
  initTimer() {
    if (this.auto > 0) {
      this.timer = setInterval(() => {
        // 如果不支持循环播放,并且当前播放到了 末尾位置,  停止定时器
        if (!this.continuous && (this.index >= this.pages.length - 1)) {
          return this.clearTimer();
        }
        if (!this.dragging && !this.animating) { // 没有在拖动, 也没有执行动画
          this.next(); // 播放下一个item
        }
      }, this.auto);
    }
  }
  • 读懂了doAnimate函数了,就很简单了。

  • 总结:
  • 1、了解了一种新的轮播图的实现方式
  • 2、两个轮播图的实现方式的差别以及性能的比较
  • 3、touch事件边界条件的处理,比如tap事件的判断,横纵向滚动的判断

Jackdan9 Thinking

Logo

前往低代码交流专区

更多推荐