混合
组件系统是Vue的核心,如何合理的规划组件,是我们在开发中需要深入思考的问题,我个人习惯把一些组件使用逻辑和方法,从组件中拆分出来,放到独立文件,最后通过混合的方式,引入相关逻辑,这样可以在一定层面上把组件再进行一次解耦,做到只要切换不同的混合文件就能提供新功能的能力
// 在hello-world项目目录下,建立basic.vue,在其中输入下面内容
<template>
</template>
<script>
export default {
data () {
return {
basicInfo: 'basicInfo'
}
},
created () {
console.log('from basic');
}
}
</script>
...
// 在index.vue中引入这个文件,并且进行混合
import basic from './basic.vue';
...
export default {
mixins: [basic],
...
// 在index.vue的模板中添加下面内容
<p>{{basicInfo}}</p>
此时在页面可以正常渲染出basicInfo的插值,还会打印出basic中的log信息,下面做如下改造:
// 在basic.vue下,添加sayInfo方法
methods: {
sayInfo () {
console.log(`from basic.vue,info:${this.basicInfo}`);
}
}
...
// 在index.vue的模板下,做下面调整
<p @click="sayInfo">{{basicInfo}}</p>
这时如果你点击页面就会发现控制台输出定义在basic.vue中的信息,可以假设如果我们切换成另外一个也有sayInfo的组件,这时点击页面就会响应另外一个组件中的同名事件,这样就达到了切换不同混合逻辑,组件功能也就不同
// 直接调用被混入对象的值,在basic.vue中是没有info的值,但是因为在index.vue中有info值,这时点击元素一样可以正常输出info的值
sayInfo () {
console.log(`from basic.vue,info:${this.basicInfo}`);
console.log(this.info);
}
进行混合时有下面几个规则需要注意:
- 当组件和引入的混合组件有数据冲突时,会以组件的数据为准
// 在basic.vue的文件中,添加info数据
data () {
return {
basicInfo: 'basicInfo',
info: 'from basic'
}
},
此时按普通组件的逻辑分析,sayInfo应该输出basic.vue的info值,但是因为组件数据的优先级高于混合组件,当出现数据冲突时,保留组件的数据,所以此时info的值依然还是index.vue中定义的info值,data,methods,computed,directives(值为对象的选项)中的同名值都会产生覆盖
- 部分钩子函数数据会产生合并为不是覆盖
// index.vue的模板做如下改造
<p @click="sayInfo">{{basicInfo}}</p>
<button @click="changeInfo">改变info值</button>
...
// index.vue添加方法
methods: {
changeInfo () {
this.info = 'change Info';
}
}
...
// index.vue添加对info数据的监听
watch: {
info () {
console.log('watch basic.vue info值发生改变');
}
},
...
// index.vue的created钩子函数添加如下内容
created () {
console.log('from index.vue created');
},
...
// basic.vue的created钩子函数添加如下内容
created () {
console.log('from basic');
},
// basic.vue添加对info监听的watch方法
watch: {
info () {
console.log('watch basic.vue info值发生改变');
}
},
在控制台会输出
from basic
from index.vue created
...
watch basic.vue info值发生改变
watch index.vue info值发生了变化
同名钩子函数将混合为一个数组,因此都将被调用。混入对象的钩子将在组件自身钩子之前调用。
上面介绍的都是在组件内的进行混合,Vue提供了Vue.mixin用来定义进行全局混合,但这种方式太过暴力,会影响所有的组件实例以及第三方模板,如果有要在全局添加相关功能的需求可以使用插件来添加。
插件
插件通常会为 Vue 添加全局功能。插件的范围没有限制——一般有下面几种:
- 添加全局方法或者属性
- 添加全局资源:指令/过滤器/过渡等
- 通过全局 mixin 方法添加一些组件选项
- 添加 Vue 实例方法,通过把它们添加到 Vue.prototype 上实现
- 一个库,提供自己的 API,同时提供上面提到的一个或多个功能
Vue的作者在插件编写方面提供了很好的封装,我们可以很容易的编写出一个Vue插件。
// 在项目下新建一个plugin.js,在其中编写如下内容
let plugin = {};
plugin.install = (Vue, options) => {
Vue.$msg = 'from plugin';
};
module.exports = plugin;
...
// 在项目的index.js中引入这个文件,并通过Vue.use使用这个插件
import plg from './plugin'; // 引入plugin.js插件
Vue.use(plg);
console.log('Vue' + Vue.$msg);
这时在控制台就能输出,我们刚刚在插件中定义的信息,通过上面的例子,我们可以看出Vue的插件应当有一个公开方法install。这个方法的第一个参数是 Vue 构造器,第二个参数是一个可选的选项对象,在方法内部可以定义如下几种信息:
MyPlugin.install = function (Vue, options) {
// 1\. 添加全局方法或属性
Vue.myGlobalMethod = function () {
// 逻辑...
}
// 2\. 添加全局资源
Vue.directive('my-directive', {
bind (el, binding, vnode, oldVnode) {
// 逻辑...
}
...
})
// 3\. 注入组件
Vue.mixin({
created: function () {
// 逻辑...
}
...
})
// 4\. 添加实例方法
Vue.prototype.$myMethod = function (methodOptions) {
// 逻辑...
}
}
全局方法或全局属性就如上例一样,加挂到Vue上,需要通过访问Vue来访问绑定到全局的方法和属性。在组件内部要访问绑定到Vue的全局变量,需要做如下改造:
// 引入Vue,以及插件
import Vue from 'vue';
import plg from './plugin'; // 引入plugin.js插件
Vue.use(plg);
...
// 这时在组件内部也可以正常访问绑定在Vue的全局方法或全局属性
mounted () {
console.log('Vue' + Vue.$msg);
}
如果要加到实例上,可以使用Vue.prototype添加
plugin.install = (Vue, options) => {
Vue.prototype.$msg = 'from plugin';
};
...
// 此时在在实例内部可以使用this进行访问
mounted () {
console.log('Vue' + this.$msg);
},
在使用组件时可以参入参数
Vue.use(plg, {value:'from index.js'});
...
// 在插件中就可以通过options进行使用
Vue.prototype.$msg = options.value;
过渡
Vue 在插入、更新或者移除 DOM 时,提供多种不同方式的应用过渡效果。
单元素/组件过渡
Vue 提供了 transition 的封装组件,在下列情形中,可以给任何元素和组件添加 entering/leaving过渡
- 条件渲染 (使用 v-if)
- 条件展示 (使用 v-show)
- 动态组件
- 组件根节点
// 在index.vue的模板页中添加如下代码
<button @click="changeShow">过渡效果</button>
<transition>
<p v-if="showFlag">过渡区域</p>
</transition>
...
// index.vue的methods下添加下面方法
methods: {
changeShow () {
this.showFlag = !this.showFlag;
}
}
这时点击按钮p标签会在隐藏和显示中切换,不过这时没有过渡的效果,如果要加过渡效果可以添加name属性。
<transition name="fade">
...
// 在index.vue中添加style标签,在其中添加相关样式文件
<style>
.fade-enter-active, .fade-leave-active {
transition: opacity 1s;
}
.fade-enter, .fade-leave-to {
opacity: 0;
}
.fade-enter-to, .fade-leave {
opacity: 1;
color: red;
}
</style>
当插入或删除包含在 transition 组件中的元素时,Vue 将会做以下处理:
-
自动嗅探目标元素是否应用了 CSS 过渡或动画,如果是,在恰当的时机添加/删除 CSS 类名。
在进入/离开的过渡中,会有 6 个 class 切换。v-enter:定义进入过渡的开始状态。在元素被插入时生效,在下一个帧移除。
v-enter-active:定义过渡的状态。在元素整个过渡过程中作用,在元素被插入时生效,在 transition/animation 完成之后移除。这个类可以被用来定义过渡的过程时间,延迟和曲线函数。
v-enter-to: 2.1.8版及以上 定义进入过渡的结束状态。在元素被插入一帧后生效 (与此同时 v-enter 被删除),在 transition/animation 完成之后移除。
v-leave: 定义离开过渡的开始状态。在离开过渡被触发时生效,在下一个帧移除。
v-leave-active:定义过渡的状态。在元素整个过渡过程中作用,在离开过渡被触发后立即生效,在 transition/animation 完成之后移除。这个类可以被用来定义过渡的过程时间,延迟和曲线函数。
v-leave-to: 2.1.8版及以上 定义离开过渡的结束状态。在离开过渡被触发一帧后生效 (与此同时 v-leave 被删除),在 transition/animation 完成之后移除。
class的切换可以简单理解成,当元素刚插入时,会插入v-enter(v-就是transition中的name属性)和v-enter-active两个class,然后在下一帧会删除v-enter,添加上v-enter-to,最后在transition/animation执行完成后,会直接删除v-enter-to和v-enter-active这两个class,当元素离开时和进入时一致,刚开始离开就添加v-leave和v-leave-active这两个class,然后在下一帧就删除v-leave添加v-leave-to。在显示和离开的整个过程中v-enter-active和v-leave-active是一直存在的,所以才会说在这两个类中定义过渡时间,延迟和曲线函数,结合上面的例子,我们来分析下,整个执行过程:
- 当我们点击按钮时,页面中的p标签进入离开状态,所以Vue会在p标签上创建两个class,fade-leave、fade-leave-active
- 进到下一帧,fade-leave会被删除,fade-leave-to会被添加,所以在点击标签的时候才会有字体会猛的变成红色,然后又变回黑色,然后按照fade-leave-active设定的动画,逐渐消失。
- 文字消失后,我们再次点击按钮,文字会先成红色,然后逐渐展示出来,最后变成黑色,这也是因为整个class添加的顺序是,先添加fade-enter,fade-enter-active然后再添加fade-enter-to,最后在全部删除,分析相关的样式代码,我们能很清晰的看出正规逻辑的执行过程。
按官网的说法,由于动画的不同,Vue针对transition和animation在添加和移除相关class的时机会有一些区别,这个区别主要集中在v-enter类名上,下面做如下改造,来验证这个说法:
// 在index.vue的style下,添加下面代码
.bounce-enter {
color: red;
font-size: 28px;
}
.bounce-enter-active {
animation: bounce-in 2s;
}
.bounce-leave {
color: red;
}
.bounce-leave-active {
animation: bounce-in 2s reverse;
}
@keyframes bounce-in {
0% {
opacity: 0.5;
}
100% {
opacity: 1;
}
}
...
// 对模板中的代码也做相关调整,animationstart和animationend是动画开始和结束的监听事件
<transition name="bounce">
<p @animationstart="startEvent" @animationend="endEvent" v-if="showFlag">过渡区域</p>
</transition>
...
// 添加两个动画的监听事件
startEvent (e) {
console.log('动画执行开始');
console.log(e.srcElement.className);
console.log(new Date().getTime());
console.log(this.showFlag);
},
endEvent (e) {
console.log('动画执行完毕');
console.log(e.srcElement.className);
console.log(new Date().getTime());
console.log(this.showFlag);
}
通过控制台的输出,我们可以很明显的看出当发生过渡时,class的变化,当元素移除时,class变化是先为:bounce-leave bounce-leave-active,然后再变为:bounce-leave-active bounce-leave-to,元素添加时,class的变化是先为:bounce-enter bounce-enter-active,然后再变为bounce-enter-active bounce-enter-to,这个值的变化也验证了,我们之前所说的执行逻辑。如果你仔细观察页面中元素的变化也能看出对应的样式也是生效的,不过这里有问题,当你多点击几次,会发现好像v-enter对应的样式并不是每一次都生效,有时会感觉Vue在执行时会忽略v-enter/v-leave的样式,这个问题暂时还没想明白,不知道是不是我设置的样式的问题?
另外要注意的是必须在v-enter-active、v-leave-active或v-enter-to、v-leave-to中设置transition或animation动画,要不整个过渡效果看起来像是没有执行一样,页面很快的发生变化,可以做如下的验证:
// 隐藏transition
.fade-enter-active, .fade-leave-active {
/*transition: opacity 2s;*/
color: #00aceb;
font-size: 30px;
}
此时页面如预期一样,发生了很快的变化,完全看不出有什么过渡效果
// 这时虽然可以这样改造,让过渡继续生效,不过从整个执行过程上来看,显然没有放到active上好
.fade-enter-to, .fade-leave-to {
transition: opacity 2s;
}
除了可以使用transition对应的name自动生成的class,还可以指定某个class:
enter-class // 替换v-enter
enter-active-class // 替换v-enter-active
enter-to-class // 替换v-enter-to
leave-class // 替换v-leave
leave-active-class // 替换v-leave-active
leave-to-class // 替换v-leave-to
可以做如下验证:
<transition name="fade" enter-active-class="intoClass" enter-to-class="externalClass">
...
.externalClass {
color: #faf2cc;
}
.intoClass {
transition: opacity 2s;
}
这时你切换页面会发现当元素重新插入时,绑定的class就变成了intoClass和externalClass,这种规则,对引入外部动画库有比较友好的支持,只需要把需要执行的相关class进行替换。
使用type设定Vue监听动画的类型
过渡效果如果只是transition或者只是animation,Vue可以很明显的知道要监听什么,但当过渡和动画同时存在时,如果不指定监听类型,Vue就不能明确的知道监听哪个。
// 样式文件做相关修改,过渡和动画同时存在
.fade-enter-active, .fade-leave-active {
transition: opacity 5s;
animation: bounce-in 2s reverse;
}
@keyframes bounce-in {
0% {
transform: scale(0);
}
50% {
transform: scale(1.5);
}
100% {
transform: scale(1);
}
}
这时点击页面如果移除元素,会发现class在transition执行完成才会移除,如果此时是以transition为主,是没有问题的,但如果想以animation为主,就要设定type值
// type值可以为animation,transition
<transition name="fade" type="animation">
这时再点击页面会发现此时过渡效果是以animation的动画为准,如果你希望动画要持续足够长的时间,可以保证所有动画过渡都执行完,可以使用duration属性,设定过渡持续时间。
// duration单位是毫秒,下面这样设定表示,过渡持续8秒(进入和离开都是8秒)
<transition name="fade" type="animation" :duration="8000">
...
// 还可以设定的更详细,{enter: 2000, leave: 8000}进入时过渡时间持续2秒,离开时持续8秒
<transition name="fade" type="animation" :duration="{enter: 2000, leave: 8000 }">
- 如果过渡组件提供了 JavaScript 钩子函数,这些钩子函数将在恰当的时机被调用。
除了普通的css动画库还有以js为主的动画库,Vue提供这方面的能力,估计很大一方面就是让Vue能和这些js库结合,使用js钩子函数会使动画操作更为准确,能比较精确的控制在某些状态下页面行为,这是css动画不好实现的地方,官方文档在这部分使用的是Velocity.js这个动画库(隐隐的神伤。。想写原生的动画,怎么实现都有问题,标记下回头找找原因)
// 在项目最外层目录运行下面命令,安装velocity-animate动画库
npm install velocity-animate --save
...
// 在index.vue中引入这个库
import Velocity from 'velocity-animate'; // 引入velocity-animate动画库
...
// index.vue模板文件做如下修改
<transition @before-enter="beforeEnter" @enter="enter" @after-enter="afterEnter" @enter-cancelled="enterCancelled" @before-leave="beforeLeave" @leave="leave" @after-leave="afterLeave" @leave-cancelled="leaveCancelled" :css="false">
...
// index.vue添加相关方法
beforeEnter (el) {
console.log('beforeEnter');
el.style.opacity = 0;
el.style.transformOrigin = 'left';
},
enter (el, done) {
console.log('enter');
Velocity(el, { opacity: 1, fontSize: '1.4em' }, { duration: 300 });
Velocity(el, { fontSize: '1em' }, { complete: done });
},
afterEnter (el) {
console.log('afterEnter');
},
enterCancelled (el) {
console.log('enterCancelled');
},
beforeLeave (el) {
console.log('beforeLeave');
},
leave (el, done) {
console.log('leave');
Velocity(el, { translateX: '15px', rotateZ: '50deg' }, { duration: 600 });
Velocity(el, { rotateZ: '100deg' }, { loop: 2 });
Velocity(el, {
rotateZ: '45deg',
translateY: '30px',
translateX: '30px',
opacity: 0
}, { complete: done })
},
afterLeave (el) {
console.log('afterLeave');
},
leaveCancelled (el) {
console.log('leaveCancelled');
}
点击按钮,可以看到控制台依次输出
beforeEnter
enter
afterEnter(动画结束后输出)
...
beforeLeave
leave
afterLeave(动画结束后输出)
在进入或移除动画没有运行完成时,点击按钮,中断动画这时就会输出
enterCancelled/leaveCancelled
官方文档上说,在 enter 和 leave 中,回调函数 done 是必须的 。否则,它们会被同步调用,过渡会立即完成。注意leave (el, done)和enter (el, done)多接收了一个参数,并在{ complete: done }中进行了触发,没特别看出过渡立即完成这个效果,可能是和使用了Velocity动画库有关,不过如果不添加这个done的回调,after-enter的钩子函数是不能正常调用,并且在下一次动画中会先执行enter-cancelled的钩子函数。
:css="false"是忽略对过渡元素添加class(官方文档上说时不对过渡元素进行css检查,没特别看出来,不过不加这个属性,过渡元素会添加v-enter,v-enter-active等class,添加了就不会添加)
使用appear设定元素初始化时的过渡/动画
通过设定appear属性可以让组件开始渲染时一样有过渡效果
// 添加appear属性是表示启用此特性,appear-class,appear-to-class,appear-active-class和v-enter,v-enter-to,v-enter-active存在周期一致
<transition appear appear-class="custom-appear-class" appear-to-class="custom-appear-to-class" appear-active-class="custom-appear-active-class">
<p v-if="showFlag">过渡区域</p>
</transition>
...
// 样式文件中添加如下内容
.custom-appear-class{
font-size: 40px;
color: red;
opacity: 0;
}
.custom-appear-to-class {
font-size: 18px;
}
.custom-appear-active-class{
opacity: 1;
transition: all 5s;
}
...
// 这时要把showFlag改为true,才能看出这个效果,因为这个加载时的过渡效果只会在进入时启用,渲染完成后就不会再使用
showFlag: true,
这时刷新页面就会发现当前元素在第一次渲染时会有一个进入过渡效果,再进行相关操作,这个过渡效果也不会出来
多元素过渡
在某些情况下会碰到在一个区域存在因为不同条件展示不同元素的情况,比如
// 此时这个key值的其实并不是很必要,因为button并不会像input元素那样存在用户输入的缓存信息,但Vue官方建议在这种情况下加上key让元素强制重新渲染,让过渡动画可以正常触发,如果此时不加key值,因为我们改动的只是文案,Vue会尽可能复用代码,并不会重新渲染当前元素,也就不会出现过渡效果
<button @click="changeState">改变文字</button>
<transition name="fade">
<button :key="docState">
{{ buttonMessage }}
</button>
</transition>
...
// data下添加docState
docState: 'saved',
...
// methods下添加changeState
changeState () {
switch (this.docState) {
case 'saved':
this.docState = 'edited';
break;
case 'edited':
this.docState = 'editing';
break;
case 'editing':
this.docState = 'saved';
break;
}
},
此时点击页面我们会看到一个比较丑陋的按钮切换效果,一个按钮正在消失,另一个按钮同时出现,此时我们可以借助Vue提供的过渡模式来使进出过渡切换的更为自然
// mode: out-in(当前元素先进行过渡,完成之后新元素过渡进入); in-out(新元素先进行过渡,完成之后当前元素过渡离开)
<transition name="fade" mode="out-in">
这时页面的过渡效果就比较自然了,一个元素消失,另外一个元素渐隐出现,如果是多个组件的过渡可以使用动态组件来实现
// 在index.vue的模板中做如下调整
<button @click="changeTab">改变tab</button>
<transition name="fade" mode="out-in">
<component :is="showTabView"></component>
</transition>
...
// 添加相关方法
changeTab () {
switch (this.showTabView) {
case 'gameList':
this.showTabView = 'newsList';
break;
case 'newsList':
this.showTabView = 'sportList';
break;
case 'sportList':
this.showTabView = 'gameList';
break;
}
}
这时在页面端就添加了对动态组件的过渡效果,上面我们看了单个元素的显示隐藏触发的过渡效果,和在同一区域不同元素和组件切换时的过渡效果,除此之外,还有列表的过渡效果没有讲,在官方文档,直接告诉我们对列表使用transition-group,但为什么不可以使用transition呢,我们使用一个实例来验证下:
// index.vue的模板改造
<button @click="add">Add</button>
<button @click="remove">Remove</button>
<ul class="ulList">
<transition name="fade">
<li v-for="item in numList">{{item}}</li>
</transition>
</ul>
...
// data添加下面属性
numList: [1, 2, 3, 4, 5, 6, 7, 8, 9],
nextNum: 10,
...
// 添加下面样式
.ulList li {
list-style: none;
}
...
// 添加相关方法
randomIndex: function () {
return Math.floor(Math.random() * this.numList.length)
},
add: function () {
this.numList.splice(this.randomIndex(), 0, this.nextNum++)
},
remove: function () {
this.numList.splice(this.randomIndex(), 1)
}
这时页面会直接提示transition只能用在单个元素,如果要使用在列表元素需要使用transition-group。
// 换成transition-group,但此时子元素li没有添加key值,一样报错提示transition-group的子元素必须包含key值
<transition-group name="fade">
<li v-for="item in numList">{{item}}</li>
...
// 添加key值
<li v-for="item in numList" :key="item">{{item}}</li>
此时页面渲染为
<button>Add</button>
<button>Remove</button>
<ul class="ulList">
<span>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
<li>6</li>
<li>7</li>
<li>8</li>
<li>9</li>
</span>
</ul>
...
// ul下的span是transition-group默认进行添加的,如果你想改动可以使用tag属性
// 没看到官方文档上说如何在包裹外层元素时,同时添加元素的class,这个问题感觉也没那么重要,在外面包裹一个div直接添加class就行了
<button @click="add">Add</button>
<button @click="remove">Remove</button>
<div class="ulList">
<transition-group name="list" tag="ul">
<li v-for="item in numList" :key="item">{{item}}</li>
</transition-group>
</div>
此时页面如果再添加元素,就会有过渡效果,不过此时过渡虽然有了,但是有些僵硬,一个元素插入,其他元素直接就跑到该到的位置,没有流畅感,这里Vue使用了FLIP动画(后面说动画时会详细讲解)来解决这个问题
// 在样式文件中添加如下内容
.list-move {
transition: transform 1s;
}
此时页面的渲染就会在元素插入时,其他元素一起过渡起来,看起来就变的很流畅,除了自动添加的样式(因为针对过渡元素设定的是list过渡,这时的list-move是Vue自动判断的,如果不需要这个自动添加的,可以使用:move-class设定不同的class)
- 如果没有找到 JavaScript 钩子并且也没有检测到 CSS 过渡/动画,DOM 操作 (插入/删除) 在下一帧中立即执行。
与第三方库的结合达到数据元素本身的过渡
上面介绍的都是以元素为基本条件触发的过渡,如果只是元素本身数据的更新,这时要进行过渡就要引入第三方库来实现这些过渡,这些包括且不限于:
- 数字和运算
- 颜色的显示
- SVG 节点的位置
- 元素的大小和其他的属性
这部分的内容我理解和动画挂钩的比较多,所以这些内容会在动画这部分讲解。