今回は、Vue を使用して内部コンポーネントのカルーセル切り替え効果を実現する方法と、Vue を使用して内部コンポーネントのカルーセル切り替え効果を実現する方法の注意点を説明します。以下は実際的なケースです。 。
ルーティングを必要としない内部コンポーネントについては、切り替え時にカルーセル トランジション効果を追加したいと考えています。その効果は次のとおりです。
カルーセル コンポーネントを導入できますが、通常、カルーセルには問題があります。すべてのスライドをレンダリングしてからそれらを切り替えると、すべてのリソースの読み込みがトリガーされます。結局のところ、スライドが多い場合、読み込む必要がある画像やその他のリソースが多すぎることになります。一度。したがって、ニーズに合わせて手動で作成するだけで済みます。
次に、この関数を段階的に実装してみましょう。まず、基本的なスイッチングを実装するデモを作成します。
1. スイッチングを実装するには、
まず、vue-cli を使用してエンジニアリング スキャフォールディングを構築し、次のコマンドを使用します。これで完了です。webpack + vue プロジェクトの場合は、slide-demo ディレクトリに入り、src/App.vue を表示します。このファイルは初期化ツールによって提供され、ページ全体のコンポーネントです。 src/components ディレクトリもあります。これは、サブコンポーネントが配置されるディレクトリです。
以下の App.vue に示すように、このディレクトリに 3 つの新しいコンポーネント (task-1.vue、task-2.vue、task-3.vue) を作成し、App.vue にインポートします。データ形式の質問は次のとおりです:
[{index: 1, type: 1, content: ''}, {index: 2, type: 1, content: ''},{index: 3, type: 2 、コンテンツ: ''}、{インデックス: 4、タイプ: 3、コンテンツ: ''}]
これは配列であり、配列内の各要素は各質問を表し、各質問には複数のタイプがあります。選択問題、穴埋め問題、判断問題などは、それぞれ上記のタスク 1、タスク 2、およびタスク 3 に対応します。 currentIndex 変数を使用して現在どの問題を行っているかを示し、初期化します。次のコード (App.vue に追加) に示すように、それを 0 にします。
currentIndex の値を変更すると、次のコンポーネント、つまり次のコンポーネントに切り替えることができます。この切り替え効果を実現するにはどうすればよいでしょうか。npm install -g vue-cli vue init webpack slide-demo # 运行后router等都选择noログイン後にコピー
次のコードに示すように、Vue によってカスタマイズされたグローバル コンポーネント コンポーネントを使用し、その is 属性と組み合わせることで、コンポーネントを動的に変更するという目的を達成できます:
<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>
currentIndex が増加すると、in:is の値は次のようになります。順番に変更されました。タスク 1 からタスク 2、タスク 3 などに変更され、コンポーネントが対応するタスク コンポーネントに置き換えられます。
次に、次の質問に切り替えるボタンを追加し、このボタンの応答関数のcurrentIndexの値を変更します。同時に、質問データをコンポーネントに渡します:
data() { return { currentIndex: 0 }; }, created() { // 请求question数据 this.questions = [ {index: 1, type: 1, question: ''}, /*...*/]; },
応答関数 nextQuestion は次のように実装されます:
<template> <p id="app"> <p class="task-container"> <component :is="'task-' + questions[currentIndex].type" </component> </p> </p> </template>
各タスクの具体的な実装リファレンスは、task-1.vue の例のようなものです:
<template> <p id="app"> <p class="task-container"> <component :is="'task-' + questions[currentIndex].type" :question="questions[currentIndex]"></component> <button class="next-question" @click="nextQuestion">下一题</button> </p> </p> </template>
最終的な効果は次のとおりです (および質問の内容):
2. カルーセル切り替え効果を追加します
カルーセル切り替えは通常、すべてのスライドを結合して長い水平画像を形成し、この水平画像の位置を変更します。古いスタイルの jQuery プラグイン flicksnap.js などの表示コンテナーでは、すべてのスライドを float: left に設定して長い画像を形成し、その後、切り替えの目的を達成するために この長い画像の翻訳値を変更します 。このプラグインの欠点は、最後の画像から最初の画像に戻す方法がないことです。この問題を解決する 1 つの方法は、DOM を常に移動することです。切り替えるたびに、最初の画像を最後の画像の後ろに移動します。これにより、最後の画像をクリックしたときに最初の画像に戻るという目的は達成されますが、このように移動するとパフォーマンスが大幅に消費され、あまりエレガントではありません。別のカルーセル プラグインである jssor slider もすべてのスライドをレンダリングし、スライドが切り替わるたびに、長い画像全体の位置ではなく、各スライドの移動値を動的に計算するため、スライドを移動する必要はありません。 DOM ノードは比較的エレガントです。上記と同様の方法で実装された Vue カルーセル プラグインも多数あります。
いずれにせよ、上記のカルーセル モードはこのシナリオには適していません。その 1 つは、この種の質問に答えるシナリオでは、各質問を完了した後に戻る必要がないということです。さらに重要なのは、すべてのスライドを一度にレンダリングしたくないためです。これにより、たとえば、img タグを show: none に設定しても、その src が読み込まれます。が通常の URL の場合、ロードを要求します。 スライドが多い場合が多いので、このカルーセルプラグインは使用しません。 Vue に付属のトランジションを使用することもできますが、トランジションの問題は、次のトランジションをカットすると、前のトランジションが破棄されて消えてしまうということです。次のトランジションのアニメーションと、次のトランジションのリソースしか存在しません。次のスライドをプリロードすることはできません。 そこで、手動で実装します。
我的想法是每次都准备两个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="'task-' + questions[currentIndex].type" ></component> <component :is="'task-' + 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="'task-' + questions[evenIndex].type" :class="{'next-task': nextIndex === evenIndex}" ref="evenTask"></component> <component :is="'task-' + questions[oddIndex].type" :class="{'next-task': 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 を使用して内部コンポーネントのカルーセル切り替え効果を実現する方法の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。