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

Vue3 globalProperties到底怎么用?有没有最佳实践和避坑指南?

terry 1小时前 阅读数 22 #Vue

很多刚从Vue2转到Vue3的开发者,一上来都会遇到全局挂载的问题——之前习惯用Vue.prototype.$xxx挂载工具函数、全局状态片段、第三方库的简化调用,结果Vue3直接把这个API改成了app.config.globalProperties,还搭配了provide/inject,难免有点懵:这俩是替代关系还是互补?什么时候用哪个?怎么用globalProperties才不容易踩雷?今天就把自己用了三年多Vue3的经验,结合官方文档和社区里的高频踩坑案例,好好聊一聊。

先搞懂:Vue3为什么要换掉Vue.prototype?

很多开发者可能觉得“能用就行,改个名字而已”,其实不是——Vue3做这个改动,是基于Composition API的设计思想和代码可维护性的长远考虑。

Vue.prototype本质上是在构造函数的原型链上挂载属性,这意味着所有Vue实例(包括组件实例、Vue2.x根实例的独立子实例)都会继承这些属性,不管它们需不需要,比如你只在5个复杂业务组件里用了全局挂载的echarts简化调用,但剩下的100个展示型组件也会挂载,虽然内存占用很小,但从软件工程的角度来看,这属于“无依赖的全局污染”,代码耦合度会变高,后续重构或者删除某个工具函数时,你根本不知道哪些地方还在偷偷用它。

Composition API里的setup函数是在组件实例创建之前执行的,这时候组件实例的this还没初始化,所以用Vue.prototype挂载的$xxx根本没法在setup里直接调用——而Vue3现在主推的就是Composition API,原来的Options API虽然兼容,但很多新特性都是围绕setup设计的,要是还用原型链挂载,会在两种写法的切换里折腾半天。

app.config.globalProperties是绑定在应用实例上的,而不是全局的Vue构造函数,这意味着如果你在同一个页面里运行多个Vue3应用实例,每个应用的全局属性是完全隔离的,不会互相干扰——这个特性在做微前端、或者用Vue3做组件库嵌入其他页面的时候,非常重要。

基础用法:怎么用globalProperties挂载和访问全局属性?

既然知道了为什么要改,那先从最基础的“怎么用”开始说。

挂载全局属性

不管是在main.js/main.ts里,还是在某个单独的插件文件里,globalProperties的挂载方式都是一样的:先拿到应用实例app,然后用点语法往app.config.globalProperties上挂属性就行。

先举个最常用的例子——挂载全局的工具函数,比如格式化时间的$formatTime:

// main.js
import { createApp } from 'vue'
import App from './App.vue'
import dayjs from 'dayjs' // 假设用dayjs做时间格式化
const app = createApp(App)
// 挂载全局工具函数
app.config.globalProperties.$formatTime = (timestamp, format = 'YYYY-MM-DD HH:mm:ss') => {
  if (!timestamp) return ''
  return dayjs(timestamp).format(format)
}
app.mount('#app')

除了工具函数,你还可以挂载第三方库的简化实例,比如把axios的基础实例挂成$axios,把echarts挂成$echarts;甚至可以挂载一些轻量的全局状态片段,比如当前登录用户的头像URL、主题色配置(但要注意,globalProperties上的状态是响应式的吗? 这个问题后面会重点说,先留个悬念)。

访问全局属性

访问的时候要注意场景:Options API和Composition API的访问方式不一样,这也是很多刚转Vue3的开发者容易搞错的地方。

场景1:Options API(包括.vue文件里的data、methods、computed、生命周期钩子等)

在Options API里,访问方式和Vue2的Vue.prototype完全一样,直接用this.$xxx就行——因为Vue3的组件实例会从应用实例的globalProperties上继承这些属性,就像继承data里的属性一样。

比如在某个.vue文件的methods里用刚才挂载的$formatTime:

<template>
  <div>{{ formattedTime }}</div>
</template>
<script>
export default {
  data() {
    return {
      createTime: 1718947200000 // 2024-06-21 00:00:00的时间戳
    }
  },
  computed: {
    formattedTime() {
      // 直接用this.$formatTime
      return this.$formatTime(this.createTime, 'YYYY年MM月DD日')
    }
  },
  mounted() {
    console.log(this.$formatTime(Date.now()))
  }
}
</script>

场景2:Composition API的setup函数(最常用的场景)

前面说了,setup函数在组件实例创建之前执行,this是undefined,所以不能用this.$xxx,那怎么办?Vue3提供了一个getCurrentInstance的组合式API,用来获取当前组件的实例上下文。

注意!getCurrentInstance只能在setup函数或者组合式函数(也就是自定义hook)的同步执行阶段调用,不能在异步操作(比如setTimeout、axios请求的then/catch、async/await里的await之后)调用,否则会返回null。

那正确的访问方式是什么样的?

<template>
  <div>{{ formattedTime }}</div>
</template>
<script setup>
import { computed, getCurrentInstance } from 'vue'
// 解构获取proxy!这点很重要!
// getCurrentInstance返回的实例有两个属性:proxy和ctx
// proxy是生产环境和开发环境都能用的安全代理,和Options API的this完全一致
// ctx是开发环境才有的调试上下文,生产环境会被删除,千万不要用ctx访问globalProperties
const { proxy } = getCurrentInstance()
const createTime = 1718947200000
const formattedTime = computed(() => {
  return proxy.$formatTime(createTime, 'YYYY年MM月DD日')
})
// 注意!如果是在异步函数里,要在同步阶段先把proxy存下来
const fetchData = async () => {
  // ❌ 错误做法:在await之后调用getCurrentInstance,返回null
  // const { proxy: asyncProxy } = getCurrentInstance()
  // ✅ 正确做法:在同步阶段先保存proxy
  const res = await fetch('https://api.example.com/time')
  const data = await res.json()
  console.log(proxy.$formatTime(data.serverTime))
}
</script>

进阶挂载:用插件封装全局属性

如果要挂载的全局属性比较多,比如五六个工具函数、三四个第三方库简化实例,直接写在main.js里会让代码变得很乱,这时候可以用Vue3的插件系统封装一下,代码会更清晰,也更容易复用。

Vue3的插件其实就是一个带有install方法的对象,或者是一个install函数,install方法会接收两个参数:第一个是应用实例app,第二个是可选的插件配置项。

比如刚才的时间格式化工具,我们可以封装成一个单独的插件:

// plugins/format.js
import dayjs from 'dayjs'
export default {
  install(app, options = {}) {
    // 可以通过options传默认的时间格式
    const defaultFormat = options.defaultFormat || 'YYYY-MM-DD HH:mm:ss'
    // 挂载全局工具函数
    app.config.globalProperties.$formatTime = (timestamp, format = defaultFormat) => {
      if (!timestamp) return ''
      return dayjs(timestamp).format(format)
    }
    // 还可以用provide/inject一起提供,后面会说
    app.provide('formatTime', app.config.globalProperties.$formatTime)
  }
}

然后在main.js里注册这个插件:

// main.js
import { createApp } from 'vue'
import App from './App.vue'
import formatPlugin from './plugins/format.js'
const app = createApp(App)
// 注册插件,可以传默认配置
app.use(formatPlugin, { defaultFormat: 'YYYY年MM月DD日 HH:mm' })
app.mount('#app')

核心疑问:globalProperties和provide/inject是替代关系吗?

这是社区里讨论最多的问题,没有之一,很多刚转Vue3的开发者会觉得:“既然Vue3推荐Composition API,推荐provide/inject,那globalProperties是不是可以不用了?”或者反过来:“provide/inject用起来麻烦,还要传key,不如globalProperties直接用this.$xxx方便,继续用不行吗?”

其实这俩不是替代关系,是互补关系,各自有适合的使用场景,不能一概而论。

先看provide/inject的特点和适用场景

provide/inject是Vue3(其实Vue2.2+就有,但Vue3做了增强,支持响应式)提供的跨层级组件通信机制,当然也可以用来做“全局级别的通信”——在根组件或者应用实例上provide,然后在任何子组件里inject。

它的特点很明显:

  1. 显式依赖:哪个组件需要用这个属性,必须显式inject,一眼就能看出来,代码的可读性和可维护性都很高,重构的时候可以精准定位到依赖这个属性的组件。
  2. 无全局污染:只有显式inject的组件才能访问这个属性,其他组件不会受影响,应用实例之间也可以隔离。
  3. 支持响应式增强:Vue3里可以用ref/reactive包裹provide的值,然后inject之后的值也是响应式的(要注意用shallowRef/shallowReactive避免不必要的响应式开销,或者用provide/inject的默认参数里的computed来控制只读)。
  4. Composition API友好:不需要用getCurrentInstance,直接用inject组合式API就能在setup的任何地方(包括异步操作里)访问,非常方便。

它的适用场景主要是:

  • 跨多层级的复杂组件通信(比如爷孙组件、曾孙组件通信,用props/emit会太麻烦,中间层组件会变成“传声筒”)。
  • 需要共享的响应式全局状态(比如用户登录信息、主题色配置、语言配置——当然如果状态很复杂,还是推荐用Pinia,provide/inject适合轻量的状态)。
  • 插件提供的可配置功能(比如刚才的formatTime插件,同时用provide和globalProperties提供,开发者可以根据自己的习惯选择用哪个)。

再看globalProperties的特点和适用场景

globalProperties的特点就是:

  1. 隐式依赖:不需要显式声明,所有组件的this(Options API)或者proxy(Composition API的setup同步阶段)都能访问,写起来很快,但重构的时候很难定位依赖。
  2. 有一定的全局污染风险:所有组件都能访问,但大部分组件可能不需要。
  3. 响应式有限制:如果你直接往globalProperties上挂一个普通对象,比如app.config.globalProperties.$user = { name: '张三' },那修改$user.name的时候,模板里的{{ $user.name }}不会更新——因为globalProperties上的属性本身不是响应式的,除非你用ref/reactive包裹一下,但即使包裹了,在Options API里访问的时候要注意.value吗?后面会讲这个坑。
  4. Options API友好:和Vue2的写法完全兼容,对于还在维护旧项目、或者习惯用Options API的开发者来说,非常顺手。

它的适用场景主要是:

  • 纯工具函数(比如时间格式化、数字格式化、防抖节流的简化封装):这些工具函数一般是纯函数,没有状态,也不需要响应式,用globalProperties挂载后,在Options API里直接用this.$xxx,写起来很快,也不会有太大的问题。
  • 第三方库的简化调用(比如把axios的基础实例挂成$axios,把dayjs挂成$dayjs,但注意像lodash这种按需引入的库,不要全局挂载完整的包,会增加打包体积,除非你用了tree-shaking)。
  • 兼容旧项目的代码:如果你是从Vue2的项目重构到Vue3,用globalProperties可以快速迁移,不用一下子改所有组件的代码,之后再慢慢用provide/inject或者Pinia替换。

简单总结:什么时候用哪个?

给大家一个简单的判断标准:

  1. 如果你写的是新的Vue3项目,主推Composition API:尽量用provide/inject,或者Pinia(状态复杂的话),尽量少用globalProperties。
  2. 如果你写的是轻量的纯工具函数,习惯用Options API:可以用globalProperties。
  3. 如果你写的是插件:最好同时用globalProperties和provide/inject提供,给开发者选择的空间。
  4. 如果你写的是响应式的全局状态:不管用什么API,都不要直接用globalProperties,除非你对响应式的处理非常熟练,否则推荐用provide/inject或者Pinia。

避坑指南:这5个坑你踩过几个?

globalProperties看起来简单,但其实有很多细节容易踩坑,我自己踩过3个,社区里也看到过很多人踩,今天就把最常见的5个坑列出来,帮大家避避。

坑1:Composition API里用ctx访问globalProperties

刚才在基础用法里已经提到过了,但还是要单独列出来,因为这是最最最常见的坑——很多刚转Vue3的开发者看了一些旧的教程,里面会用const { ctx } = getCurrentInstance(),然后用ctx.$xxx访问,但这个ctx是开发环境的调试上下文,生产环境会被完全删除,上线后代码会直接报错:Cannot read properties of undefined (reading '$formatTime')。

✅ 正确做法:一定要用proxy,也就是const { proxy } = getCurrentInstance(),然后用proxy.$xxx访问。

坑2:在异步操作里调用getCurrentInstance

getCurrentInstance只能在setup函数或者组合式函数的同步执行阶段调用,

// ✅ 正确:同步阶段调用
const { proxy } = getCurrentInstance()
const fetchData = async () => {
  // ❌ 错误:await之后是异步阶段,getCurrentInstance返回null
  // const { proxy: asyncProxy } = getCurrentInstance()
  // ✅ 正确:用同步阶段存下来的proxy
  const res = await proxy.$axios.get('https://api.example.com/data')
  console.log(res)
}

如果组合式函数里也有异步操作,同样要在组合式函数的同步阶段先把需要的proxy或者全局属性存下来:

// useUser.js 自定义hook
import { getCurrentInstance } from 'vue'
export default function useUser() {
  // ✅ 正确:同步阶段调用
  const { proxy } = getCurrentInstance()
  const fetchUserInfo = async () => {
    const res = await proxy.$axios.get('/api/user/info')
    return res.data
  }
  return { fetchUserInfo }
}

坑3:globalProperties上的响应式属性处理不当

刚才提到过,globalProperties本身不是响应式的,如果你直接挂一个普通对象:

// main.js
app.config.globalProperties.$user = { name: '张三' }

然后在某个组件里修改:

<script>
export default {
  methods: {
    changeName() {
      this.$user.name = '李四'
    }
  }
}
</script>

模板里的{{ $user.name }}不会更新,因为$user不是响应式的。

那如果用ref/reactive包裹呢?

// main.js
import { reactive } from 'vue'
app.config.globalProperties.$user = reactive({ name: '张三' })

这时候修改this.$user.name,模板里的{{ $user.name }}会更新吗?在Options API里会!因为Vue3的组件实例会自动解包proxy上的ref/reactive属性,就像解包data里的属性一样——但是在Composition API的setup函数里用proxy.$user的话,需要注意吗?不需要!因为proxy就是Options API的this的代理,也会自动解包。

如果你往globalProperties上挂的是一个ref的基本类型,

// main.js
import { ref } from 'vue'
app.config.globalProperties.$count = ref(0)

这时候在Options API里用this.$count,会自动解包成0,不用写.value;但如果在组合式函数的同步阶段先存下来这个$count:

// ❌ 错误做法
const { proxy } = getCurrentInstance()
const count = proxy.$count // 这时候count是0,不是ref,修改count不会更新模板

✅ 正确做法:如果在Composition API里需要访问globalProperties上的ref/reactive属性,并且要修改它的话,最好不要用proxy,而是用provide/inject一起提供:

// main.js
import { reactive, ref } from 'vue'
const user = reactive({ name: '张三' })
const count = ref(0)
app.config.globalProperties.$user = user
app.config.globalProperties.$count = count
// 同时用provide提供
app.provide('user', user)
app.provide('count', count)

然后在Composition API的setup函数里用inject:

<script setup>
import { inject } from 'vue'
// inject的是ref/reactive本身,不是解包后的值
const user = inject('user')
const count = inject('count')
const changeName = () => {
  user.name = '李四'
}
const increment = () => {
  count.value++ // 这里要写.value
}
</script>

坑4:全局属性的命名冲突

globalProperties是一个全局的命名空间,如果你挂的属性名和组件自身的data、methods、computed属性名冲突了,会怎么样?组件自身的属性会覆盖globalProperties上的属性——

<script>
export default {
  data() {
    return {
      // 和globalProperties上的$formatTime冲突了
      $formatTime: '我是组件自己的属性'
    }
  },
  mounted() {
    console.log(this.$formatTime) // 输出“我是组件自己的属性”,不是工具函数
  }
}
</script>

虽然官方建议全局属性名加上$前缀,但还是有可能冲突——比如你自己加了个$axios,结果第三方组件库也加了个$axios,那就麻烦了。

✅ 正确做法:

  1. 尽量用更独特的前缀,比如你的项目名缩写,比如如果你的项目叫“米家商城”,可以用$mjmFormatTime、$mjmAxios。
  2. 如果用插件封装,可以通过插件的配置项让开发者自定义前缀。
  3. 尽量多用provide/inject,显式传key,key也可以用项目名缩写或者Symbol(Symbol完全不会冲突)。

坑5:打包后globalProperties上的属性被 tree-shaking 掉了?

这个坑比较少见,但如果你的工具函数是按需引入的,或者你的第三方库是按需引入的,可能会遇到——比如你只在工具函数里用了dayjs的format方法,然后用tree-shaking打包,结果dayjs的format方法被 tree-shaking 掉了?不对,其实不是globalProperties的问题,是tree-shaking的判断逻辑问题——因为globalProperties上的属性是动态挂载的,tree-shaking有时候会判断不出来这个工具函数有没有被使用,所以会把它和它依赖的第三方库代码一起删掉。

✅ 正确做法:

  1. 如果是自己写的工具函数,尽量不要太“动态”,比如直接在install方法里写死,不要通过配置项动态生成(除非配置项是静态的)。
  2. 如果依赖第三方库,确保第三方库支持ES模块(也就是package.json里有"module"字段),并且你是用import的方式引入的,不是require。
  3. 如果还是被 tree-shaking 掉了,可以在工具函数里加一个/#PURE/的注释,或者在webpack/vite的配置里把这个工具函数和它依赖的第三方库加入到noParse或者exclude里(不推荐,会增加打包体积)。

最佳实践:怎么用globalProperties才合理?

说了这么多用法和避坑,最后给大家总结几个最佳实践,按照这些来用,globalProperties不仅不会成为你的负担,还能提高你的开发效率。

最佳实践1:只挂载纯工具函数和无状态的第三方库简化实例

尽量不要往globalProperties上挂载有状态的东西,比如全局的用户信息、购物车数据——这些有状态的东西,要么用Pinia(状态复杂的话),要么用provide/inject(状态轻量的话)。

纯工具函数的例子:时间格式化、数字格式化、防抖节流的简化封装、正则表达式的验证函数等。 无状态的第三方库简化实例的例子:axios的基础实例(注意不要在实例上挂载有状态的东西,比如请求拦截器里的全局loading状态,那个最好用Pinia或者provide/inject管理)、dayjs的实例、lodash-es的按需引入函数的集合等。

最佳实践2:用插件封装,并且同时提供globalProperties和provide/inject

如果要挂载的全局属性比较多,一定要用插件封装,代码会更清晰,也更容易复用,最好同时用globalProperties和provide/inject提供,给开发者选择的空间——习惯用Options API的可以用globalProperties,习惯用Composition API的可以用provide/inject。

最佳实践3:给全局属性加独特的前缀

尽量用更独特的前缀,比如你的项目名缩写,避免和组件自身的属性或者第三方组件库的属性冲突,如果用插件封装,可以通过配置项让开发者自定义前缀。

最佳实践4:新的Vue3项目尽量少用globalProperties,多用provide/inject和Pinia

虽然globalProperties很方便,但它的隐式依赖会降低代码的可维护性,尤其是当项目变大之后——你根本不知道哪些地方还在偷偷用你挂载的全局属性,重构的时候会非常痛苦,所以新的Vue3项目,尽量多用provide/inject和Pinia,尽量少用globalProperties。

最佳实践5:在TS项目里给globalProperties加类型声明

如果你的项目用的是TypeScript,直接用proxy.$xxx会报错,因为TypeScript不知道proxy上有这些属性——这时候需要给globalProperties加类型声明。

怎么加?很简单,在项目的src目录下新建一个global-properties.d.ts文件(名字可以随便取,只要是.d.ts结尾的就行),然后写:

// src/global-properties.d.ts
import type { FormatTimeFunction } from './plugins/format' // 假设你封装了formatTime的类型
declare module 'vue' {
  interface ComponentCustomProperties {
    $formatTime: FormatTimeFunction
    $axios: typeof import('axios').default
  }
}
// 确保这个文件被tsconfig.json包含

然后在tsconfig.json的include数组里加上这个文件(src目录下的所有.d.ts文件都会被自动包含,不用手动加,但如果没生效的话,可以手动加一下)。

写在最后

Vue3的globalProperties虽然不是什么复杂的API,但它的细节很多,尤其是和Composition API、provide/inject搭配使用的时候,很容易踩坑,希望今天的这篇文章能帮你搞懂globalProperties的用法、最佳实践和避坑指南,以后用的时候能少走弯路。

最后再强调一遍:globalProperties和provide/inject不是替代关系,是互补关系,各自有适合的使用场景——不要一味地否定其中一个,也不要一味地只用其中一个,要根据实际情况选择合适的方式。

版权声明

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

热门