Skip to content

Vue3 自定义全局组件和全局指令和全局 Loading

本篇文章项目地址:vue-custom-directives-components

自定义全局组件和全局指令首先要先搭建项目,本次搭建项目是用过 vite 搭建的,具体搭建过程可以参考 vite 官网

bash
pnpm create vite my-vue-app --template vue

这次搭建项目没有使用 ts,如果需要的话可以在创建项目的时候选择 ts 模板。

bash
pnpm create vite my-vue-app --template vue

项目目录结构如下

项目目录结构
📦vue-directs-components
 ┣ 📂.vscode
 ┃ ┣ 📜extensions.json
 ┃ ┗ 📜settings.json
 ┣ 📂public
 ┃ ┗ 📜vite.svg
 ┣ 📂src
 ┃ ┣ 📂assets
 ┃ ┃ ┗ 📜vue.svg
 ┃ ┣ 📂components
 ┃ ┃ ┣ 📂Loading
 ┃ ┃ ┃ ┣ 📜index.js
 ┃ ┃ ┃ ┗ 📜index.vue
 ┃ ┃ ┗ 📜index.js
 ┃ ┣ 📂directives
 ┃ ┃ ┣ 📂color
 ┃ ┃ ┃ ┗ 📜index.js
 ┃ ┃ ┣ 📂font
 ┃ ┃ ┃ ┗ 📜index.js
 ┃ ┃ ┗ 📜index.js
 ┃ ┣ 📜App.vue
 ┃ ┣ 📜main.js
 ┃ ┗ 📜style.css
 ┣ 📜.gitignore
 ┣ 📜README.md
 ┣ 📜eslint.config.js
 ┣ 📜index.html
 ┣ 📜package.json
 ┣ 📜pnpm-lock.yaml
 ┗ 📜vite.config.js

开始之前说明

要做全局的自定义指令和组件有一个问题就是:Vue 中有没有一种方法能够为 Vue 添加全局的自定义指令和组件呢?或者说有没有一种方式能够为 Vue 添加全局功能的工具代码。

翻看 Vue 的官方文档,发现 Vue 的 Plugins 功能能够实现这个需求。

插件 (Plugins) 是一种能为 Vue 添加全局功能的工具代码。使用插件的方式如下:

js
import { createApp } from 'vue'

const app = createApp({})

app.use(myPlugin, {
  /* 可选的选项 */
})

插件可以是一个带有 install() 方法的对象,也可以是一个函数。如果是对象,那么它必须提供一个 install 方法。如果是函数,那么它会被作为 install 方法。install 方法调用时,会将 app 作为参数传入。

js
const myPlugin = {
  install(app, options) {
    // 配置此应用
  }
}

// 或者
function myPlugin(app, options) {
  // 配置此应用
}

而且插件是没有明确的使用范围的,也就是说我们既可以使用插件来添加全局的自定义指令和组件,也可以使用插件来添加全局的工具函数。官方也给出了一些使用场景:

在下面的例子中就会用到 app.component()app.directive() 方法来注册全局组件和自定义指令。

为什么不用 Vue.extend

为什么在创建 Loading 插件的时候不像 Vue2 自定义插件 使用 Vue.extend, 而是使用了 createApp

在 Vue3 的迁移指南中有说道:

在 Vue 2.x 中,Vue.extend 曾经被用于创建一个基于 Vue 构造函数的“子类”,其参数应为一个包含组件选项的对象。在 Vue 3.x 中,我们已经没有组件构造器的概念了。应该始终使用 createApp 这个全局 API 来挂载组件

本文用的是 createApp 创建的,也可以使用:createVNoderender 函数来创建。

自定义全局指令

在项目的 src 目录下创建 directives 文件夹,然后在 directives 文件夹下创建 colorfont 文件夹,最后在 colorfont 文件夹下创建 index.js 文件。

directives/color/index.js 文件内容

js
export default {
  created(el, binding) {
    el.style.color = binding.value
  }
}

directives/font/index.js 文件内容

js
export default {
  created(el, binding) {
    el.style.fontFamily = binding.value
  }
}

以上的两个指令都非常简单,只是作为例子示范,实际开发中可以根据自己的需求来编写。

directives/index.js 文件内容如下

js
import color from './color'
import font from './font'

const directives = {
  color,
  font
}

export default {
  install(app) {
    Object.keys(directives).forEach((key) => {
      app.directive(key, directives[key])
    })
  }
}

这里的 directives 对象是用来存放所有的指令的,然后通过 Object.keys 遍历 directives 对象,然后通过 app.directive 注册指令,这样子就可以注册全部的指令了。

其实上面的这种把一个个指令全部手动引入到 directives/index.js 文件中的方式并不是很好,因为如果指令很多的话,那么就会显得很臃肿,所以我们可以通过 import.meta.glob 来实现自动引入指令。大概方式如下:

ts
import type { App } from 'vue'

interface Directive {
  name: string
  directive: Record<string, () => {}>
}

const directives = import.meta.glob<Record<string, Directive>>('./**/*.ts', { eager: true })

export function setDirectives(app: App) {
  Object.keys(directives).forEach((key) => {
    app.directive(directives[key].default.name, directives[key].default.directive)
  })
}

自定义全局组件

在项目的 src 目录下创建 components 文件夹,然后在 components 文件夹下创建 Loading 文件夹,最后在 Loading 文件夹下创建 index.vueindex.js 文件。

components/Loading/index.vue 文件内容

vue
<script setup>
defineProps({
  title: {
    type: String,
    default: 'loading...'
  },
  loading: {
    type: Boolean,
    default: true
  }
});
</script>

<template>
  <div v-if="loading" class="loading-container">
    <div class="loading-rotate" />
    {{ title }}
  </div>
</template>

<style scoped>
.loading-container {
  position: fixed;
  top: 0;
  left: 0;
  display: flex;
  justify-content: center;
  align-items: center;
  flex-direction: column;
  width: 100%;
  height: 100%;
  background: #fff;
  font-size: 20px;
  font-family: monospace;
  color: #f00;
  z-index: 999;

}
.loading-rotate {
  width: 40px;
  height: 40px;
  margin-bottom: 10px;
  border: 5px solid #ccc;
  border-top-color: #f00;
  border-radius: 50%;
  animation: loading 1s linear infinite;
}
@keyframes loading {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}
</style>

components/Loading/index.js 文件内容

js
import { createApp } from 'vue';
import Loading from './index.vue';

let instance;

const loading = function (options) {
  if (!instance) {
    const app = createApp(Loading, {
      ...options
    });

    instance = app.mount(document.createElement('div'));

    document.body.appendChild(instance.$el);
  }

  instance.close = function () {
    document.body.removeChild(instance.$el);
    instance = null;
  };

  return instance;
};

export default loading;

components/Loading/index.js 是用来创建全局 loading 指令的,后续可以通过 $loading 来调用,而不是要在每个组件中都引入 Loading 组件。

components/index.js 文件内容

js
import loading from './Loading';
import Loading from './Loading/index.vue';

const components = {
  Loading
};

const fns = {
  loading
};

export default {
  install(app) {
    Object.keys(fns).forEach((key) => {
      app.provide(key, fns[key]);
      // app.config.globalProperties[`$${key}`] = fns[key];
    });
    Object.keys(components).forEach((key) => {
      app.component(key, components[key]);
    });
  }
};

components/index.js 文件是用来注册全局组件的,这里注册了 Loading 组件,然后通过 app.component 注册组件,这样子就可以注册全部的组件了。

通过 app.provide 方法把 loading 方法注入到全局,如此就可以在整个应用层面提供依赖,这样子就可以通过 inject 方法来注入 loading 方法,从而实现全局服务式调用组件的效果。

你可能注意到了 components/index.js 文件中舍弃了的 app.config.globalProperties 方法挂载全局服务式调用组件的方法,这是因为在官方的项目 issue 中尤大大说了:

getCurrentInstance is used mostly for official vue libraries that need additional internal access, not for userland application code. It was mistakenly documented in WIP v3 docs but is no longer considered a public API

简单的说就是 getCurrentInstance 方法是用来给官方的库使用的,而不是用来给用户使用的,所以在这里就不使用了。

注册全局组件和指令

main.js 文件中注册全局组件和指令。

js
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'

import components from './components'
import directives from './directives'

const app = createApp(App)

app.use(components)
app.use(directives)

app.mount('#app')

使用全局组件和指令

App.vue 文件中使用全局组件和指令。

vue
<script setup>
import { inject } from 'vue';

const $loading = inject('loading');

const loading1 = $loading({
  title: 'loading...'
});

setTimeout(() => {
  loading1.close();
}, 4000);

let loading2;

setTimeout(() => {
  loading2 = $loading({
    title: '加载中...'
  });
}, 6000);

setTimeout(() => {
  loading2.close();
}, 10000);
</script>

<template>
  <div class="app-container">
    <p v-color="`red`" v-font="`monospace`">
      vue-directives-components
    </p>
    <div>全局自定义指令</div>
    <div>全局自定义组件</div>
  </div>
</template>

<style scoped>
.app-container {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  width: 100%;
  height: 100vh;
  color: #474747;
}
</style>

由于我们舍弃 app.config.globalProperties 方法挂载全局服务式调用组件的方法,所以不能用 getCurrentInstance 方法来获取 loading 方法,而是通过依赖注入的方式,所以在使用的时候需要通过 inject 方法来注入 loading 全局服务式调用组件。

总结

通过上面的例子可以看出,自定义全局组件和指令的方式其实就是通过 app.componentapp.directive 方法来注册全局组件和指令,然后通过 app.provide 方法把组件注入到全局,从而实现全局服务式调用组件的效果。

最终通过插件(Plugins)为 Vue 添加全局功能的工具代码,从而实现自定义全局组件和指令的效果。

可以看到 Vue 的插件功能非常强大,可以实现很多功能,比如:添加全局的工具函数,添加全局的自定义指令,添加全局的自定义组件,添加全局的工具函数等等。这方面的功能非常使用,可以根据自己的需求来实现功能,而且插件是没有明确的使用范围的,也就是说我们既可以使用插件来添加全局的自定义指令和组件,也可以使用插件来添加全局的工具函数。

参考