Code前端首页关于Code前端联系我们

Vue3中use函数到底是什么?怎么用use注册插件和自定义组合式函数?

terry 17小时前 阅读数 268 #Vue

接触Vue3开发有段时间的朋友,肯定对use这个关键词不陌生——要么在main.js/main.ts里见过它配VueRouter、Pinia,要么在写组件时用它引入过自己封装的useMouse、useDark这些小工具,但真要掰扯清楚use的本质、两种主要用法的区别,可能不少人还会有点懵,今天咱们就把这个事儿说透,从核心原理到具体案例,连踩过的小坑都给你提个醒。

use在Vue3里有两个完全不同的身份,别搞混

很多新手一开始踩坑,都是因为把两种use当成了同一个东西,其实不是!Vue3的use家族分两支:一支是全局的app.use(),专门管插件的全局注册;另一支是组件/组合式函数里的useXxx(),属于组合式API的命名规范,用来复用逻辑,咱们先从最常用、也是最早接触的全局use开始聊。

全局身份:app.use()是怎么把第三方库“装”进Vue应用的?

从Vue2.x的Vue.use()升级到3.x的app.use(),本质没大变,都是帮你做插件的“一键初始化”,不用你在每个组件里手动import、手动调用挂载逻辑,但Vue3改成实例级别的注册后,灵活性更高了——你可以给不同的app实例装不同的插件,适合做微前端或者同项目多入口的场景。

那app.use()到底在后台干了啥?咱们可以拆解成4步核心逻辑:

  1. 判断插件是不是已经注册过:为了避免重复注册带来的性能浪费或者逻辑冲突,app.use()内部有个Set集合专门存已注册的插件,如果重复传同一个插件,它会直接跳过,不会报错。
  2. 处理插件的两种形态:插件可以是一个带install方法的对象,也可以是一个纯函数——如果是纯函数,它会被直接当作install方法用。
  3. 调用install方法并传参:install方法的第一个参数固定是当前的app实例,第二个及以后的参数是你在app.use(插件, 配置1, 配置2)里传的额外配置项,举个例子,比如用VueRouter的时候,我们会传createRouter返回的实例,其实那个实例就是带install方法的对象,app.use()拿到后会把app传给它的install,让它去挂载全局组件(比如RouterLink、RouterView)、注册全局属性(router、$route)、添加路由守卫之类的。
  4. 返回app实例,支持链式调用:这是个很贴心的小设计,你可以连续写app.use(Router).use(Pinia).use(ElementPlus),不用单独写三行。

手写一个简单的全局插件,验证app.use()的逻辑

光说原理可能不够直观,咱们来写个能全局显示当前时间的小插件试试,这个插件要实现两个功能:① 注册一个全局组件<GlobalTime>,能自动更新当前时间;② 给每个组件的this上(或者setup里的getCurrentInstance().proxy上)挂一个$getNowTime()方法,用来获取格式化后的时间。

// src/plugins/globalTime.js
import { ref, onMounted, onUnmounted, h } from 'vue';
// 定义带install方法的插件对象
const GlobalTimePlugin = {
  install(app, options = {}) {
    // 处理用户传的配置项,比如时间格式
    const { format = 'YYYY-MM-DD HH:mm:ss' } = options;
    // 注册全局组件
    app.component('GlobalTime', {
      setup() {
        const nowTime = ref('');
        let timer = null;
        // 模拟简单的时间格式化(生产环境推荐用dayjs/moment)
        const formatTime = (date) => {
          const padZero = (num) => String(num).padStart(2, '0');
          return format
            .replace('YYYY', date.getFullYear())
            .replace('MM', padZero(date.getMonth() + 1))
            .replace('DD', padZero(date.getDate()))
            .replace('HH', padZero(date.getHours()))
            .replace('mm', padZero(date.getMinutes()))
            .replace('ss', padZero(date.getSeconds()));
        };
        onMounted(() => {
          nowTime.value = formatTime(new Date());
          timer = setInterval(() => {
            nowTime.value = formatTime(new Date());
          }, 1000);
        });
        onUnmounted(() => {
          clearInterval(timer);
        });
        return () => h('span', { class: 'global-time' }, nowTime.value);
      }
    });
    // 注册全局方法和属性
    app.config.globalProperties.$getNowTime = () => {
      const padZero = (num) => String(num).padStart(2, '0');
      const date = new Date();
      return format
        .replace('YYYY', date.getFullYear())
        .replace('MM', padZero(date.getMonth() + 1))
        .replace('DD', padZero(date.getDate()))
        .replace('HH', padZero(date.getHours()))
        .replace('mm', padZero(date.getMinutes()))
        .replace('ss', padZero(date.getSeconds()));
    };
  }
};
export default GlobalTimePlugin;

然后在main.js里注册它:

// src/main.js
import { createApp } from 'vue';
import App from './App.vue';
import GlobalTimePlugin from './plugins/globalTime';
const app = createApp(App);
// 可以传自定义配置,比如改成中文短格式
app.use(GlobalTimePlugin, { format: 'YYYY年MM月DD日 HH时mm分ss秒' });
app.mount('#app');

现在你在任何组件里,不管是用<GlobalTime>标签,还是用this.$getNowTime()(或者setup里先引入getCurrentInstance,然后proxy.$getNowTime()),都能正常用啦。

组合式身份:useXxx()只是个命名规范,但为什么一定要遵守?

聊完全局的,咱们再来说组件/组合式函数里的use,其实从Vue3的API本身来说,你完全可以给组合式函数起任何名字,比如叫getMouse、manageDark,但官方和所有社区的组合式API库(比如VueUse、ElementPlus的hooks库)都严格遵守use开头的命名规范,这是为啥?

遵守useXxx()规范的3个核心原因

  1. 区分普通函数和组合式函数:组合式函数和普通JS函数最大的区别是,它可以在内部调用Vue3的响应式API(ref、reactive、computed、watch、生命周期钩子),而且必须在组件的setup()顶层或者其他组合式函数的顶层调用——用use开头,一眼就能看出来“这个函数不能随便乱放,不能在条件判断/循环/异步函数里直接用”,避免很多低级错误。
  2. IDE/工具链的自动识别:现在主流的IDE(比如VS Code + Volar)、代码检查工具(比如ESLint + vue-eslint-plugin)都对useXxx()有专门的支持——比如Volar会自动给use开头的函数做类型提示,vue-eslint-plugin的vue/composables-order规则会要求你把所有use开头的函数放在setup()的最前面,这些都能大大提升开发效率和代码质量。
  3. 社区生态的统一:不管是用官方推荐的VueUse,还是自己封装团队内部的hooks,统一的命名规范能让代码更易读、易维护——新人接手项目,看到useMouse就知道是管鼠标位置的,看到useLocalStorage就知道是管本地存储的,不用去翻函数内部的代码。

手写一个符合规范的自定义组合式函数useLocalStorage

既然useXxx()只是规范,那咱们自己写一个也很简单,就拿最常用的本地存储来举例子,这个组合式函数要实现几个功能:① 读取localStorage里的指定key值,如果没有就用默认值;② 监听数据变化,自动同步到localStorage;③ 支持JSON序列化和反序列化,因为localStorage只能存字符串;④ 支持清除这个key。

// src/composables/useLocalStorage.js
import { ref, watch } from 'vue';
export function useLocalStorage(key, defaultValue) {
  // 先尝试从localStorage里读取
  const storedValue = localStorage.getItem(key);
  // 初始化响应式数据
  const data = ref(storedValue ? JSON.parse(storedValue) : defaultValue);
  // 监听data的变化,自动同步到localStorage
  watch(
    data,
    (newValue) => {
      if (newValue === null || newValue === undefined) {
        localStorage.removeItem(key);
      } else {
        localStorage.setItem(key, JSON.stringify(newValue));
      }
    },
    { deep: true } // 深度监听,处理对象/数组的变化
  );
  // 提供一个清除方法
  const clear = () => {
    data.value = null;
  };
  // 返回响应式数据和方法
  return { data, clear };
}

然后在组件里用:

<template>
  <div class="local-storage-demo">
    <h2>本地存储演示</h2>
    <p>当前存储的用户名:{{ userName }}</p>
    <input v-model="userName" placeholder="输入用户名" />
    <button @click="clearUserName">清除用户名</button>
  </div>
</template>
<script setup>
import { useLocalStorage } from '@/composables/useLocalStorage';
// 直接解构响应式数据,注意不能解构ref的value,要保持响应式
const { data: userName, clear: clearUserName } = useLocalStorage('vue3-demo-username', '游客');
</script>
<style scoped>
.local-storage-demo {
  padding: 20px;
  max-width: 400px;
  margin: 50px auto;
  border: 1px solid #eee;
  border-radius: 8px;
}
input {
  width: 100%;
  padding: 8px;
  margin: 10px 0;
  border: 1px solid #ddd;
  border-radius: 4px;
  box-sizing: border-box;
}
button {
  padding: 8px 16px;
  background-color: #42b983;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}
</style>

这个组合式函数是不是很好用?不管在哪个组件里需要用到本地存储的响应式同步,只要调用useLocalStorage()就行,不用重复写读取、序列化、监听、清除的代码。

用useXxx()时最容易踩的2个坑,一定要避开

虽然useXxx()只是个命名规范,但因为它依赖Vue3的响应式系统和setup()的调用时机,所以还是有几个坑要注意:

坑1:在条件判断/循环/异步函数里直接调用useXxx()

这个是新手最容易犯的错误,比如你想写一个“只有登录了才获取用户信息”的逻辑,然后写成了这样:

// 错误写法!
import { useUserInfo } from '@/composables/useUserInfo';
const isLoggedIn = ref(false);
if (isLoggedIn.value) {
  const { userInfo } = useUserInfo(); // 这里直接调用会报错
}

为什么会报错?因为Vue3的响应式API和生命周期钩子,都是依赖于当前正在执行的组件/组合式函数的上下文的——当你在条件判断里调用useXxx()时,上下文已经丢失了,Vue找不到对应的组件实例,就会抛出inject() can only be used inside setup() or functional components或者类似的错误。

那正确的写法是什么?有两种:

  1. 把条件判断放在组合式函数内部:修改useUserInfo,让它接收一个isLoggedIn的参数,只有当isLoggedIn为true时才去请求数据。
  2. 把useXxx()放在setup()顶层,然后在内部用条件判断控制逻辑:比如useUserInfo内部可以返回一个isLoading、error、fetchUserInfo方法,然后在组件里当isLoggedIn变化时调用fetchUserInfo。

坑2:解构组合式函数返回的ref时,直接去掉了.value

组合式函数返回的ref,如果你直接在setup()里解构赋值给普通变量,比如const { userInfo } = useUserInfo(),那userInfo本身还是ref对象,在template里可以直接用(Vue会自动解包),但在setup()的JS逻辑里必须加.value,比如console.log(userInfo.value)

如果你想在setup()的JS逻辑里也不加.value,可以用Vue3的toRefs()或者toRef()来处理,但要注意toRefs()只能处理响应式对象(reactive返回的),不能处理ref本身,比如如果useUserInfo返回的是一个包含多个ref的普通对象,你可以用VueUse的useToRefs(),或者自己手动写解构逻辑:

import { toRef } from 'vue';
import { useUserInfo } from '@/composables/useUserInfo';
const result = useUserInfo();
const userInfo = toRef(result, 'userInfo');
const isLoading = toRef(result, 'isLoading');
// 现在userInfo和isLoading还是ref,但你可以这样写(不加.value也可以,但推荐加上,避免混淆)
console.log(userInfo.value);

Vue3的use到底是什么?

今天咱们聊了这么多,总结一下就是:

  • Vue3的use有两个身份:全局的app.use()是插件注册器,用来一键初始化第三方库或自定义插件;组件/组合式函数里的useXxx()是组合式API的命名规范,用来区分普通函数和可以调用响应式API的逻辑复用函数。
  • app.use()的核心逻辑是检查重复、处理插件形态、调用install方法传参、返回app实例支持链式调用。
  • useXxx()规范必须遵守,因为它能区分函数类型、获得工具链支持、统一社区生态。
  • 用useXxx()时要避开两个坑:不能在条件判断/循环/异步函数里直接调用,不能在setup()的JS逻辑里随便去掉ref的.value。

其实不管是app.use()还是useXxx(),都是Vue3为了让代码更灵活、更易复用、更易维护而设计的——app.use()解决了全局配置的问题,useXxx()解决了组件间逻辑复用的问题,这两个东西组合起来,就是Vue3比Vue2.x更好用的核心原因之一。

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

热门