Vue3开发中全局方法怎么定义和用?和Vue2原型、provide/inject比选哪个?
你刚从Vue2转到Vue3写项目,之前习惯用Vue.prototype.$xxx挂载全局工具函数,比如格式化时间的$formatTime、弹出提示的$message?现在一查官方文档发现Vue3的根实例构造函数改得像“简化版工具人”,原型链直接绑定好像不太对劲?甚至还有人说Composition API里用app.config.globalProperties太麻烦,不如provide/inject好用?别急,今天这篇文章就把Vue3全局方法的所有事说透:从最基础的两种定义方式,到不同场景下的选型建议,再到TypeScript下的类型适配,连踩过的坑都会提,看完你直接就能上手用。
Vue3全局方法的定义核心是什么?
首先得理清楚Vue3的架构变化——核心团队砍掉了原来Vue.prototype这种全局可变的挂载点,主要是为了避免原型链污染,防止多人协作时你用$msg当消息提示我用$msg当表单验证器,互相覆盖还难排查,那现在Vue3给的官方推荐全局挂载方案是什么?就是app.config.globalProperties,这个属性本质是Vue3内部设计的“隔离式全局属性容器”,只在你初始化的这一个Vue应用实例及其子组件里生效,不会影响其他同时存在的Vue实例(比如页面上有两个独立的Vue弹窗组件),从根源上解决了污染问题。
那具体怎么用app.config.globalProperties定义和调用全局方法呢?举个最常用的格式化时间例子。
定义步骤
- 先写好你的全局方法,最好单独抽成一个utils/global.js文件,方便维护和复用,
// utils/global.js import dayjs from 'dayjs'; // 建议用成熟的第三方库,别自己手写复杂的时间转换逻辑
export const globalMethods = {
// 格式化时间,参数支持Date对象、时间戳、ISO字符串
formatTime(time, format = 'YYYY-MM-DD HH:mm:ss') {
if (!time) return '';
return dayjs(time).format(format);
},
// 简单的金额格式化,保留两位小数,带千分位
formatMoney(amount, symbol = '¥') {
const num = parseFloat(amount);
if (isNaN(num)) return ${symbol}0.00;
return ${symbol}${num.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',')};
}
};
在main.js/main.ts里引入并挂载到当前应用实例的globalProperties上,注意挂载前要先createApp生成实例:
```javascript
// main.js
import { createApp } from 'vue';
import App from './App.vue';
import { globalMethods } from './utils/global';
const app = createApp(App);
// 批量挂载更省事,不用一个个写app.config.globalProperties.$xxx
Object.keys(globalMethods).forEach(key => {
// 还是建议加个$前缀,和Vue2保持习惯,也避免和组件内部的data、props、computed重名
app.config.globalProperties[`$${key}`] = globalMethods[key];
});
app.mount('#app');
调用步骤
调用的时候要分两种场景,因为Vue3有Options API和Composition API两种写法,它们的访问方式不一样。
Options API里的调用
Options API的调用和Vue2完全一致,直接在template、methods、computed、watch里用$xxx就行,
<!-- App.vue Options API 写法 -->
<template>
<div class="home">
<p>当前时间:{{ $formatTime(new Date()) }}</p>
<p>商品金额:{{ $formatMoney(12345.6789) }}</p>
<button @click="showRandomTime">看随机时间</button>
</div>
</template>
<script>
export default {
name: 'Home',
methods: {
showRandomTime() {
const randomStamp = Date.now() - Math.floor(Math.random() * 31536000000); // 随机取过去一年的时间
alert(this.$formatTime(randomStamp, 'MM-DD HH:mm'));
}
}
};
</script>
Composition API里的调用
Composition API里不能直接用this(除非你是用<script setup>的顶层await或者Options API的遗留写法,不推荐),这时候要引入Vue3的getCurrentInstance函数,这个函数能获取当前组件的实例上下文,实例的proxy属性上就有挂载的globalProperties,
<!-- App.vue Composition API 写法 -->
<template>
<div class="home">
<p>当前时间:{{ formatCurrentTime }}</p>
<p>商品金额:{{ formatCurrentMoney }}</p>
<button @click="showRandomTime">看随机时间</button>
</div>
</template>
<script setup>
import { computed, getCurrentInstance } from 'vue';
// 获取当前组件的实例,注意getCurrentInstance只能在setup或生命周期钩子的同步代码里调用
const { proxy } = getCurrentInstance();
// 用computed包裹的话,性能更好,依赖变化时才会重新计算
const formatCurrentTime = computed(() => proxy.$formatTime(new Date()));
const formatCurrentMoney = computed(() => proxy.$formatMoney(12345.6789));
const showRandomTime = () => {
const randomStamp = Date.now() - Math.floor(Math.random() * 31536000000);
alert(proxy.$formatTime(randomStamp, 'MM-DD HH:mm'));
};
</script>
这里有个小坑要提醒:getCurrentInstance只在开发环境和生产环境的组件顶层、setup、生命周期钩子(onMounted、onCreated这种)的同步代码里有效,异步回调里(比如setTimeout、axios的then里)直接调用会返回null,所以最好在同步代码里把proxy解构出来存成变量,再用到异步里。
除了app.config.globalProperties,还有没有其他方式?
有,而且官方文档里其实也隐晦提过这个“备选但更推荐局部扩展”的方式——就是app.provide配合inject,很多人搞不清provide/inject和globalProperties的区别,其实它们的定位完全不一样:
- app.config.globalProperties是用来挂载纯工具类、无状态、全组件可能用到的全局方法/属性,比如刚才的$formatTime、$formatMoney,或者是全局的配置常量(比如后端API的基础URL,但常量也可以单独抽成config文件import,更推荐import,类型提示更方便)。
- app.provide是用来提供有状态、需要跨多层组件传递、不一定要全组件用到的数据或方法,比如用户登录后的个人信息、主题切换的方法(主题色可能要存储在localStorage里,有状态),或者是只有管理后台的侧边栏、内容区、顶部导航栏才会用到的侧边栏折叠状态。
那用app.provide怎么定义和调用“类全局”的方法呢?还是举个例子,比如主题切换的方法:
定义步骤
- 单独抽成一个composables/useTheme.js文件,方便管理状态和逻辑,复用性也更强:
// composables/useTheme.js import { ref, watch } from 'vue';
// 这里其实也可以不用provide/inject,直接import这个composable,但如果是深层嵌套的组件,provide/inject更方便 export const useTheme = () => { // 从localStorage里读取主题,默认是light const currentTheme = ref(localStorage.getItem('theme') || 'light');
// 切换主题的方法 const toggleTheme = () => { currentTheme.value = currentTheme.value === 'light' ? 'dark' : 'light'; };
// 监听主题变化,同步到localStorage和html标签的class上 watch(currentTheme, (newTheme) => { localStorage.setItem('theme', newTheme); document.documentElement.className = newTheme; }, { immediate: true }); // immediate: true表示组件初始化时就执行一次watch
return { currentTheme, toggleTheme }; };
// 定义一个key,用来provide和inject时匹配,最好用Symbol,避免和其他provide/inject的key重名 export const THEME_KEY = Symbol('theme');
在main.js里引入并provide:
```javascript
// main.js
import { createApp } from 'vue';
import App from './App.vue';
import { globalMethods } from './utils/global';
import { useTheme, THEME_KEY } from './composables/useTheme';
const app = createApp(App);
// 批量挂载工具函数
Object.keys(globalMethods).forEach(key => {
app.config.globalProperties[`$${key}`] = globalMethods[key];
});
// provide主题相关的状态和方法,根组件provide的话,所有子组件都能inject到
const { currentTheme, toggleTheme } = useTheme();
app.provide(THEME_KEY, { currentTheme, toggleTheme });
app.mount('#app');
调用步骤
不管是Options API还是Composition API,inject的调用方式都差不多,不过Composition API里更简单:
Composition API里的调用
<!-- components/Header.vue -->
<template>
<header class="header">
<span>当前主题:{{ currentTheme }}</span>
<button @click="toggleTheme">切换主题</button>
</header>
</template>
<script setup>
import { inject } from 'vue';
import { THEME_KEY } from '../composables/useTheme';
// 直接用inject注入,参数是之前定义的THEME_KEY,第二个参数是默认值(可选)
const { currentTheme, toggleTheme } = inject(THEME_KEY, {
currentTheme: 'light',
toggleTheme: () => console.warn('未提供主题相关的状态和方法')
});
</script>
Options API里的调用
<!-- components/Header.vue Options API -->
<template>
<header class="header">
<span>当前主题:{{ currentTheme }}</span>
<button @click="toggleTheme">切换主题</button>
</header>
</template>
<script>
import { THEME_KEY } from '../composables/useTheme';
export default {
name: 'Header',
inject: {
// 用对象的形式注入,key是组件内部用的变量名,from是之前定义的THEME_KEY,default是默认值
themeData: {
from: THEME_KEY,
default: () => ({
currentTheme: 'light',
toggleTheme: () => console.warn('未提供主题相关的状态和方法')
})
}
},
computed: {
currentTheme() {
return this.themeData.currentTheme;
}
},
methods: {
toggleTheme() {
this.themeData.toggleTheme();
}
}
};
</script>
TypeScript下怎么给全局方法加类型提示?
不管是globalProperties还是provide/inject,TypeScript下不加类型提示的话,写代码时会全是any,不仅容易出错,还体验很差,那怎么加呢?
app.config.globalProperties的类型适配
官方文档里明确说了,要给globalProperties加类型提示,需要扩展Vue的ComponentCustomProperties接口,具体步骤:
- 在src目录下新建一个types/vue-shim.d.ts文件(名字可以随便取,只要是.d.ts结尾的TypeScript声明文件就行):
// types/vue-shim.d.ts import dayjs from 'dayjs';
// 先声明刚才写的globalMethods的类型 declare const globalMethods: { formatTime(time: dayjs.ConfigType, format?: string): string; formatMoney(amount: number | string, symbol?: string): string; };
// 扩展Vue的ComponentCustomProperties接口,把globalMethods的方法加上$前缀 declare module 'vue' { interface ComponentCustomProperties { $formatTime: typeof globalMethods.formatTime; $formatMoney: typeof globalMethods.formatMoney; } }
// 这个是用来让TypeScript识别.vue文件的,如果你用的是Vue CLI或者Vite,可能已经有了,但最好加上 declare module '*.vue' { import type { DefineComponent } from 'vue'; const component: DefineComponent<{}, {}, any>; export default component; }
然后在tsconfig.json里的include数组里加上这个声明文件的路径,
```json
{
"compilerOptions": {
// 其他配置...
},
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue",
"src/types/**/*.d.ts" // 加上这个
]
}
这样不管是Options API里的this.$xxx,还是Composition API里的proxy.$xxx,都会有完整的类型提示了。
app.provide的类型适配
provide/inject的类型适配稍微麻烦一点,但核心就是定义好注入数据的类型,然后给Symbol加类型注解:
- 修改composables/useTheme.ts文件,加上类型:
// composables/useTheme.ts import { ref, watch, InjectionKey, Ref } from 'vue';
// 定义注入数据的类型 export interface ThemeData { currentTheme: Ref<'light' | 'dark'>; toggleTheme: () => void; }
// 定义Symbol时加上InjectionKey类型注解,参数是刚才定义的ThemeData
export const THEME_KEY: InjectionKey
export const useTheme = () => { const currentTheme: Ref<'light' | 'dark'> = ref(localStorage.getItem('theme') as 'light' | 'dark' || 'light');
const toggleTheme = () => { currentTheme.value = currentTheme.value === 'light' ? 'dark' : 'light'; };
watch(currentTheme, (newTheme) => { localStorage.setItem('theme', newTheme); document.documentElement.className = newTheme; }, { immediate: true });
return { currentTheme, toggleTheme }; };
2. 这样在Composition API里用inject的时候,TypeScript会自动推断出类型,甚至不需要给第二个默认值的类型注解(当然给了更安全),如果注入失败,TypeScript会报错提示,不会再是any了。
### 三种方式(算上Vue2遗留的话)该怎么选?
现在再把Vue2的原型链绑定也加进来对比,方便刚转过来的同学梳理思路:
| 方式 | Vue3支持度 | 污染风险 | 状态支持 | 类型提示(TS) | 适用场景 |
|------|-----------|---------|---------|--------------|---------|
| Vue.prototype.$xxx | 支持但**不推荐** | 极高 | 不推荐(容易混乱) | 可以但麻烦 | 完全不用,除非是迁移老项目的临时方案 |
| app.config.globalProperties | 官方推荐 | 极低(单应用隔离) | 无状态纯工具 | 简单(扩展ComponentCustomProperties) | 全组件可能用到的纯工具类方法/常量 |
| app.provide + inject | 官方推荐(局部扩展更优先) | 极低(可控制作用域到任意层级) | 支持(可响应) | 简单(给InjectionKey加类型) | 有状态、跨多层组件传递、局部区域使用的数据/方法 |
举几个具体的例子帮你做判断:
- ✅ 用app.config.globalProperties:格式化时间、格式化金额、防抖节流的封装函数、跳转路由的快捷方法(不过路由现在已经有useRouter了,不太需要全局挂载)。
- ✅ 用app.provide + inject:用户登录后的个人信息、购物车的商品数量、主题切换、多语言切换(虽然vue-i18n也是用provide/inject实现的)。
- ❌ 别用Vue.prototype.$xxx:不管什么情况,都别用,污染风险太大,多人协作时肯定会出问题。
### 最后再提几个容易踩的坑
1. **getCurrentInstance的异步调用问题**:刚才说过了,一定要在同步代码里解构出proxy,再用到异步回调里。
2. **provide/inject的key重名问题**:最好用Symbol作为key,不要用字符串,避免和其他插件、组件的provide/inject重名。
3. **globalProperties和组件内部变量重名问题**:虽然加了$前缀,但还是要注意,尽量不要在组件内部的data、props、computed里用$开头的变量名。
4. **TypeScript下的声明文件路径问题**:一定要在tsconfig.json的include数组里加上声明文件的路径,否则TypeScript识别不到。
5. **provide的数据如果是响应式的,inject的组件如果修改了数据,所有依赖这个数据的组件都会更新**:这点有利有弊,要注意控制数据的修改权限,最好只在provide的根组件或指定的组件里修改,其他组件只读取或调用提供的方法修改。
现在你应该对Vue3的全局方法(包括类全局的provide/inject)有了全面的了解吧?赶紧去自己的项目里试试吧,如果还有什么问题,欢迎在评论区留言讨论。 版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
code前端网

