Maison  >  Article  >  interface Web  >  Comment utiliser Vue pour obtenir un effet de commutation de carrousel de composants internes

Comment utiliser Vue pour obtenir un effet de commutation de carrousel de composants internes

php中世界最好的语言
php中世界最好的语言original
2018-05-26 15:35:591910parcourir

Cette fois, je vais vous montrer comment utiliser Vue pour obtenir l'effet de commutation de carrousel de composants internes, et quelles sont les précautions à prendre pour utiliser Vue pour obtenir l'effet de commutation de carrousel de composants internes. Ce qui suit est un cas pratique, prenons. un regard.

Pour les composants internes qui ne nécessitent pas de routage, vous souhaitez ajouter un effet de transition carrousel lors de la commutation. L'effet est le suivant :

Nous pouvons. introduisez un composant Carrousel, mais il y a un problème. Habituellement, le composant carrousel restituera toutes les diapositives, puis les changera, ce qui déclenchera le chargement de toutes les ressources. Ce n'est peut-être pas ce à quoi nous nous attendons, après tout, s'il y a beaucoup de diapositives. Il y a trop d’images et d’autres ressources qui doivent être chargées en même temps. Nous pouvons donc simplement en écrire un manuellement pour répondre aux besoins.

Implémentons maintenant cette fonction étape par étape, écrivons d'abord une démo pour implémenter la commutation de base.

1 Implémenter la commutation

Utilisez d'abord vue-. cli Pour créer un échafaudage de projet, utilisez la commande suivante :

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

De cette façon, un projet webpack + vue est construit. Entrez dans le répertoire slide-demo et affichez src/App.vue. fourni par l’outil d’initialisation. C’est un composant de la page entière. Il existe également un répertoire src/components, qui est le répertoire dans lequel sont placés les sous-composants.

Créez trois nouveaux composants dans ce répertoire : task-1.vue, task-2.vue, task-3.vue, puis importez-les dans App.vue, comme indiqué dans App.vue ci-dessous :

<script>
// import HelloWorld from &#39;./components/HelloWorld&#39;
import Task1 from "./components/task-1";
import Task2 from "./components/task-2";
import Task3 from "./components/task-3";
 
export default {
  name: &#39;App&#39;,
  components: {
    Task1,
    Task2,
    Task3
  }
}
</script>

Nos questions sur le format des données sont les suivantes :

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

c'est un tableau. Chaque élément du tableau représente chaque question. Chaque question a un type, tel que des questions à choix multiples, des questions à remplir, des questions de jugement, etc., qui correspondent à la tâche-1 ci-dessus. -2 et task-3 respectivement. Nous utilisons une variable currentIndex pour indiquer sur quelle question nous nous trouvons actuellement, initialisée à 0, comme indiqué dans le code suivant (ajouté à App.vue) :

  data() {
    return {
      currentIndex: 0
    };
  },
  created() {
    // 请求question数据
    this.questions = [
      {index: 1, type: 1, question: &#39;&#39;}, /*...*/];
  },

. En modifiant la valeur de currentIndex, nous pouvons passer à la question suivante, c'est-à-dire au composant suivant, comment obtenir cet effet de commutation ?

Vous pouvez utiliser un composant de composant global personnalisé par Vue et le combiner avec son attribut is pour atteindre l'objectif de changement dynamique du composant, comme indiqué dans le code suivant :

<template>
<p id="app">
  <p class="task-container">
    <component :is="&#39;task-&#39; + questions[currentIndex].type" 
    </component>
  </p>
</p>
</template>

Quand currentIndex augmente, cela changera la valeur dans:is, de la tâche-1 à la tâche-2, à la tâche-3, etc., de sorte que le composant soit remplacé par le composant de tâche correspondant.

Ensuite, ajoutez un bouton pour passer à la question suivante, et modifiez la valeur de currentIndex dans la fonction de réponse de ce bouton. En même temps, transmettez les données de la question au composant :

<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>

La fonction de réponse nextQuestion est implémentée comme suit :

methods: {
  nextQuestion() {
    this.currentIndex = (this.currentIndex + 1) 
        % this.questions.length;
  }
},

La référence d'implémentation spécifique de chaque tâche est à titre d'exemple dans task-1.vue :

<template>
<section>
  <h2>{{question.index}}. 选择题</h2>
  <p>{{content}}</p>
</section>
</template>
<script>
export default {
  props: ["question"]
}
</script>

L'effet final est le suivant (plus le contenu de la question) :

2. effet de commutation

Le changement de carrousel implique généralement de rassembler toutes les diapositives pour former une longue image horizontale, puis de changer la position de l'image horizontale dans le conteneur d'affichage. Par exemple, l'ancien plug-in jQuery. flipsnap.js fait flotter toutes les diapositives : à gauche, formez une longue image, puis modifiez la valeur de traduction de cette longue image pour atteindre l'objectif de commutation. L'inconvénient de ce plug-in est qu'il n'y a aucun moyen de revenir de la dernière image à la première image. Une façon de résoudre ce problème est de déplacer constamment le DOM : à chaque fois que vous changez, déplacez la première image derrière la dernière. image, de sorte que Cela permet de revenir à la première image en cliquant sur la dernière, mais la déplacer ainsi consomme beaucoup de performances et n'est pas très élégant. Un autre plug-in de carrousel, jssor slider, restitue également toutes les diapositives, puis calcule dynamiquement la valeur de traduction de chaque diapositive à chaque fois qu'elle est commutée, au lieu de la position de l'image longue globale. Cela élimine le besoin. pour déplacer les nœuds DOM, ce qui est relativement élégant. Il existe également de nombreux plug-ins de carrousel Vue qui sont implémentés de la même manière que celle mentionnée ci-dessus.

Quoi qu'il en soit, le mode carrousel ci-dessus ne convient pas à nos scénarios. L'un d'entre eux est que ce type de scénario de questions-réponses ne nécessite pas de revenir à la question précédente. Vous ne pouvez pas revenir en arrière après avoir terminé chacun d'entre eux. question. Le plus important est que nous ne voulons pas afficher toutes les diapositives en même temps , ce qui entraînerait le déclenchement du chargement des ressources de chaque diapositive, comme la balise img, même si vous la configurez pour qu'elle s'affiche. : none , mais tant que son src est une URL normale, il demandera le chargement. Comme il y a souvent de nombreuses diapositives, ce plug-in carrousel n'est pas utilisé.

Vous pouvez également utiliser la transition fournie avec Vue, mais le problème avec la transition est que lorsque vous coupez la suivante, la précédente disparaît car elle est détruite. Il n'y a que l'animation de la suivante, et la diapositive suivante ne peut pas être préchargée.

Nous en implémentons donc un manuellement.

我的想法是每次都准备两个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: &#39;App&#39;,
  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中文网其它相关文章!

推荐阅读:

怎样使用Angular5为组件标签添加样式class

如何使用vue-cli开发环境实现跨域请求

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn