# 自定义指令与插件

Vue 自定义指令

认识自定义指令

  • 在 Vue 的模板语法中我们学习过各种各样的指令:v-show、v-for、v-model 等等,除了使用这些指令之外,Vue 也允许我们来自定义自己的指令。
    • 注意:在 Vue 中,代码的复用和抽象主要还是通过组件;
    • 通常在某些情况下,你需要对 DOM 元素进行底层操作,这个时候就会用到自定义指令;
  • 自定义指令分为两种:
    • 自定义局部指令:组件中通过 directives 选项,只能在当前组件中使用;
    • 自定义全局指令:app 的 directive 方法,可以在任意组件中被使用;
  • 比如我们来做一个非常简单的案例:当某个元素挂载完成后可以自定获取焦点
    • 实现方式一:如果我们使用默认的实现方式,定义 ref 绑定到 input 中 , 调用 focus 方法;
    • 实现方式二:自定义一个 v-focus 的局部指令;
    • 实现方式三:自定义一个 v-focus 的全局指令;

默认方式实现聚焦

App.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<div class="app">
<input type="text" ref="inputRef" />
</div>
</template>

<script setup>
import { onMounted, ref } from 'vue'

const inputRef = ref(null)

onMounted(() => {
inputRef.value.focus()
})
</script>

Hooks 方式实现聚焦

./src/hooks/useInput.js

1
2
3
4
5
6
7
8
9
import { ref, onMounted } from 'vue'

export default function useInput() {
const inputRef = ref()
onMounted(() => {
inputRef.value?.focus()
})
return { inputRef }
}

App.vue

1
2
3
4
5
6
7
8
9
10
<template>
<div class="app">
<input type="text" ref="inputRef" />
</div>
</template>

<script setup>
import useInput from '@/hooks/usrInput.js'
const { inputRef } = useInput()
</script>

局部自定义指令实现

  • 实现方式二:自定义一个 v-focus 的局部指令
    • 这个自定义指令实现非常简单,我们只需要在组件选项中使用 directives 即可;
    • 它是一个对象,在对象中编写我们自定义指令的名称(注意:这里不需要加 v-);
    • 自定义指令有一个生命周期,是在组件挂载后调用的 mounted,我们可以在其中完成操作;
  • Vue2 写法实现局部自定义指令 :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<div class="app">
<input type="text" v-focus />
</div>
</template>

<script>
export default {
directives: {
focus: {
//自定义指令的生命周期函数,指令应用的元素被挂载完毕时调用,el:指令应用的 DOM 节点
mounted(el) {
el?.focus()
console.log(el)
},
},
},
}
</script>
  • Vue3 写法实现局部自定义指令 :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
<div class="app">
<input type="text" v-focus />
</div>
</template>

<script setup>
const vFocus = {
//自定义指令的生命周期函数,指令应用的元素被挂载完毕时调用,el:指令应用的 DOM 节点
mounted(el) {
el?.focus()
},
}
</script>

全局自定义指令实现

  • 自定义一个全局的 v-focus 指令可以让我们在任何地方直接使用

main.js

1
2
3
4
5
6
7
8
9
10
11
12
13
import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)

// 注册全局自定义指令
app.directive('focus', {
mounted(el) {
el?.focus()
},
})

app.mount('#app')

./src/App.vue , 中使用自定义指令

1
2
3
4
5
<template>
<div class="app">
<input type="text" v-focus />
</div>
</template>

全局自定义指令抽离

01 新建 ./src/directives/focus.js

1
2
3
4
5
6
7
export default function directiveFocus(app) {
app.directive('focus', {
mounted(el) {
el?.focus()
},
})
}

02 新建 ./src/directives/index.js

1
2
3
4
5
import directiveFocus from './focus.js'

export default function useDirectives(app) {
directiveFocus(app)
}

03 在 main.js 中导入并注册全局自定义指令

1
2
3
4
5
6
7
8
9
import { createApp } from 'vue'
import App from './App.vue'
import useDirectives from './directives'

const app = createApp(App)

useDirectives(app)

app.mount('#app')

指令的生命周期

  • 一个指令定义的对象,Vue 提供了如下的几个钩子函数:
  • created:在绑定元素的 attribute 或事件监听器被应用之前调用;
  • beforeMount:当指令第一次绑定到元素并且在挂载父组件之前调用;
  • mounted:在绑定元素的父组件被挂载后调用;
  • beforeUpdate:在更新包含组件的 VNode 之前调用;
  • updated:在包含组件的 VNode 及其子组件的 VNode 更新后调用;
  • beforeUnmount:在卸载绑定元素的父组件之前调用;
  • unmounted:当指令与元素解除绑定且父组件已卸载时,只调用一次;

指令修饰符

  • 如果我们指令需要接受一些参数或者修饰符应该如何操作呢?
    • info 是参数的名称;
    • aaa-bbb 是修饰符的名称;
    • 后面是传入的具体的值;

1662194317956

  • 在我们的生命周期中,我们可以通过 bindings 获取到对应的内容:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
<div class="app">
<h2 v-why:kobe.abc.cba="message">哈哈哈</h2>
</div>
</template>

<script setup>
const message = '你好李银河'

const vWhy = {
mounted(el, bindings) {
console.log(bindings.value) // 给自定义指令传入的值
el.textContent = bindings.value
},
}
</script>

案例_价格格式化指令

01 新建 ./src/directive/unit.js

1
2
3
4
5
6
7
8
9
10
11
12
export default function directiveUnit(app) {
app.directive('unit', {
mounted(el, bingings) {
const defaultText = el.textContent
let unit = bingings.value
if (!unit) {
unit = '¥' //默认值
}
el.textContent = unit + defaultText
},
})
}

02 新建 ./src/directives/index.js

1
2
3
4
5
import directiveUnit from './unit.js'

export default function useDirectives(app) {
directiveUnit(app)
}

03 在 main.js 中导入并注册全局自定义指令

1
2
3
4
5
6
7
8
9
import { createApp } from 'vue'
import App from './App.vue'
import useDirectives from './directives'

const app = createApp(App)

useDirectives(app)

app.mount('#app')

04 自定义指令的使用

1
2
3
4
5
<template>
<div class="app">
<h2 v-unit>{{123}}</h2>
</div>
</template>

案例_时间格式化指令

01 安装插件 : npm install dayjs

02 新建 ./src/directive/ftime.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import dayjs from 'dayjs'

export default function directiveFtime(app) {
app.directive('ftime', {
mounted(el, bindings) {
// 1.获取时间, 并且转化成毫秒
let timestamp = el.textContent
if (timestamp.length === 10) {
timestamp = timestamp * 1000
}

timestamp = Number(timestamp)

// 2.获取传入的参数
let value = bindings.value
if (!value) {
value = 'YYYY-MM-DD HH:mm:ss'
}

// 3.对时间进行格式化
const formatTime = dayjs(timestamp).format(value)
el.textContent = formatTime
},
})
}

02 新建 ./src/directives/index.js

1
2
3
4
5
import directiveFtime from './ftime.js'

export default function useDirectives(app) {
directiveFtime(app)
}

03 在 main.js 中导入并注册全局自定义指令

1
2
3
4
5
6
7
8
9
import { createApp } from 'vue'
import App from './App.vue'
import useDirectives from './directives'

const app = createApp(App)

useDirectives(app)

app.mount('#app')

04 自定义指令的使用

1
2
3
4
5
6
<template>
<div class="app">
<h2 v-ftime>{{1623549563257}}</h2>
<h2 v-ftime="'YYYY/MM/DD hh:mm:ss'">{{1623549563257}}</h2>
</div>
</template>

案例_图片懒加载指令

01 自定义指令

./src/directives/lazy.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 实现图片懒加载的 自定义指令
export default {
mounted(el) {
console.log('自定义指令绑定完毕', el) // el 为该自定义指令应用的真实 DOM 节点
// 保存图片的地址
const imgSrc = el.src
el.src = ''

// 观察者 IntersectionObserver,它可以用来监听元素是否进入了设备的可视区域之内,是一个类,以 new 的形式声明一个对象
const observer = new IntersectionObserver((entries) => {
// 当元素出现在和离开可视区域时,都会触发回调函数
const { isIntersecting } = entries[0] //解构出 isIntersecting 值 , 为一个布尔值
if (isIntersecting) {
// 加载图片
el.src = imgSrc
// 停止观察
observer.unobserve(el)
}
})

// 开启观察
observer.observe(el)
},
}

// IntersectionObserver ,它可以用来监听元素是否进入了设备的可视区域之内
// 应用场景:
// 当页面滚动时,懒加载图片或其他内容。
// 实现“可无限滚动”网站,也就是当用户滚动网页时直接加载更多内容,无需翻页。
// 对某些元素进行埋点曝光
// 滚动到相应区域来执行相应动画或其他任务

02 在 入口文件 main.js 中注册自定义指令

1
2
3
4
5
6
7
import { createApp } from 'vue'
import App from './App.vue'
import lazy from './directives/lazy'

const app = createApp(App)
app.directive('lazy', lazy)
app.mount('#app')

03 使用自定义指令

  • 在任意组件中,给需要懒加载的图片添加 v-lazy 指令即可
1
2
3
<img v-lazy src="../assets/images/01.webp" alt="" />
<img v-lazy src="../assets/images/02.webp" alt="" />
<img v-lazy src="../assets/images/03.webp" alt="" />

Teleport 内置组件

认识 Teleport

  • 在组件化开发中,我们封装一个组件 A,在另外一个组件 B 中使用:
    • 那么组件 A 中 template 的元素,会被挂载到组件 B 中 template 的某个位置;
    • 最终我们的应用程序会形成一棵 DOM 树结构;
  • 但是某些情况下,我们希望组件不是挂载在这个组件树上的,可能是移动到 Vue app 之外的其他位置:
    • 比如移动到 body 元素上,或者我们有其他的 div#app 之外的元素上;
    • 这个时候我们就可以通过 teleport 来完成;
  • Teleport 是什么呢?
    • 它是一个 Vue 提供的内置组件,类似于 react 的 Portals ;
    • teleport 翻译过来是心灵传输、远距离运输的意思;
      • 它有两个属性:
        • to:指定将其中的内容移动到的目标元素,可以使用选择器;
        • disabled:是否禁用 teleport 的功能;

和组件结合使用

1
2
3
4
5
6
7
8
<template>
<div class="app">
<!--会将 helloworld 组件挂载到 body 下,默认该组件是挂载到 div.app 下-->
<teleport to="body">
<HelloWorld></HelloWorld>
</teleport>
</div>
</template>
  • 如果我们将多个 teleport 应用到同一个目标上( to 的值相同),那么这些目标会进行合并:

1662202018730

Suspense 内置组件

  • 注意:目前(2022-08-01)Suspense 显示的是一个实验性的特性,API 随时可能会修改。
  • Suspense 是一个内置的全局组件,该组件有两个插槽:
    • default:如果 default 可以显示,那么显示 default 的内容;
    • fallback:如果 default 无法显示,那么会显示 fallback 插槽的内容;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<div class="app">
<suspense>
<template #default>
<HelloWorld></HelloWorld>
</template>
<template #fallback>
<h2>Loading</h2>
</template>
</suspense>
</div>
</template>

<script setup>
import { defineAsyncComponent } from 'vue'
const HelloWorld = defineAsyncComponent(() => import('./components/HelloWorld.vue'))
</script>

插件

认识插件

  • 通常我们向 Vue 全局添加一些功能时,会采用插件的模式,它有两种编写方式:
    • 对象类型:一个对象,但是必须包含一个 install 的函数,该函数会在安装插件时执行;
    • 函数类型:一个 function,这个函数会在安装插件时自动执行;
  • 插件可以完成的功能没有限制,比如下面的几种都是可以的:
    • 添加全局方法或者 property,通过把它们添加到 config.globalProperties 上实现;
    • 添加全局资源:指令 / 过滤器 / 过渡等;
    • 通过全局 mixin 来添加一些组件选项;
    • 一个库,提供自己的 API,同时提供上面提到的一个或多个功能;

插件的本质

对象写法

main.js

1
2
3
4
5
6
// 安装插件,对象写法 , 本质就是调用内部的 install 方法
app.use({
install: function (app) {
console.log('传入对象的 install 被执行', app)
},
})

函数写法

main.js

1
2
3
4
// 安装插件,函数写法 , 本质就是调用传入的函数
app.use(function (app) {
console.log('传入的函数被执行', app)
})