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

vue3全局注册组件会不会有什么坑?怎么用才高效规范?

terry 42分钟前 阅读数 9 #Vue

为什么很多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会注册,pinia好像不会直接注册组件,但有些第三方插件比如vue-i18n可能会注册标签组件,这些都是插件开发者为了方便用户使用而设计的全局组件,我们不用管,直接用就行。

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组件注册的最佳实践,大家可以根据自己的项目规模和需求选择合适的方式:

  1. 纯UI展示、无复杂业务逻辑、全项目100%使用的高频基础组件:用批量自动注册的方式全局注册,放在src/components/global目录下。
  2. 第三方UI库的高频组件:用按需全局注册的方式,放在src/plugins目录下。
  3. 业务组件(哪怕是多个页面使用,但不是100%使用的):尽量用按需引入的方式,不要全局注册,除非是非常非常高频的,比如外卖平台的“wm-address-selector”地址选择器,可能80%的页面都会用到,这种可以考虑全局注册,但最好还是加个独特的项目前缀。
  4. 第三方UI库的低频组件:直接在使用的页面顶部按需引入,不要全局注册。
  5. 自定义指令:用app.directive全局注册。
  6. 原型链上的挂载:尽量用provide/inject或者组合式函数替代,除非是从Vue2迁移过来的老项目。

遵循这些最佳实践,你的Vue3项目既不会有太大的包体积,也不会有命名冲突的问题,可维护性也会大大提高,不管是新手还是老手,接手起来都会非常顺手。

版权声明

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

热门