首頁 > web前端 > js教程 > 主體

Vue做出內部組件輪播切換步驟詳解

php中世界最好的语言
發布: 2018-05-02 10:30:45
原創
2266 人瀏覽過

這次帶給大家Vue做出內部組件輪播切換步驟詳解,Vue做出內部組件輪播切換的注意事項有哪些,下面就是實戰案例,一起來看一下。

對於那些不需要路由的內部元件,在切換的時候希望增加一個輪播過渡的效果,效果如下:

##我們可以引入一個輪播組件,但是有個問題,通常輪播組件都會把所有的slide都渲染出來再進行切換,這樣就導致所有的資源都會觸發加載,這可能不是我們所期待的,畢竟如果slide比較多的情況需要一次性加載的圖片等資源太多了。所以我們可以手動簡單地寫一個,滿足需求即可。

現在一步一步來實作這個功能,先寫一個實作基本切換的demo.

#1. 實作切換##先用vue-cli建立一個工程鷹架,使用以下指令:

npm install -g vue-cli
vue init webpack slide-demo # 运行后router等都选择no
登入後複製

這樣就搭了一個webpack vue的工程,進入slide-demo目錄,查看src/App.vue,這個檔案是初始化工具提供的,是整個頁面的組件。還有一個src/components目錄,這個是放子組件的目錄。

在這個目錄裡面新建3個元件:task-1.vue、task-2.vue、task-3.vue,然後在App.vue裡面import進來,如下App.vue所示:

<script>
// import HelloWorld from './components/HelloWorld'
import Task1 from "./components/task-1";
import Task2 from "./components/task-2";
import Task3 from "./components/task-3";
export default {
  name: 'App',
  components: {
    Task1,
    Task2,
    Task3
  }
}
</script>
登入後複製

我們的資料格式questions是這樣的:

[{index: 1, type: 1, content: ''}, {index: 2, type: 1, content : ''},
{index: 3, type: 2, content: ''}, {index: 4, type: 3, content: ''}]


##它是一個數組,數組裡的每個元素代表每題,每題都有一個類型,如選擇題、填空題、判斷題等,分別對應上面的task-1、task-2、task-3,我們用一個currentIndex變數表示目前是在哪題,初始化為0,如下程式碼所示(加到App.vue裡面):

  data() {
    return {
      currentIndex: 0
    };
  },
  created() {
    // 请求question数据
    this.questions = [
      {index: 1, type: 1, question: ''}, /*...*/];
  },
登入後複製
透過改變currentIndex的值,從而切到一下題即下一個元件,要怎麼實現這個切換的效果呢?

可以使用Vue自訂的一個全域元件component,給合它的is屬性,達到動態改變元件的目的,如下程式碼所示:

<template>
<p id="app">
  <p class="task-container">
    <component :is="&#39;task-&#39; + questions[currentIndex].type" 
    </component>
  </p>
</p>
</template>
登入後複製
當currentIndex增加時,就會改變:is裡面的值,依序從task-1變成task-2、task-3等,這樣component就會換成對應的task元件。

接著,再加入一個切換到下一題的按鈕,在這個按鈕的回應函數裡面改變currentIndex的值。同時把question的資料傳給component:

<template>
<p id="app">
  <p class="task-container">
    <component :is="&#39;task-&#39; + questions[currentIndex].type" 
      :question="questions[currentIndex]"></component>
    <button class="next-question" @click="nextQuestion">下一题</button>
  </p>
</p>
</template>
登入後複製
回應函數nextQuestion實作如下:

methods: {
  nextQuestion() {
    this.currentIndex = (this.currentIndex + 1) 
        % this.questions.length;
  }
},
登入後複製
具體每個task的實作參考如task-1.vue範例:

<template>
<section>
  <h2>{{question.index}}. 选择题</h2>
  <p>{{content}}</p>
</section>
</template>
<script>
export default {
  props: ["question"]
}
</script>
登入後複製
最後的效果如下(加上題目內容):

2. 新增輪播切換效果

輪播切換通常是把所有的slide都拼起來,拼成一張長長的橫圖,然後改變這個橫圖在顯示容器裡面的位置,如老牌jQuery插件flipsnap.js,它是把所有的slide都float: left,形成一張長圖,然後改變這張長圖的translate值

,達到切換的目的。這個插件的缺點是沒有辦法從最後一張切回第一張,解決這個問題的方法之一是不斷地移動DOM:每次切的時候都把第一張移到最後一張的後面,這樣就實現了最後一張點下一張的時候回到第一張的目的,但是這樣移來移去地對性能消耗比較大,不是很優雅。另外一個輪播插件jssor slider,它也是把所有的slide都渲染出來,然後每次切換的時候都

動態地計算每張slide的translate的值 ,而不是整體長圖的位置,這樣就不用移動DOM節點,相對較優雅。還有很多Vue的輪播插件的實作也是類似上面提到的方式。

不管怎么样,上面的轮播模式都不太适用于我们的场景,其中一个是这种答题的场景不需要切回上一题,每道题做完就不能回去了,更重要的一个是我们 不希望一次性把所有的slide都渲染出来 ,这样会导致每张幻灯片里的资源都触发加载,就比如img标签虽然你把它display: none了,但是只要它的src是一个正常的url,它就会请求加载。 由于slide往往会比较多,就不使用这种轮播插件了。

还可以使用Vue自带的transition,但是transition的问题是,切下一个的时候,上一个不见了,因为被销毁了,只有下一个的动画,并且不能预加载下一个slide的资源。

所以我们手动实现一个。

我的想法是每次都准备两个slide,第1个slide是当前展示用的,第2个slide拼在它的后面,准备切过来,当第2个slide切过来之后,删掉第1个slide,然后在第2个的后面再接第3个slide,不断地重复这个过程。如果我们没有使用Vue,而是自己增删DOM,那么没什么问题,可以很任性地自己发挥。 使用Vue可以怎么优雅地实现这个功能呢

在上面一个component的基础上,再添加一个component,刚开始第1个component是当前展示的,而第2个component是拼在它右边的,当第2个切过去之后,就把第1个移到第2的后面,同时把内容改成第3个slide的内容,依此类推。使用Vue不太好动态地改DOM,但是可以 借助jssor slider的思想 ,不移动DOM,只是改变component的translate的值。

给其中一个component套一个next-task的类,具有这个类的组件就表示它是下一张要出现的,它需要translateX(100%),如下代码所示:

<template>
<p id="app">
  <p class="task-container">
    <component :is="&#39;task-&#39; + questions[currentIndex].type" 
      ></component>
    <component :is="&#39;task-&#39; + questions[currentIndex + 1].type" 
      class="next-task"></component>
  </p>
</p>
</template>
 
<style>
.next-task {
  display: none;
  transform: translateX(100%);
  /* 添加一个动画,当改变transform的值时就会触发这个动画 */
  transition: transform 0.5s ease;
}
</style>
登入後複製

上面代码把具有.next-task类的component隐藏了,这样是做个优化,因为display: none的元素只会构建DOM,不会进行layout和render渲染。

所以就把问题转换成怎么在这两个component之间,切换next-task的类。一开始next-task是在第2个,当第2个切过来之后,next-task变成加在第1个上面,这样轮流交替。

进而,发现一个规律,如果currentIndex是偶数话,如o、2、4…,那么next-task是加在第2个component的,而如果currentIndex是奇数,则next-task是加在第1个component的。所以可以根据currentIndex的奇偶性切换。

如下代码所示:

<template>
<p id="app">
  <p class="task-container">
    <component :is="&#39;task-&#39; + questions[evenIndex].type" 
      :class="{&#39;next-task&#39;: nextIndex === evenIndex}"
      ref="evenTask"></component>
    <component :is="&#39;task-&#39; + questions[oddIndex].type" 
      :class="{&#39;next-task&#39;: nextIndex === oddIndex}
      ref="oddTask"></component>
  </p>
</p>
</template>
 
<script>
 
export default {
  name: 'App',
  data() {
    return {
      currentIndex: 0, // 当前显示的index
      nextIndex: 1,  // 表示下一张index,值为currentIndex + 1
      evenIndex: 0,  // 偶数的index,值为currentIndex或者是currentIndex + 1
      oddIndex: 1   // 奇数的index,值同上
    };
  },
}
登入後複製

第1个component用来显示偶数的slide,第2个是用来显示奇数的slide(分别用一个evenIndex和oddIndex代表),如果nextIndex是偶数的,那么偶数的component就会有一个next-task的类,反之则亦然。然后在下一题按钮的响应函数里面改变这几个index的值:

methods: {
  nextQuestion() {
    this.currentIndex = (this.currentIndex + 1) 
      % this.questions.length;
    this._slideToNext();
  },
  // 切到下一步的动画效果
  _slideToNext() {
 
  }
}
登入後複製

nextQuestion函数可能还有其它一些处理,在它里面调一下_slideToNext函数,这个函数的实现如下:

_slideToNext() {
  // 当前slide的类型(currentIndex已经加1了,这里要反一下)
  let currentType = this.currentIndex % 2 ? "even" : "odd",
    // 下一个slide的类型
    nextType = this.currentIndex % 2 ? "odd": "even";
  // 获取下一个slide的dom元素
  let $nextSlide = this.$refs[`${nextType}Task`].$el;
  $nextSlide.style.display = "block";
  // 把下一个slide的translate值置为0,原本是translateX(100%)
  $nextSlide.style.transform = "translateX(0)";
  // 等动画结束后更新数据
  setTimeout(() => {
    this.nextIndex = (this.currentIndex + 1) 
      % this.questions.length;
    // 原本的next是当前显示的slide
    this[`${nextType}Index`] = this.currentIndex;
    // 而原本的current slide要显示下下张的内容了
    this[`${currentType}Index`] = this.nextIndex;
  }, 500);
}
登入後複製

代码把下一个slide的display改成block,并把它的translateX的值置为0,这个时候不能马上更新数据触发DOM更新,要等到下一个slide移过去的动画结束之后再开始操作,所以加了一个setTimeout,在回调里面调换nextTask的类,加到原本的current slide,并把它的内容置成下下张的内容。这些都是通过改变相应的index完成的。

这样基本上就完成了,但是我们发现一个问题,切是切过去了,就是没有动画效果。这个是因为从display: none变到display: block是没有动画的,要么改成visibility: hidden到visible,要么触发动画的操作加到$nextTick或者setTimeout 0里面,考虑到性能问题,这里使用第二种方案:

$nextSlide.style.display = "block";
// 这里使用setimeout,因为$nextTick有时候没有动画,非必现
setTimeout(() => {
  $nextSlide.style.transform = "translateX(0)";
  // ...
}, 0);
登入後複製

经过这样的处理之后,点下一题就有动画了,但是又发现一个问题,就是偶数的next-task会被盖住,因为偶数的是使用第一个component,它是会被第二个compoent盖住的,所以需要给它加一个z-index:

.next-task { 
  display: none;
  transform: translateX(100%);
  transition: transform 0.5s ease;
  z-index: 2;
}
登入後複製

这个问题还比较好处理,另外一个不太好处理的问题是:动画的时间是0.5s,如果用户点下一题的速度很快在0.5s之内,上面的代码执行就会有问题,会导致数据错乱。如果每次切到下一题之后按钮初始化都是disabled,因为当前题还没答,只有答了才能变成可点状态,可以保证0.5s的时间是够的,那么就可以不用考虑这种情况。但是如果需要处理这种情况呢?

3. 解决点击过快的问题

我想到两个方法,第一个方法是用一个sliding的变量标志当前是否是在进行切换的动画,如果是的话,点击按钮的时候就直接更新数据,同时把setTimeout 0.5s的计时器清掉。这个方法可以解决数据错乱的问题,但是切换的效果没有了,或者是切换到一半的时候突然就没了,这样体验不是很好。

第二个方法是延后切换,即如果用户点击过快的时候,把这些操作排队,等着一个个做切换的动画。

我们用一个数组表示队列,如果当前已经在做滑动的动画,则入队暂不执行动画,如下代码所示:

methods: {
  nextQuestion() {
    this.currentIndex = (this.currentIndex + 1) 
      % this.questions.length;
    // 把currentIndex插到队首
    this.slideQueue.unshift(this.currentIndex);
    // 如果当前没有滑动,则执行滑动
    !this.sliding && this._slideToNext();
  },
}
登入後複製

每次点击按钮都把待处理的currentIndex插到队列里面,如果当前已经在滑动了,则不立刻执行,否则执行滑动_slideToNext函数:

_slideToNext() {
  // 取出下一个要处理的元素
  let currentIndex = this.slideQueue.pop();
  // 下一个slide的类型
  let nextType = currentIndex % 2 ? "odd" : "even";
  let $nextSlide = this.$refs[`${nextType}Task`].$el;
  $nextSlide.style.display = "block";
  setTimeout(() => {
    $nextSlide.style.transform = "translateX(0)";
    this.sliding = true;
    setTimeout(() => {
      this._updateData(currentIndex);
      // 如果当前还有未处理的元素,
      // 则继续处理即继续滑动
      if (this.slideQueue.length) {
        // 要等到两个component的DOM更新了
        this.$nextTick(this._slideToNext);
      } else {
        this.sliding = false;
      }
    }, 500);
  }, 0);
},
登入後複製

这个函数每次都先取出当前要处理的currentIndex,然后接下来的操作和第2点提到的一样,只是在0.5s动画结束后的异步回调里面需要判断一下,当前队列是否还有未处理的元素,如果有的话,需要继续执行_slideToNext,直到队列空了。这个执行需要挂在nextTick里面,因为需要等到两个component的DOM更新了才能操作。

这样理论上就没问题了,但实际上还是有问题,感受如下:

我们发现有些slide没有过渡效果,而且不是非必现的,没有规律。经过一番排查,发现如果把上面的nextTick改成setTimeout情况就会好一些,并且setTimeout的时间越长,就越不会出现失去过渡效果的情况。但是这个不能从根本上解决问题,这里的原因应该是Vue的自动更新DOM和transition动画不是很兼容,有可能是Vue的异步机制问题,也有可能是JS结合transition本身就有问题,但以前没有遇到过,具体没有深入排查。不管怎么样,只能放弃使用CSS的transition做动画。

如果有使用jQuery的话,可以使用jQuery的animation,如果没有的话,那么可以使用原生dom的animate函数,如下代码所示:

_slideToNext(fast = false) {
  let currentIndex = this.slideQueue.pop();
  // 下一个slide的类型
  let nextType = currentIndex % 2 ? "odd" : "even";
  // 获取下一个slide的dom元素
  let $nextSlide = this.$refs[`${nextType}Task`].$el;
  $nextSlide.style.display = "block";
  this.sliding = true;
  // 使用原生animate函数
  $nextSlide.animate([
    // 关键帧
    {transform: "translateX(100%)"},
    {transform: "translateX(0)"}
  ], {
    duration: fast ? 200 : 500,
    iteration: 1,
    easing: "ease"
  // 返回一个Animate对象,它有一个onfinish的回调
  }).onfinish = () => {
    // 等动画结束后更新数据
    this._updateData(currentIndex);
    if (this.slideQueue.length) {
      this.$nextTick(() => {
        this._slideToNext(true);
      });
    } else {
      this.sliding = false;
    }
  };
},
登入後複製

使用animate函数达到了和transition同样的效果,并且还有一个onfinish的动画结束回调函数。上面代码还做了一个优化,如果用户点得很快的时候,缩短过渡动画的时间,让它切得更快一点,这样看起来更自然一点。使用这样的方式,就不会出现transition的问题了。最后的效果如下:

这个体验感觉已经比较流畅了。

原生animate不兼容IE/Edge/Safari,可以装一个polyfill的库,如这个 web-animation ,或者使用其它一些第三方的动画库,或自己用setInterval写一个。

如果你要加上一题的按钮,支持返回上一题,那么可能需要准备3个component,中间那个用于显示,左右两边各跟着一个,准备随时切过来。具体读者可以自行尝试。

这种模式除了答题的场景,还有多封邮件预览、PPT展示等都可以用到,它除了有一个过渡的效果外,还能提前预加载下一个slide需要的图片、音频、视频等资源,并且不会像传统的轮播插件那样一下子把所有的slide都渲染了。适用于slide比较多的情况,不需要太复杂的切换动画。

相信看了本文案例你已经掌握了方法,更多精彩请关注php中文网其它相关文章!

推荐阅读:

vue-cli怎样做出跨域请求

nodejs中如何使用websocket(附代码)

以上是Vue做出內部組件輪播切換步驟詳解的詳細內容。更多資訊請關注PHP中文網其他相關文章!

相關標籤:
來源:php.cn
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
最新問題
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板
關於我們 免責聲明 Sitemap
PHP中文網:公益線上PHP培訓,幫助PHP學習者快速成長!