vue3全局注册组件会不会有什么坑?怎么用才高效规范?
为什么很多Vue项目一开始都爱用全局注册组件?
刚接触Vue2转3或者直接学Vue3的新手,肯定会觉得全局注册太香了:只要在main.js或者main.ts里import一下、调用app.component,整个项目的任何组件里直接套标签就行,不用每次都写一堆import引入Button、Dialog这些高频组件,新手做demo、练手项目的时候,确实用全局注册能省不少复制粘贴的时间,页面里插个按钮、加个轮播图随手就来。
再看大厂早期的Vue开源组件库引入方式,比如Ant Design Vue2的快速入门,也是推荐把常用的Button、Input、Icon等10来个组件全局注册,图的就是开发效率,新手跟着官方教程或者前辈的demo走,自然就养成了先全库注册或者高频全注册的习惯。
不过很多人做的项目稍微大一点,比如页面超过20个、组件树有三四层的时候,就会发现全局注册的“香”慢慢变味了,甚至会遇到一些莫名其妙的问题,接下来就好好聊聊这些坑。
全局注册Vue3组件的3个核心大坑,你踩过几个?
包体积飙升是第一个看得见的硬伤
这应该是最直观的问题了,比如你用Element Plus做电商后台,一开始觉得反正每个页面都有表格、分页、按钮,直接全库全局注册得了,结果打包后看dist文件夹里的js文件,可能比按需引入大了300%甚至更多。
为啥会这样?Vue3虽然有Tree Shaking,但Tree Shaking只对ES6模块(也就是import/export语法,而且要确保没有副作用)生效,全库全局注册的话,你是直接import ElementPlus from 'element-plus',然后调用app.use(ElementPlus),这种引入方式相当于把Element Plus的所有组件、插件、样式文件都强制打包进去了,不管你实际用没用到Cascader Panel这种很少见的组件,或者没用到Message这种手动触发的插件,它们都会躺在你的最终包里。
要是你的项目部署在国内的CDN还好,压缩后的大文件加载速度可能勉强能接受,但如果是面向海外用户或者小带宽场景,首屏加载时间可能直接超过3秒,根据电商网站的统计数据,首屏加载超过3秒,用户流失率会高达50%以上,这对产品来说是致命的。
组件命名冲突是第二个容易踩的隐性坑
Vue2里组件命名冲突可能还只是覆盖前一个组件的问题,Vue3里虽然稍微做了一点优化,但冲突还是会出现,而且排查起来特别麻烦。
举个真实的例子,我之前帮朋友改一个二手交易小程序的Web版,他们用了Vant4做UI库,还自己封装了一个全局的“SubmitButton”组件,专门用来提交订单、发布商品,一开始没问题,直到他们做了一个“商品发布页带分类筛选”的功能,引入了Vant4的Field组件,Field组件内部有个默认slot叫“submit”?不对,不是slot,是Field组件在某些版本的全局注册时,底层注册了一个叫“van-submit-button”的内部辅助组件?哦不,朋友当时封装的全局SubmitButton,可能是因为没加前缀,或者加了自己的项目前缀但和某个依赖里的组件前缀撞了——他们项目前缀是“my”,结果有个小众的图表库依赖里,也有个叫“my-submit”的内部组件被不小心全局带出来了?
反正当时排查了快两个小时:发布页的提交按钮点了没反应,打开Vue DevTools看,当前组件树里的SubmitButton根本不是他们自己封装的,而是那个图表库带过来的内部组件,里面没有绑定点击事件,也没有他们写的样式,后来把自己封装的全局组件前缀改成更独特的“ershou-submit-button”,问题才解决。
这里要注意,Vue3的全局组件命名规则和Vue2差不多,推荐用kebab-case(短横线命名)或者PascalCase(大驼峰命名),而且最好加3-5个字符的独特项目前缀,比如你做外卖平台可以叫“wm-button”,做在线教育可以叫“jy-answer-card”,这样能大大降低和第三方UI库、依赖库内部组件的命名冲突概率。
项目可维护性下降是第三个长期积累的隐形炸弹
包体积可以通过按需引入解决,命名冲突可以通过加前缀解决,但可维护性下降这个问题,只有等项目人员流动、或者你自己隔了两三个月再回来改代码的时候,才能深刻体会到。
比如你在一个100+页面的Vue3项目里,全局注册了20个自己封装的业务组件:“wm-order-card”“wm-address-selector”“wm-coupon-picker”……过了半年,新来的前端同事接手修改订单详情页,他看到页面里有个“wm-order-card”标签,根本不知道这个组件是自己项目封装的还是第三方UI库的,也不知道这个组件的props、events、slots有哪些,只能去main.js或者main.ts里翻全局注册的代码,然后再去找对应的.vue文件,vue文件还放在很深的目录里,找起来特别费劲。
如果是按需引入的业务组件,同事只要看组件顶部的import语句,就能直接跳转到对应的.vue文件,而且如果某个页面不用某个组件了,直接删掉import和标签就行,不用担心影响其他页面,全局注册的业务组件就不一样了,你根本不知道哪个页面用了它,哪怕99个页面都不用了,只有1个页面在用,你也不敢随便删,生怕删了之后某个角落的页面报错。
还有一个可维护性的问题是组件的依赖关系不清晰,全局注册的业务组件如果依赖了其他全局注册的组件,你在.vue文件里根本看不到依赖链,只能靠记忆或者全局搜索,这对大型项目的维护来说是非常不利的。
那Vue3里什么时候用全局注册才合适?
说了这么多全局注册的坑,是不是全局注册就不能用了?当然不是,存在即合理,全局注册在某些场景下还是非常好用的,而且是必须的。
纯UI展示、无复杂业务逻辑、全项目100%使用的高频基础组件
比如你自己封装的“wm-icon”图标组件,不管是首页、列表页、详情页、个人中心,几乎每个页面都会用到,而且这个组件只是根据name属性渲染对应的SVG图标,没有复杂的props、events、slots,也没有依赖其他业务组件,这种组件全局注册就非常合适,不用每个页面都写一次import WmIcon from '@/components/WmIcon.vue'。
再比如Ant Design Vue、Element Plus、Vant4这些第三方UI库的Icon组件,很多官方教程也推荐全局注册,图的就是开发效率,而且Icon组件一般体积都很小,哪怕全库全局注册Icon,对最终包体积的影响也可以忽略不计。
自定义指令和原型链上的挂载(虽然不是组件,但和全局注册逻辑类似)
哦,这里稍微跑题一点,但也是Vue3全局操作的一部分,新手容易搞混,Vue3里自定义指令可以通过app.directive全局注册,原型链上的挂载可以通过app.config.globalProperties实现,比如你可以把axios挂载到app.config.globalProperties.$axios上,然后在组件里通过this.$axios调用(不过Vue3推荐用provide/inject或者组合式函数来替代this.$xxx的方式,除非是从Vue2迁移过来的老项目)。
自定义指令比如“v-copy”一键复制、“v-debounce”防抖、“v-lazy”图片懒加载,这些也是全项目100%会用到的,全局注册非常合适。
插件里注册的内部组件
比如你用了vue-router或者pinia,它们内部会注册一些全局组件吗?vue-router会注册
Vue3全局注册组件的2种高效规范写法
即使你要用全局注册组件,也不要像新手那样直接在main.js里写一堆import和app.component,太乱了,而且可维护性差,接下来给大家分享两种高效规范的写法。
第一种:批量自动注册全局基础组件
这种写法适合自己封装的纯UI展示、无复杂业务逻辑的高频基础组件,比如图标组件、按钮组件、输入框组件、分割线组件等,你可以把这些组件统一放在src/components/global目录下,然后新建一个index.js或者index.ts文件,批量自动注册这些组件。
批量自动注册的核心逻辑是用Webpack或者Vite的require.context(Webpack)或者import.meta.glob(Vite)来读取global目录下的所有.vue文件,然后循环遍历注册。
举个Vite的例子,先看目录结构:
src/
components/
global/
WmIcon.vue
WmButton.vue
WmInput.vue
WmDivider.vue
index.ts
main.ts
然后在src/components/global/index.ts里写批量自动注册的逻辑:
import type { App, Component } from 'vue'
// 读取global目录下的所有.vue文件,包括子目录(如果有的话,这里我们暂时不建子目录)
const components: Record<string, Component> = import.meta.glob(
'./**/*.vue',
{ eager: true } // 直接同步导入所有组件,不用动态导入
)
export default {
install(app: App) {
// 循环遍历所有组件
for (const path in components) {
// 获取组件实例
const component = components[path].default || components[path]
// 获取组件的文件名作为组件名,/WmIcon.vue -> WmIcon
const componentName = component.name || path.split('/').pop()?.replace('.vue', '') || ''
// 全局注册组件,同时支持kebab-case和PascalCase
app.component(componentName, component)
// 如果组件名是大驼峰,再注册一个短横线的别名
if (/^[A-Z]/.test(componentName)) {
const kebabCaseName = componentName.replace(/([A-Z])/g, '-$1').toLowerCase().slice(1)
app.component(kebabCaseName, component)
}
}
}
}
最后在main.ts里引入并安装这个插件:
import { createApp } from 'vue'
import App from './App.vue'
import globalComponents from '@/components/global'
const app = createApp(App)
// 安装全局基础组件插件
app.use(globalComponents)
app.mount('#app')
这种写法的好处是:以后你要新增一个全局基础组件,只要把它放到src/components/global目录下,并且给组件的name属性赋值(比如WmAvatar),或者直接用默认的文件名,它就会自动注册到全局,不用再去修改main.ts文件,可维护性大大提高。
而且这里我加了一个小技巧:如果组件名是大驼峰,会自动注册一个短横线的别名,比如WmIcon会同时注册为
第二种:按需全局注册第三方UI库的高频组件
这种写法适合第三方UI库的高频组件,比如Element Plus的Button、Input、Table、Pagination、Message、Notification等,既不会像全库全局注册那样导致包体积飙升,也不会像完全按需引入那样每个页面都写一堆import。
举个Element Plus的例子,目录结构可以这样:
src/
plugins/
element-plus.ts
main.ts
然后在src/plugins/element-plus.ts里写按需全局注册的逻辑:
import type { App } from 'vue'
import {
ElButton,
ElInput,
ElTable,
ElTableColumn,
ElPagination,
ElMessage,
ElNotification,
ElMessageBox
} from 'element-plus'
// 引入Element Plus的基础样式(必须引入,否则组件没有样式)
import 'element-plus/dist/index.css'
// 如果用ElMessage、ElNotification、ElMessageBox,还要单独引入它们的样式吗?Element Plus最新版本好像不用了,基础样式里已经包含了,不过旧版本可能需要,大家可以自己试一下
// 把Message、Notification、MessageBox挂载到app.config.globalProperties上,方便在组合式函数或者setup语法糖里通过getCurrentInstance获取(不过更推荐直接从element-plus里import)
export default {
install(app: App) {
// 按需全局注册高频UI组件
app.use(ElButton)
app.use(ElInput)
app.use(ElTable)
app.use(ElTableColumn)
app.use(ElPagination)
// 挂载到原型链上(可选)
app.config.globalProperties.$message = ElMessage
app.config.globalProperties.$notify = ElNotification
app.config.globalProperties.$alert = ElMessageBox.alert
app.config.globalProperties.$confirm = ElMessageBox.confirm
app.config.globalProperties.$prompt = ElMessageBox.prompt
}
}
最后在main.ts里引入并安装这个插件:
import { createApp } from 'vue'
import App from './App.vue'
import globalComponents from '@/components/global'
import elementPlus from '@/plugins/element-plus'
const app = createApp(App)
// 安装全局基础组件插件
app.use(globalComponents)
// 安装Element Plus高频组件插件
app.use(elementPlus)
app.mount('#app')
这种写法的好处是:你可以根据项目的实际需求,只全局注册高频使用的第三方UI组件,不会引入没用的组件,包体积会比全库全局注册小很多,而且开发效率也很高,不用每个页面都写import ElButton from 'element-plus'。
如果后面某个页面需要用到Element Plus的Cascader Panel这种很少见的组件,你直接在那个页面的顶部按需引入就行,不用修改全局的插件文件,非常灵活。
写在最后:Vue3组件注册的最佳实践
最后总结一下Vue3组件注册的最佳实践,大家可以根据自己的项目规模和需求选择合适的方式:
- 纯UI展示、无复杂业务逻辑、全项目100%使用的高频基础组件:用批量自动注册的方式全局注册,放在src/components/global目录下。
- 第三方UI库的高频组件:用按需全局注册的方式,放在src/plugins目录下。
- 业务组件(哪怕是多个页面使用,但不是100%使用的):尽量用按需引入的方式,不要全局注册,除非是非常非常高频的,比如外卖平台的“wm-address-selector”地址选择器,可能80%的页面都会用到,这种可以考虑全局注册,但最好还是加个独特的项目前缀。
- 第三方UI库的低频组件:直接在使用的页面顶部按需引入,不要全局注册。
- 自定义指令:用app.directive全局注册。
- 原型链上的挂载:尽量用provide/inject或者组合式函数替代,除非是从Vue2迁移过来的老项目。
遵循这些最佳实践,你的Vue3项目既不会有太大的包体积,也不会有命名冲突的问题,可维护性也会大大提高,不管是新手还是老手,接手起来都会非常顺手。
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
code前端网


