写一个自定义指令并不难,vue中提供了directive这个方法让我们可以很方便的注册以及使用自定义指令,那么我们接下来一步步来看下如何实现以及使用一个全局的自定义指令
以一个加载loading的动画指令为例
<template>
<div v-loading="loading"></div>
<template>
<script>
import { ref } from 'vue'
export default {
props: {
handleLeave : {
type: Function,
default: () => null
}
},
setup() {
let loading = ref(false)
return { loading }
}
}
</script>
复制代码
当loading为true时,展示loading动画,false则移除
接下来需要实现一个可以渲染动画的组件,我们命名为loading.vue
<template>
<transition name="fade" @after-leave="handleAfterLeave">
<div
v-show="visible"
class="loading-mask">
<div class="loading-spinner">
<svg class="circular" viewBox="25 25 50 50">
<circle class="path" cx="50" cy="50" r="20" fill="none"/>
</svg>
</div>
</div>
</transition>
</template>
<script>
import { ref } from 'vue'
export default {
name:'Loading',
setup(props, { emit }) {
let visible = ref(false)
return {
visible,
show: () => visible.value = true,
close: () => visible.value = false,
handleAfterLeave: () => {
props.handleLeave()
}
}
},
}
</script>
<style lang="scss">
.loading-mask {
position: absolute;
z-index: 2000;
background-color: hsla(0,0%,100%,.9);
margin: 0;
top: 0;
right: 0;
bottom: 0;
left: 0;
transition: opacity .3s;
}
.loading-spinner {
top: 50%;
margin-top: -21px;
width: 100%;
text-align: center;
position: absolute;
.circular {
height: 42px;
width: 42px;
animation: loading-rotate 2s linear infinite;
}
.path {
animation: loading-dash 1.5s ease-in-out infinite;
stroke-dasharray: 90,150;
stroke-dashoffset: 0;
stroke-width: 2;
stroke: #409eff;
stroke-linecap: round;
}
}
@keyframes loading-rotate{to{transform:rotate(1turn)}}
@keyframes loading-dash {
0% {
stroke-dasharray: 1,200;
stroke-dashoffset: 0;
}
50% {
stroke-dasharray: 90,150;
stroke-dashoffset: -40px;
}
100% {
stroke-dasharray: 90,150;
stroke-dashoffset: -120px;
}
}
</style>
复制代码
这是一个非常简单的loading组件,除了遮罩层以及svg动画,接收一个关闭的回调,并暴露了一些组件方法,后面我们再说如何调用
因为这个loading组件不是单纯的逻辑处理,也是要调用到vue的渲染能力,所以其实我们还要实现一个全局的组件渲染功能,我之前有写一篇关于vue3全局api组件的相关的文章,这里就直接展示代码
import { createVNode, render } from 'vue'
import Loading from './loading.vue'
const loadingDirective = {}
const container = document.createElement('div')
loadingDirective.install = app => {
const createLoading = () => {
const vLoading = createVNode(Loading)
render(vLoading, container)
document.body.appendChild(container)
}
}
export default loadingDirective
复制代码
在loading.js中我们先声明一个createLoading方法来实现渲染功能,暴露install方法是因为后面希望通过use方法来注册到全局单例上,这里已经实现了把组件渲染出来,接下来就是注册指令的重点,我们先展示代码
app.directive('loading', {
mounted: function(el, binding) {
createLoading(el, binding)
const instance = el.instance
!!binding.value && instance.show()
},
updated: function(el, binding) {
const instance = el.instance
if (binding.oldValue !== binding.value) {
!!binding.value ? instance.show() : instance.close()
}
}
})
复制代码
在vue2中指令方法是暴露在Vue全局对象中,3中则是应用实例对象上,所以我们通过install方法传入的app实例便可完成指令注册,这样也是避免了创建多个单例时互相污染的情况
3中的指令钩子函数共有7个,并且与组件的生命周期一一对应,这也更有利于去理解各个阶段的含义,这里不展开说明,有兴趣的可以自行查阅官方文档,这里我们主要使用到两个钩子,mounted、updated
在mounted我们实现的逻辑是初始化loading组件,获取到组件实例并根据传入的绑定值确认是否触发show方法;
updated中是反复执行的逻辑,指令所绑定的组件vnode和其子组件vnode更新都会触发此方法,这里就是确认每次方法被触发时绑定值的真假来触发对应组件实例方法
因为在钩子函数中可以传入el(指令绑定到的元素对象),所以可以简化我们的渲染逻辑,不必额外创建container以及调用appendChild方法,当然也可以根据实际需求进行扩展,然后我们再借助在el上添加了instance来指向loading组件实例以此来调用相关方法,也可以避免我们再额外声明变量产生闭包,那么至此,我们的loading.js可以改写为
import { createVNode, render } from 'vue'
import Loading from './index.vue'
const loadingDirective = {}
loadingDirective.install = app => {
const createLoading = (el, binding) => {
const vm = binding.instance
const leaveExr = el.getAttribute('loading-leave')
const handleLeave = Object.prototype.toString.call(vm[leaveExr]).silce(8,-1) === 'Function' ? vm[leaveExr] : () => null
const vLoading = createVNode(Loading, { handleLeave })
render(vLoading, el)
el.instance = vLoading.component.ctx
}
app.directive('loading', {
mounted: function(el, binding) {
createLoading(el, binding)
const instance = el.instance
!!binding.value && instance.show()
},
updated: function(el, binding) {
const instance = el.instance
if (binding.oldValue !== binding.value) {
!!binding.value ? instance.show() : instance.close()
}
}
})
}
export default loadingDirective
复制代码
这里主要注意两个地方,第一就是如何向指令传参以及在el对象上添加instace属性指向loading组件实例对象
createLoading接收的两个参数分别来自mounted的el和binding,el就是指令所绑定到的元素,binding.instance指向使用指令的组件实例,我们向指令传参是通过自定义属性实现,再通过访问组件属性来查找对应方法,通过createVnode方法传入loading组件,此方法返回的vnode对象暴露了组件实例上下文ctx,我们便可以通过这个属性来实现对loading组件的访问
那么最后就是调用了,在入口文件完成实例化后便可在应用实例上调用use方法完成指令注册
import { createApp } from 'vue'
import App from './App.vue'
import vLoading from './loading.js'
const app = createApp()
app.use(vLoading)
app.mount('#app')
复制代码
最后我们可以在最初的使用例子上再传入一个方法
<template>
<div v-loading="loading" loading-leave="handleLeave"></div>
<template>
<script>
import { ref } from 'vue'
export default {
setup() {
let loading = ref(false)
const handleLeave = () => console.log('指令关闭了')
return { loading, handleLeave }
}
}
</script>
复制代码
一个带渲染功能的自定义指令就基本完成,核心就是directive方法,然后根据各个钩子提供的参数来实现我们想要的功能
vue3中向组件传参不再像2中那么自由,所有的参数都被处理到了props对象上,可以在setup方法的第一个参数来获取,作为组合式api的入口起点