学完这篇你就能在 Vue 3 项目里丢掉一长串 import 和 app.component 手写注册,改成在 src/components 下新建 .vue 文件就自动全局可用。
全局组件自动注册的核心思路只有三条:用 import.meta.glob 把目录下的组件模块一次性扫出来、把文件路径转成组件名、循环调用 app.component() 注册。下面是完整做法。
第一步:确定组件目录与命名约定
先约定一个扫不到的目录边界,比如 src/components,之后所有子目录里的 .vue 都会被注册。文件名就是组件名来源,建议统一用 UserCard.vue、base/BaseButton.vue 这种 PascalCase 或 kebab-case。
创建注册入口文件 src/components/index.js:
import { defineAsyncComponent } from 'vue'
// 注意:glob 的匹配模式必须是字面量字符串,不能写成变量
const modules = import.meta.glob('./**/*.vue')
import.meta.glob 返回一个对象,键是相对路径(如 './base/BaseButton.vue'),值是一个返回 Promise 的加载函数——默认是懒加载的。
第二步:路径转组件名
把 ./base/BaseButton.vue 变成 BaseButton,或者带上层级变成 BaseBaseButton 以防重名。加一个转换函数:
function toPascalCase(str) {
return str.replace(/(^\w|[\/_-]\w)/g, (m) =>
m.replace(/[\/_-]/g, '').toUpperCase()
)
}
第三步:写 install 方法并注册
Vue 插件规范是提供 install(app),这样就能用 app.use() 挂载:
export default {
install(app) {
for (const [path, loader] of Object.entries(modules)) {
const name = toPascalCase(
path.replace(/^\.\//, '').replace(/\.vue$/, '')
)
app.component(name, defineAsyncComponent(loader))
}
}
}
如果你想一次性打包进主 chunk(组件不多、希望首屏就有),改成同步导入:
const modules = import.meta.glob('./**/*.vue', { eager: true })
export default {
install(app) {
for (const [path, mod] of Object.entries(modules)) {
const name = toPascalCase(path.replace(/^\.\//, '').replace(/\.vue$/, ''))
app.component(name, mod.default)
}
}
}
eager 模式下 mod 是模块对象,组件本体在 mod.default 上,忘记写 .default 会得到一个空组件。
第四步:在 main.js 挂载
import { createApp } from 'vue'
import App from './App.vue'
import componentsPlugin from './components/index'
const app = createApp(App)
app.use(componentsPlugin)
app.mount('#app')
注意:app.component() 必须在 app.mount() 之前调用。挂载后再注册,模板里解析不到,控制台会报 Failed to resolve component。
第五步:排除不想注册的文件
组件目录里常混着 __tests__、*.stories.vue、index.vue 之类的文件,用负向匹配排除:
const modules = import.meta.glob([
'./**/*.vue',
'!./**/__tests__/**',
'!./**/*.stories.vue',
'!./index.vue',
])
注意:import.meta.glob 的参数由 Vite 在构建时静态分析,写成变量或拼接字符串会直接失效,匹配不到任何文件。这是最常见的失败原因。
第六步:处理命名冲突与体积问题
同名文件(不同子目录下都叫 Header.vue)会产生同一个组件名,后注册的覆盖前面的,且不会报错。要么在转换函数里保留目录层级,要么在开发环境加一层检测:
const registered = new Set()
// 循环内
if (registered.has(name)) console.warn(`[components] 组件名重复:${name}`)
registered.add(name)
另外,懒加载模式(默认)虽然让每个组件独立分包,但如果某个全局组件几乎每页都用,异步加载反而多一次请求,这类高频组件建议保留手动静态导入。
注意:开发环境新增 .vue 文件有时不会立刻被扫到,Vite 的 glob 结果会被缓存,重启 npm run dev 最稳妥;写入新文件后如果模板里仍提示未解析,先重启再排查命名。
小结
- 核心三步:
import.meta.glob 扫目录 → 路径转 PascalCase → 循环 app.component 注册
- 插件形式(
install + app.use)比在 main.js 里写循环更干净,也方便复用
- 默认懒加载返回的是 loader,要包
defineAsyncComponent;{ eager: true } 拿到的模块要取 .default
app.component() 必须在 app.mount() 之前
- glob 参数只能是字面量,用数组 +
! 前缀做排除
- 生产项目加一个重复名检测,避免静默覆盖