1. 全局挂载这件事,到底在解决什么问题
刚接触 Vue 那会儿,我最常干的一件事就是把一个工具函数从utils.js里import到每一个用到的组件里。三五个组件还好,等到项目里几十个组件都要用同一个格式化时间的方法时,每个文件顶部都挂着一行几乎一样的 import,改个路径还得全局搜索替换,那种感觉就像每次出门都要把家里所有钥匙挨个揣兜里。
全局挂载要解决的就是这个痛点:让一个函数、一个变量、一个对象,在任意组件里都能直接拿到,不用反复引入。它本质上是把某些高频、通用、与业务弱相关的能力,提升到应用级别,让组件只关心自己的逻辑,而不是把精力浪费在“这个函数从哪来”上。
但这里有个前提得先说清楚:全局挂载不是越多越好。我见过有项目把几十个方法全塞到全局,结果新人接手时完全不知道某个$format是从哪冒出来的,调试时断点都找不到源头。所以我的原则一直是——只有真正跨模块复用、且调用频率足够高的东西,才值得挂到全局。像那种只有两三个页面用到的函数,老老实实 import 反而更清晰。
这篇文章我会把 Vue 里全局挂载的几种主流方式掰开揉碎讲一遍,包括它们各自的适用场景、底层原理、踩过的坑,以及在不同 Vue 版本下的差异。不管你是刚上手 Vue 的新手,还是想给团队统一规范的老人,应该都能从中找到能直接抄作业的部分。
2. 先搞清楚 Vue 的实例和原型链机制
在动手挂载之前,有必要花点时间理解 Vue 到底是怎么让全局方法“无处不在”的。很多人照着教程写app.config.globalProperties.$xxx = fn能用,但问他为什么能用就说不清了。理解这层机制,后面遇到问题才能自己排查。
2.1 原型链是全局访问的底层支撑
JavaScript 里每个对象都有一个原型,访问一个属性时如果自身没有,就会顺着原型链往上找。Vue 的组件实例本质上也是对象,它同样遵循这套规则。当我们把方法挂到 Vue 的原型或者应用的全局属性上时,组件实例在查找属性时就能顺着链条找到它。
用生活化的类比:组件实例就像一个员工,他手头没有某份资料时,会去问自己的直属上级(原型),上级没有就继续往上问,直到问到公司层面(全局配置)。全局挂载做的事情,就是把资料放到公司层面,让所有员工都能查到。
在 Vue 2 里,这个“公司层面”就是Vue.prototype;在 Vue 3 里,则变成了app.config.globalProperties。名字变了,但思路一脉相承。
2.2 Vue 2 与 Vue 3 的机制差异
Vue 2 直接操作构造函数原型:
Vue.prototype.$http = axios所有由这个构造函数创建的实例,都能通过原型链访问到$http。这种方式简单粗暴,但有个隐患——它污染了全局的 Vue 构造函数,如果页面里同时存在多个 Vue 应用,它们会共享同一份原型,容易互相干扰。
Vue 3 引入了应用实例的概念,用createApp创建出来的每个应用都是独立的:
const app = createApp(App) app.config.globalProperties.$http = axios这样每个应用有自己的全局属性空间,互不影响。这也是为什么 Vue 3 里不再推荐直接改Vue.prototype,而是走app.config.globalProperties。理解这个差异,能帮你在迁移老项目时少踩坑。
2.3 全局属性与全局变量的边界
这里要区分两个概念:全局属性是挂在应用实例上、通过组件实例访问的;全局变量则更宽泛,可以是模块级别的导出,也可以是挂到window上的东西。
我个人的划分标准是这样的:
| 类型 | 访问方式 | 适用场景 | 风险 |
|---|---|---|---|
| 全局属性 | this.$xxx | 组件内高频调用的工具方法 | 命名冲突、来源不清晰 |
| 模块导出 | import { xxx } | 纯函数、常量、配置 | 需要显式引入 |
| 全局变量 | window.xxx | 跨框架、跨脚本共享 | 污染全局、难以追踪 |
大部分情况下,我优先推荐模块导出,其次是全局属性,最后才考虑挂 window。原因很简单:前两者有明确的来源和类型提示,IDE 能帮你跳转,而 window 上的东西基本靠记忆。
3. 全局函数的几种挂载方式逐一拆解
这一节是重头戏。我把实际项目里用过的几种方式都列出来,每种都配上代码、适用场景和我踩过的坑。
3.1 方式一:app.config.globalProperties(Vue 3 首选)
这是 Vue 3 官方推荐的全局挂载方式。写法很直接:
// main.js import { createApp } from 'vue' import App from './App.vue' const app = createApp(App) app.config.globalProperties.$formatDate = (timestamp) => { const date = new Date(timestamp) return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}` } app.mount('#app')组件里这样用:
export default { mounted() { console.log(this.$formatDate(Date.now())) } }在组合式 API 的setup里,this不可用,需要用getCurrentInstance:
import { getCurrentInstance } from 'vue' export default { setup() { const { proxy } = getCurrentInstance() console.log(proxy.$formatDate(Date.now())) } }注意:
getCurrentInstance官方明确说它是给库作者用的高级 API,业务代码里滥用会让逻辑变得难以追踪。我的建议是,如果项目以组合式 API 为主,全局函数尽量走模块导入,而不是挂到 globalProperties 上。
这种方式的好处是类型清晰、来源明确,配合 TypeScript 还能做类型声明。坏处是每个挂载的属性都要加$前缀(这是约定,避免和组件自身属性冲突),写起来略啰嗦。
3.2 方式二:Vue.prototype(Vue 2 经典做法)
Vue 2 项目里最常见的就是这个:
// main.js import Vue from 'vue' import axios from 'axios' Vue.prototype.$http = axios Vue.prototype.$formatDate = (timestamp) => { /* ... */ }组件里直接this.$http.get(...)就能用。这套写法在 Vue 2 生态里根深蒂固,很多老项目、老教程都是这么教的。
但要注意几个坑。第一,Vue.prototype上的属性名如果和组件自身的 data、methods 重名,组件自身的会覆盖原型上的,容易出诡异 bug。第二,如果项目里用了 SSR,服务端和客户端共享同一个 Vue 构造函数,原型上的状态可能被污染。第三,多个 Vue 应用共存时会互相影响。
所以我的经验是:Vue 2 项目里用Vue.prototype没问题,但一定要加$前缀,并且只挂真正全局的东西。
3.3 方式三:provide / inject 实现依赖注入
provide和inject是 Vue 提供的依赖注入机制,它不算严格意义上的“全局”,但能实现跨层级传递,很多时候比全局挂载更合适。
// 根组件 export default { provide() { return { formatDate: this.formatDate } }, methods: { formatDate(timestamp) { /* ... */ } } }后代组件:
export default { inject: ['formatDate'], mounted() { console.log(this.formatDate(Date.now())) } }这种方式的好处是依赖关系显式——组件声明了自己需要什么,而不是凭空从全局拿。坏处是每个需要的组件都要写 inject,而且 provide 的值默认不是响应式的(Vue 3 里可以用ref或reactive包裹解决)。
我一般在做组件库或者需要严格控制依赖的场景下用 provide/inject,业务项目里如果只是图省事,还是 globalProperties 更直接。
3.4 方式四:挂到 window 或 globalThis
最原始的方式,直接挂到全局对象上:
window.$formatDate = (timestamp) => { /* ... */ }组件里window.$formatDate(...)或者直接$formatDate(...)都能用。
这种方式我基本只在两种情况下用:一是需要和外部脚本(比如某个第三方 SDK)共享方法,二是做微前端时主子应用之间通信。其他情况一律不推荐,因为它完全绕过了 Vue 的机制,类型提示没有、来源追踪没有、命名冲突风险极高。
3.5 方式五:插件形式统一注册
如果全局函数比较多,一个个挂太散,可以封装成 Vue 插件:
// plugins/global.js export default { install(app) { app.config.globalProperties.$formatDate = (timestamp) => { /* ... */ } app.config.globalProperties.$debounce = (fn, delay) => { /* ... */ } app.config.globalProperties.$deepClone = (obj) => { /* ... */ } } }// main.js import GlobalPlugin from './plugins/global' app.use(GlobalPlugin)这种方式把全局挂载集中管理,好处是清晰、可维护,坏处是插件内部的东西对使用者来说还是“黑盒”。我一般会配合 TypeScript 的模块增强,给 globalProperties 加上类型声明,这样 IDE 里能自动补全。
4. 全局变量的挂载与状态共享
函数讲完了,再说变量。全局变量和全局函数在挂载方式上大同小异,但变量涉及响应式,情况会复杂一些。
4.1 普通全局变量:模块导出最省心
如果只是常量、配置项这类不需要响应式的东西,最推荐的方式是模块导出:
// config/constants.js export const APP_NAME = 'MyApp' export const API_TIMEOUT = 10000 export const STATUS_MAP = { 1: '待处理', 2: '处理中', 3: '已完成' }用的时候import { APP_NAME } from '@/config/constants'。这种方式有类型提示、有来源、可 tree-shaking,是纯变量的最佳实践。
4.2 响应式全局变量:用 reactive 或 ref
如果全局变量需要响应式,比如用户信息、主题配置,可以这样:
// store/global.js import { reactive } from 'vue' export const globalState = reactive({ userInfo: null, theme: 'light', locale: 'zh-CN' })组件里:
import { globalState } from '@/store/global' export default { setup() { return { globalState } } }模板里globalState.theme变化会自动更新。这种方式比挂到 globalProperties 上更好,因为响应式对象在模块级别是单例的,所有组件共享同一份,且来源清晰。
4.3 挂到 globalProperties 的响应式变量
如果非要挂到 globalProperties,要注意响应式的问题:
app.config.globalProperties.$globalState = reactive({ theme: 'light' })组件里this.$globalState.theme是响应式的,因为 reactive 返回的是 Proxy。但如果挂的是普通对象,就不会有响应式效果。这一点很多人会踩坑——以为挂上去就自动响应了,其实不是。
4.4 全局变量与状态管理库的取舍
说到全局状态,绕不开状态管理库。我的判断标准是:
- 如果只是几个简单的全局变量,用模块导出 + reactive 就够了,没必要上状态管理库。
- 如果状态逻辑复杂、涉及多个模块的读写、需要调试工具追踪,那状态管理库更合适。
- 全局挂载适合“只读”或“极少修改”的配置类变量,不适合频繁变更的业务状态。
我见过有项目把用户登录态、购物车数据全挂到 globalProperties 上,结果状态变更完全失控,最后不得不重构。这个教训值得记一下。
5. 实操:从零搭建一套全局挂载方案
光说理论不够,这一节我带你走一遍完整的搭建流程,包括目录结构、代码、类型声明和测试。
5.1 目录结构设计
我习惯这样组织:
src/ ├── plugins/ │ ├── global-functions.js // 全局函数插件 │ └── index.js // 插件统一出口 ├── utils/ │ ├── format.js // 格式化相关 │ └── common.js // 通用工具 ├── config/ │ └── constants.js // 常量配置 └── main.js把全局函数的具体实现放在utils里,插件只负责挂载,这样函数本身还能被单独 import 使用,不依赖 Vue 实例。
5.2 编写全局函数插件
// utils/format.js export function formatDate(timestamp, separator = '-') { if (!timestamp) return '' const date = new Date(timestamp) const y = date.getFullYear() const m = String(date.getMonth() + 1).padStart(2, '0') const d = String(date.getDate()).padStart(2, '0') return [y, m, d].join(separator) } export function formatMoney(amount, symbol = '¥') { if (amount == null) return '' return symbol + Number(amount).toFixed(2) }// plugins/global-functions.js import { formatDate, formatMoney } from '@/utils/format' export default { install(app) { app.config.globalProperties.$formatDate = formatDate app.config.globalProperties.$formatMoney = formatMoney } }// main.js import { createApp } from 'vue' import App from './App.vue' import GlobalFunctions from './plugins/global-functions' const app = createApp(App) app.use(GlobalFunctions) app.mount('#app')5.3 TypeScript 类型声明补全
用 TS 的话,globalProperties 上的属性默认没有类型,需要做模块增强:
// types/global.d.ts import { formatDate, formatMoney } from '@/utils/format' declare module 'vue' { interface ComponentCustomProperties { $formatDate: typeof formatDate $formatMoney: typeof formatMoney } } export {}加上这段之后,this.$formatDate在组件里就有类型提示和自动补全了。这个细节很多人忽略,但用起来体验差别很大。
5.4 在组合式 API 中的调用封装
组合式 API 里用getCurrentInstance拿 proxy 有点绕,我一般封装一个 hook:
// composables/useGlobal.js import { getCurrentInstance } from 'vue' export function useGlobal() { const instance = getCurrentInstance() if (!instance) { throw new Error('useGlobal 必须在 setup 内调用') } return instance.proxy }用的时候:
import { useGlobal } from '@/composables/useGlobal' export default { setup() { const global = useGlobal() console.log(global.$formatDate(Date.now())) } }提示:这个 hook 只是让写法好看一点,本质上还是依赖 getCurrentInstance。如果项目以组合式 API 为主,我更建议直接 import 函数,而不是绕这一圈。
5.5 验证挂载是否生效
写完别急着跑业务,先写个最小验证。在 App.vue 里:
export default { mounted() { console.log('formatDate:', this.$formatDate(Date.now())) console.log('formatMoney:', this.$formatMoney(1234.5)) } }控制台能正常输出就说明挂载成功。如果报this.$formatDate is not a function,八成是插件没注册或者注册顺序有问题。
6. 常见问题与排查技巧实录
这一节是我这些年踩坑攒下来的经验,基本都是文档里不会写的。
6.1 挂载了但组件里拿不到
最常见的原因有三个。第一,插件注册在app.mount之后,这时候应用已经挂载,再注册就晚了。第二,在setup里用了this,组合式 API 的 setup 里 this 是 undefined。第三,属性名拼写错误,比如挂的是$formatDate,用的却是$formatdate,大小写敏感。
排查顺序:先确认注册时机,再确认调用位置,最后检查拼写。
6.2 命名冲突导致覆盖
如果全局属性名和组件自身的 data、computed、methods 重名,组件自身的会优先。比如组件里有个formatDate方法,那this.formatDate拿到的是组件自己的,this.$formatDate才是全局的。这也是为什么全局属性一定要加$前缀——就是为了避开这类冲突。
6.3 响应式丢失问题
挂到 globalProperties 上的普通对象不会自动响应式。如果需要一个响应式的全局变量,要么用reactive包裹,要么干脆走模块导出。我见过有人挂了个普通对象,然后纳闷为什么改了值视图不更新,折腾半天才发现是响应式的问题。
6.4 SSR 场景下的注意事项
服务端渲染时,全局属性是在服务端和客户端分别创建的。如果全局属性里存了用户相关的状态,要确保每个请求都有独立的实例,否则会出现状态串号。Vue 3 的createApp每次调用都返回新实例,天然支持这一点,但前提是你别把状态挂到模块级别的变量上。
6.5 常见问题速查表
| 现象 | 可能原因 | 解决方向 |
|---|---|---|
| this.$xxx is not a function | 插件未注册或注册时机晚 | 检查 app.use 和 mount 顺序 |
| setup 里拿不到 | 用了 this | 改用 getCurrentInstance 或直接 import |
| 值改了视图不更新 | 挂的是普通对象 | 用 reactive 包裹或改模块导出 |
| 属性被覆盖 | 与组件自身属性重名 | 全局属性统一加 $ 前缀 |
| 类型报错 | 缺少类型声明 | 做 ComponentCustomProperties 模块增强 |
| 多应用互相干扰 | 用了 Vue.prototype | 改用 app.config.globalProperties |
6.6 我个人的几条硬性建议
第一,全局属性只挂真正全局的东西,判断标准是“超过一半的组件可能用到”。第二,命名统一加$前缀,形成团队约定。第三,能用模块导出解决的,优先模块导出,全局挂载是备选不是首选。第四,一定要配 TypeScript 类型声明,否则半年后你自己都不记得挂了什么。第五,插件文件里加注释,写清楚每个挂载项的用途和参数。
7. 不同场景下的选型建议
最后聊聊怎么根据项目情况选方案。这块没有标准答案,但有一些经验性的判断。
7.1 小型项目:模块导出为主
小项目组件少、依赖简单,直接import工具函数最省事。全局挂载反而增加了理解成本,新人进来还得先搞清楚$xxx是从哪来的。我做过几个页面级别的项目,全程没用全局挂载,代码一样清爽。
7.2 中大型项目:插件化统一管理
项目一大,工具函数多了,就需要统一管理。这时候用插件形式把全局函数集中注册,配合类型声明,既方便调用又便于维护。关键是团队要有约定,什么能挂什么不能挂,避免全局空间被滥用。
7.3 组件库开发:provide/inject 更合适
做组件库时,全局挂载是大忌,因为组件库不应该假设使用者的应用环境。这时候 provide/inject 是更好的选择,让使用者自己决定注入什么,组件库只声明依赖。
7.4 微前端场景:谨慎使用全局挂载
微前端里多个子应用可能共存,全局挂载容易互相污染。这时候要么用沙箱隔离,要么干脆每个子应用独立管理自己的工具函数,不往全局挂。这块坑很深,我一般建议微前端场景下全局挂载能不用就不用。
7.5 选型对照表
| 场景 | 推荐方式 | 理由 |
|---|---|---|
| 小型项目 | 模块导出 | 简单直接,无额外心智负担 |
| 中大型项目 | 插件 + globalProperties | 统一管理,便于维护 |
| 组件库 | provide/inject | 不污染使用者环境 |
| 组合式 API 为主 | 模块导出 | 避免 getCurrentInstance 的绕路 |
| 需要响应式全局状态 | reactive 模块导出 | 响应式且来源清晰 |
| 微前端 | 各应用独立管理 | 避免全局污染 |
选型的核心就一句话:全局挂载是手段不是目的,能让代码更清晰、更好维护的方案就是好方案。别为了用某个 API 而用,也别因为教程都这么写就照搬。多想想半年后接手的人能不能一眼看懂,这个判断标准比什么都管用。