Skip to content

自定义插件

插件 (Plugins) 是一种能为 Vue 添加全局功能的工具代码。插件需要通过 app.use() 函数进行安装:

js
import { createApp } from 'vue'

const app = createApp({})

// 调用 app.use() 安装插件,其中:
// 第1个参数是被安装的“插件的对象”
// 第2个参数是传给插件的“配置选项”
app.use(myPlugin, {
  /* 可选的选项 */
})

// 插件必须在 mount 之前进行安装
app.mount('#app')

定义插件

插件是一个包含 install 函数的对象,install 函数接收两个参数:第 1 个参数是 app 实例,第 2 个参数是传给插件的配置选项

js
const myPlugin = {
  install(app, options) {
    // 插件的具体实现
  }
}

插件的主要应用场景如下:

  1. 通过 app.component()app.directive() 注册一到多个全局组件或自定义指令;
  2. 通过 app.provide() 使一个资源可被注入进整个应用;
  3. app.config.globalProperties 中添加一些全局实例属性或方法;
  4. 一个可能上述三种都包含了的功能库 (例如 vue-router)。

通过插件注册全局组件

封装名为 EscookButton 的组件:

vue
<script setup>
defineProps({
  type: {
    default: 'primary',
    // 属性值仅接收以下4个字符串
    validator(value) {
      return ['primary', 'danger', 'warning', 'default'].includes(value)
    }
  }
})
</script>

<template>
  <!-- 点击事件可通过 Attribute 透传直接绑定到 button 上 -->
  <button class="escook-button" :class="type"><slot /></button>
</template>

<style scoped>
.escook-button {
  --primary: #006aff;
  --danger: rgb(255, 79, 79);
  --warning: rgb(255, 185, 55);
  --default: rgb(241, 241, 241);
  border: 2px solid #000;
  height: 36px;
  padding: 0 10px;
  border-radius: 5px;
  font-size: 14px;
  cursor: pointer;
}
/* hover 时不透明度变为 0.9 */
.escook-button:hover {
  opacity: 0.9;
}
/* 点击时不透明度变为 0.8 */
.escook-button:active {
  opacity: 0.8;
}

/* 被禁用时不透明度变为 0.6 */
.escook-button[disabled] {
  opacity: 0.6;
}

.primary {
  background-color: var(--primary);
  color: white;
  border-color: hsl(from var(--primary) h s calc(l - 10));
}

.danger {
  background-color: var(--danger);
  color: white;
  border-color: hsl(from var(--danger) h s calc(l - 10));
}

.warning {
  background-color: var(--warning);
  color: white;
  border-color: hsl(from var(--warning) h s calc(l - 10));
}

.default {
  background-color: var(--default);
  color: #000;
  border-color: hsl(from var(--default) h s calc(l - 10));
}
</style>

通过插件,全局注册此 EscookButton 组件:

js
// src/utils/escookPlugin.js
import EscookButton from '@/components4/EscookButton.vue'

export default {
  install(app, options) {
    app.component('EscookButton', EscookButton)
  }
}

main.js 入口模块中,导入并安装此插件:

js
import { createApp } from 'vue'
import App from './App.vue'
// 1. 导入自定义的 Vue3 插件
import escookPlugin from './utils/escookPlugin'

const app = createApp(App)
// 2. 在 app 创建之后且 app.mount() 之前,安装自定义的 Vue 组件
app.use(escookPlugin, { upperCase: true })
app.mount('#app')

🌹 温馨提示 🌹

通过插件注册的组件,全局可用。

通过插件挂载全局指令

我们可以把之前封装的 v-lazy 指令,通过插件的形式挂载为全局指令。首先,把自定义指令相关的样式抽离为单独的 css 文件:

css
/* src/directives/v-lazy.css */
.escook-lazy-loading {
  background: #fbfbfb url('@/assets/loading.gif?inline') no-repeat center;
  background-size: 140px;
}

.escook-lazy-error {
  background: #fbfbfb url('@/assets/bad.jpg?inline') no-repeat center;
  background-size: 30px;
}

然后,把自定义指令封装为独立的 src/directives/v-lazy.js 模块:

js
import defaultImg from '@/assets/1x1.png?inline'
import './v-lazy.css'

const observer = new IntersectionObserver(
  (entries) => {
    // entries 是一个数组,存放着元素和视口相交的状态信息。
    // 当判断到元素和视口相交时,有几个元素满足相交条件,
    // entries 中就有几个相交的状态信息
    entries.forEach((entry) => {
      // 判断元素是否和视口相交
      if (entry.isIntersecting) {
        entry.target.src = entry.target.dataset.src
        // 相交1次之后,不再监听当前元素是否和视口相交
        observer.unobserve(entry.target)

        entry.target.addEventListener('load', function (e) {
          e.target.classList.remove('escook-lazy-loading')
          e.target.removeAttribute('data-src')
        })

        entry.target.addEventListener('error', function (e) {
          e.target.src = defaultImg
          e.target.classList.remove('escook-lazy-loading')
          e.target.classList.add('escook-lazy-error')
        })
      }
    })
  },
  {
    // 交叉的阈值:
    // 1 表示元素完整显示到视口中,才算相交
    // 0.5 表示至少元素的一半在视口中可见,才算相交
    // 0 表示元素只要有1个像素在视口中可见,就算相交
    threshold: 0
  }
)

export default {
  // 绑定了 v-lazy 的 img 图片,
  // 在将要被插入 DOM 前,为其添加 loading 类名并设置默认图片
  beforeMount(el, binding) {
    el.classList.add('escook-lazy-loading')
    el.src = defaultImg
    el.dataset.src = binding.value
    observer.observe(el)
  }
}

最后,改造 src/utils/escookPlugin.js 插件模块,全局注册自定义的 v-lazy 指令:

js
import EscookButton from '@/components4/EscookButton.vue'
// 导入自定义的 vLazy 指令
import vLazy from '@/directives/v-lazy.js'

export default {
  install(app, options) {
    app.component('EscookButton', EscookButton)
    // 全局注册自定义的 v-lazy 指令
    app.directive('lazy', vLazy)
  }
}

通过插件注入全局数据

使用插件形参中的 app 参数,调用其上的 app.provide() 方法,可以全局提供数据给所有组件使用。

通过 Provide 向全局提供响应式数据

改造 src/utils/escookPlugin.js 插件,在头部区域导入 ref 响应式 API:

js
import { ref } from 'vue'

改造插件的 install 函数,在函数内部调用 app.provide() 方法,全局提供数据给所有组件使用:

js
export default {
  install(app, options) {
    app.component('EscookButton', EscookButton)
    app.directive('lazy', vLazy)
    // 全局提供数据,给所有组件使用,
    // 第一个参数是数据名,由程序员自定义
    // 第二个参数是要提供的数据,可以使用 ref 或 reactive API 提供响应式的数据
    app.provide('$global', ref({ name: 'zs', age: 20 }))
  }
}

在组件中,使用 vue 提供的 inject() 函数,注入全局数据到当前的组件中使用:

vue
<script setup>
import { inject } from 'vue'
const globalData = inject('$global')
</script>

<template>
  <p>{{ globalData }}</p>
  <!-- 点击此按钮,可以修改 Provide 提供的响应式数据, -->
  <!-- 所有用到此响应式数据的组件,都会被更新渲染 -->
  <button @click="globalData.age++">age++</button>
</template>

通过 options 提供需要注入的数据

在刚才的案例中,Provide 提供的数据是写死到 install 函数内部的。我们可以让用户通过 install 函数的第二个参数,传入要全局 Provide 的数据:

js
// main.js
app.use(escookPlugin, {
  // 需要被全局提供的数据
  globalData: { name: 'zs', age: 30 }
})

在插件的 install 函数中,我们可以使用第二个参数 options 来接收传入的数据,并把其包装为响应式数据,通过 Provide 提供到全局:

js
export default {
  install(app, options) {
    app.component('EscookButton', EscookButton)
    app.directive('lazy', vLazy)
    // 全局提供数据,给所有组件使用,
    // 第一个参数是数据名,由程序员自定义
    // 第二个参数是要提供的数据,可以使用 ref 或 reactive API 提供响应式的数据
    // app.provide('$global', ref({ name: 'zs', age: 20 }))
    app.provide('$global', ref(options.globalData))
  }
}

通过插件挂载全局属性和方法

在插件的 install 函数中,除了可以使用 Provide/Inject 向全局提供数据之外,还可以通过 app.config.globalProperties 向全局挂载属性和方法。

挂载全局属性

把全局属性挂载到 app.config.globalProperties 上即可:

js
import EscookButton from '@/components4/EscookButton.vue'
import { vLazy } from '../utils/lazy.js'
import { ref } from 'vue'

export default {
  install(app, options) {
    // 往全局挂载属性
    app.config.globalProperties.$msg = 'Hello liulongbin.'

    app.component('EscookButton', EscookButton)
    app.directive('lazy', vLazy)
    app.provide('$global', ref(options.globalData))
  }
}

在组件的 <template> 模板中,可以直接访问到全局挂载的属性值:

html
<p>{{ $msg }}</p>

<script setup> 中,需要先通过 getCurrentInstance() 函数获取到当前 SFC 组件的 instance 实例,再通过 instance.appContext.config.globalProperties 访问全局挂载的属性:

vue
<script setup>
import { getCurrentInstance } from 'vue'

// 1. 通过 getCurrentInstance() 获取当前组件实例
const instance = getCurrentInstance()
// 2. 通过 instance.appContext.config.globalProperties 访问全局挂载的属性
const msg = instance.appContext.config.globalProperties.$msg

console.log(msg)
</script>

挂载全局方法

挂载和使用全局方法的过程,与挂载和使用全局属性的过程完全一样。

例如,我们要全局挂载一个 $toast 方法,用来展示 toast 提示消息。首先,在 src/utils/ 下创建 toast.js 模块,向外默认导出一个 toast 函数:

js
export default function (msg, duration = 3000) {
  const toast = document.createElement('div')
  toast.style.position = 'fixed'
  toast.style.padding = '5px 15px'
  toast.style.fontSize = '12px'
  toast.style.color = '#fff'
  toast.style.backgroundColor = '#000'
  toast.style.boxShadow = '0px 0px 2px #2b2b2b'
  toast.style.borderRadius = '3px'
  toast.style.left = '50%'
  toast.style.top = '50%'
  toast.style.transform = 'translate(-50%, -50%)'
  toast.style.zIndex = '999'
  toast.style.cursor = 'default'
  toast.textContent = msg

  document.body.appendChild(toast)

  setTimeout(() => {
    document.body.removeChild(toast)
  }, duration)
}

然后,在 escookPlugin.js 中导入刚才封装的 toast 函数,并把它挂载到全局:

js
// 1. 导入 toast 函数
import toast from './toast'
import EscookButton from '@/components4/EscookButton.vue'
import { vLazy } from '../utils/lazy.js'
import { ref } from 'vue'

export default {
  install(app, options) {
    app.config.globalProperties.$msg = 'Hello liulongbin.'
    // 2. 往全局挂载 $toast 方法
    app.config.globalProperties.$toast = toast

    app.component('EscookButton', EscookButton)
    app.directive('lazy', vLazy)
    app.provide('$global', ref(options.globalData))
  }
}

在组件的模板中,可以直接调用 $toast() 函数展示提示消息:

html
<button @click="$toast('Hello liulongbin!')">按钮</button> <button @click="showToast">按钮</button>

而在 <script setup> 中,需要先获取组件实例,再通过 instance.appContext.config.globalProperties 访问全局挂载的方法:

vue
<script setup>
import { getCurrentInstance } from 'vue'

// 1. 获取当前组件的实例
const instance = getCurrentInstance()
const showToast = () => {
  // 2. 通过 instance.appContext.config.globalProperties 访问全局挂载的方法
  instance.appContext.config.globalProperties.$toast('你好,刘龙宾.')
}
</script>

天不生夫子,万古长如夜