【vue3】封装函数配合directive方法实现自定义V-指令
封装一个函数
上一篇我们讲过《Vue3开发一个v-loading的自定义指令》
通过项目的进一步开发,我们发现,我们需要多个指令,而且组件逻辑都与v-loading相似,那么相似的部分,我们就可以封装一下。
目录结构
src----assets
|-----js
|----create-loading-like-directive.js 创建类似v-loading的自定义指令
|----dom.js 专门存放dom操作的函数
|-----components
|----base
|--- no-result
|---no-result.vue 请求无数据时展示的组件
|---directive.js 将no-result组件转化为指令
create-loading-like-directive.js
作用,创造一个类似于v-loading一样的指令。
注意使用的时候需要在main.js中进行注册(见《二》)
// 自定义v-组件指令
import { createApp } from 'vue'
import { addClass, removeClass } from '@/assets/js/dom'
// 该样式添加在了assets/scss/base.scss中,.g-relative{position: relative}
const relativeCls = 'g-relative'
/*
* 参数如果传一个loading组件那么就创建v-loading指令,如传no-result那么就创建v-no-result指令
* Comp 就是import进入的组件
* */
export default function createLoadingLikeDirective(Comp) {
const directive = {
/*
主要写一些钩子函数 在钩子中去实现逻辑
指令主要是将loading组件生成的DOM动态插入到指令作用的DOM对象上(v-loading=true),
如果v-loading=false那么就删除动态插入的指令挂载时的钩子函数
*/
mounted(el, binding) {
/*
el指向指令所在的dom 如 <div v-loading="true" id="box">
那么el就是#box binding.value就是代表的true
判断v-loading值为true动态插入到指令作用的节点下
如果创建组件对应的dom?先用这个loading组件新建一个vue实例(app对象),
然后再动态取挂载,就会产生一个实例,在实例中拿到它的DOM对象
*/
const app = createApp(Comp)
/*
拿到它的实例,挂载到动态创建的DOM上,vue开发是支持多实例的,可以创建多个实例
因为创建的元素没挂载到BODY上,实际也没有完成dom层的挂载,
目的是创建出来的实例的DOM对象要挂载到el上(指令所在的DOM)
*/
const instance = app.mount(document.createElement('div'))
/*
* 拿到组件的名称,当多个指令应用于一个元素上时,后面的会覆盖前面的el.instance,导致删除失败
* 通过对象多一个维度,解决后面创建的值覆盖前面值的问题 小技巧谨记
* */
const name = Comp.name
// 检测如果el中没有这个字段 那么创建一个对象
if (!el[name]) {
el[name] = {}
}
/*
因为instance在mounted中只创建一次,但是之后会经常用到,要保留起来,
如果要在其他的钩子函数也要访问它的话就存在el对象上
这样操作在其他钩子中也可以获取到这个实例
不同的组件,使用不同的el[name],呢么其各自的instance就不同了,比如el['loading'].ins, el['no-result'].ins
*/
el[name].instance = instance
// 通过binding.arg拿到动态参数,如果组件中有多个参数可以考虑传进来的是一个数组
const title = binding.arg
// 如果参数不是空 执行实例中的方法
if (typeof title !== 'undefined') {
// 组件内部的方法,如果多个组件,需要统一一致的方法
instance.setTitle(title)
}
// binding.value就是代表指令传递的值
if (binding.value) {
append(el)
}
},
// 当组件更新的时候执行,因为指令不是一成不变的比如由v-loading=true变为v-loading=false 就会执行
updated(el, binding) {
// 通过binding.arg拿到动态参数
const title = binding.arg
const name = Comp.name
// 如果参数不是空 执行实例中的方法
if (typeof title !== 'undefined') {
el[name].instance.setTitle(title)
}
// 如果loading前后值不一致
if (binding.value !== binding.oldValue) {
// 如果是true那么就插入否则删除
binding.value ? append(el) : remove(el)
}
}
}
return directive
// 元素挂载的操作
function append(el) {
const name = Comp.name
// 根据loading组件样式,是使用absolute,而当el不是fixed或retaive时候给其动态添加定位属性
const style = getComputedStyle(el)
// 判断el的样式中有无定位,===-1就是没有 希望v-loading不受样式限制
if (['absolute', 'fixed', 'relative'].indexOf(style.position) === -1) {
addClass(el, relativeCls)
}
// 因为loading组件生成的实例instance已经赋值给el.instance属性上了,所以在这里可以直接通过el拿到
// el.instance.$el就是loading组件的DOM对象
el.appendChild(el[name].instance.$el)
}
function remove (el) {
const name = Comp.name
removeClass(el, relativeCls)
el.removeChild(el[name].instance.$el)
}
}
这里应用到了一个小技巧,请见介绍如何使用的部分。
DOM.js
// 存放比较通用的dom操作
// el是一个DOM对象
export function addClass (el, className) {
// 如果当前元素样式列表中没有className
if (!el.classList.contains(className)) {
el.classList.add(className)
}
}
export function removeClass (el, className) {
el.classList.remove(className)
}
如何使用
将组件转化为v-指令
在组件的directive.js中:
import NoResult from './no-result'
import createLoadingLikeDirective from '@/assets/js/create-loading-like-directive'
const noResultDirective = createLoadingLikeDirective(NoResult)
export default noResultDirective
在main.js中进行注册
import { createApp } from 'vue'
import App from './App.vue'
import lazyPlugin from 'vue3-lazy'
// 注册指令 引入注册的文件
import loadingDirective from '@/components/base/loading/directive'
import noResultDirective from '@/components/base/no-result/directive'
/* 注册的时候使用directive(‘指令名称’, 指令对象)
因为叫v-loading所以这里传入Loading,
* directive('loading', loadingDirective)
全局注册后这个app下就可以全局使用v-loading了
* */
createApp(App).use(store).use(router).use(lazyPlugin, {
// 配置默认加载的图片 使用webpack的require语法,它会自适应选择图片加载的方式外链或者base64
loading: require('@/assets/images/default.png')
}).directive('loading', loadingDirective).directive('no-result', noResultDirective).mount('#app')
具体使用
比如在div上:
<div
class="list"
v-loading="loading"
v-no-result:[noResultText]="noResult"
>...</div>
v-no-result上的 :[noResultText] 是指令参数 固定写法。如果是多个参数,noResultText就传入一个数组。
props: {
title: String,
// v-loading不是每次都通过数据,也可以通过赋值去改变状态
loading: Boolean,
noResultText: {
type: String,
default: '抱歉,没找到可以播放的歌曲'
}
},
computed: {
// 显示是否有内容,当无内容的时候显示
noResult() {
// 当loading加载结束并且歌单列表为空 就返回true
return !this.loading && !this.songs.length
},
}
针对于组件的参数,推荐在props中进行定义,方便可以从组件外部修改状态,同时赋于默认值。
小技巧
通过上面例子,我们可以看到,可能会在一个元素上同时使用两个指令的情况,那么这时就需要对封装的函数特别注意了。
const name = Comp.name
if (!el[name]) {
el[name] = {}
}
el[name].instance = instance
在封装的时候,我们获取了组件的名称,并且使用组件名称作为el对象的一个字段。这时因为,如果按照以前的写法:
el[name].instance = instance
当两个指令同时用的时候,后面的一个会覆盖掉之前的,导致在v-loading指令执行update的时候,实际删除的是no-result组件的元素,而那时它其实还没有创建,导致报错。因此在遇到这种清况时,通过动态添加字段,扩展维度,让不同的组件分别保存在对象的不同字段中。避免冲突。
希望在以后的工作中,大家可以充分利用这个小技巧。
还有要注意的
还有要注意的一点就是,使用同样的封装函数,对于组件,必须要有类似的要求。比如:
el[name].instance.setTitle(title)
setTitle方法,必须同类型的组件都要实现这个方法。
no-result组件
<template>
<div class="no-result">
<div class="no-result-content">
<div class="icon"></div>
<p class="text">{{title}}</p>
</div>
</div>
</template>
<script type="text/ecmascript-6">
export default {
name: 'no-result',
data() {
return {
title: '抱歉,没有结果'
}
},
methods: {
setTitle(title) {
this.title = title
}
}
}
</script>
<style lang="scss" scoped>
.no-result {
position: absolute;
top: 50%;
left: 50%;
transform: translate3d(-50%, -50%, 0);
.no-result-content {
text-align: center;
.icon {
width: 86px;
height: 90px;
margin: 0 auto;
@include bg-image('no-result');
background-size: 86px 90px;
}
.text {
margin-top: 30px;
font-size: $font-size-medium;
color: $color-text-d;
}
}
}
</style>