1037 字
5 分钟
Vue 3 的全局组件、局部组件与递归组件

三种组件用法#

Vue 3 里常见三种用法。全局组件注册一次,整个应用都能用;局部组件只在导入它的文件里能用;递归组件在自己的模板里调用自己,用来渲染树形结构。

用法注册方式适用场景
全局组件app.component(name, component)Button、Icon 这类到处都在用的基础组件
局部组件import 后直接在模板里用绝大多数业务组件
递归组件组件内引用自身树、菜单、级联选择器

官方文档对应两节:组件注册和 name 选项。

全局组件#

全局注册的组件在整个应用的任何模板里都能直接使用,不用再次导入。注册方法是 app.component(name, component)。

注册与使用#

先写一个 Card 组件:

Card.vue
<template>
<div class="card">
<div class="card-header">
<div>标题</div>
<div>副标题</div>
</div>
<div v-if="content" class="card-content">
{{ content }}
</div>
</div>
</template>
<script setup lang="ts">
type Props = {
content: string
}
defineProps<Props>()
</script>

在 main.ts 里注册,然后挂载应用:

main.ts
import { createApp } from 'vue'
import App from './App.vue'
import Card from './components/Card/index.vue'
const app = createApp(App)
app.component('Card', Card) // 全局注册
app.mount('#app')

注册之后,任何组件的模板里都能直接写 <Card />:

<template>
<Card content="我是全局卡片内容" />
</template>

注册顺序#

顺序是 createApp → component → mount,注册要写在 app.mount() 之前。

批量注册#

图标这类高频组件可以遍历注册。下面以 Element Plus 的图标为例:

main.ts
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
const app = createApp(App)
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}

全局注册的代价#

全局注册的组件不参与 Tree-shaking。即使某个页面没有用到,它依然会出现在打包结果里。

依赖关系也不清晰:看到模板里的 <Card />,你无法从当前文件判断它的实现来自哪里。组件多起来以后,这类定位很费时间。

局部组件#

局部注册的组件只能在使用它的父组件里使用。在 <script setup> 中,import 之后就能直接在模板里写,不用再注册。

Layout.vue
<template>
<div class="wraps">
<LayoutHeader />
<LayoutMenu />
<LayoutMain />
</div>
</template>
<script setup lang="ts">
import LayoutHeader from './Header.vue'
import LayoutMenu from './Menu.vue'
import LayoutMain from './Content.vue'
</script>

作用域是隔离的:A 组件导入了 B,只有 A 能用 B。C 想用 B,得自己再 import 一次。

局部注册的依赖关系清晰,没被用到的组件不会进入打包结果,所以它是默认选择。

递归组件#

递归能停下来,靠模板里的终止条件:没有子节点时不再渲染下一层。组件名不是必须的,单文件组件可以直接用文件名引用自己。

数据与类型#

树形数据用递归类型描述,children 指向同一种类型:

type TreeList = {
name: string
children?: TreeList[]
}
const data = reactive<TreeList[]>([
{
name: 'no.1',
children: [{ name: 'no.1-1', children: [{ name: 'no.1-1-1' }] }]
},
{ name: 'no.2', children: [{ name: 'no.2-1' }] },
{ name: 'no.3' }
])

父组件把这份数据交给递归组件:

App.vue
<template>
<TreeItem :data="data" />
</template>

组件名称#

单文件组件可以直接用文件名引用自己,不必声明 name。从 3.2.34 起,<script setup> 会按文件名推导组件名,TreeItem.vue 推导出来的名字就是 TreeItem。

需要显式声明 name 的场景是 <KeepAlive> 的 include 和 exclude:它们按组件名匹配要缓存的组件,没有推导出名字时就得自己写。Vue 3.3 起用 defineOptions 设置:

TreeItem.vue
<script setup lang="ts">
defineOptions({ name: 'TreeItem' })
</script>

3.3 之前的项目用两个 <script> 标签,一个只用来导出 name,另一个写 <script setup>:

TreeItem.vue
<script lang="ts">
export default {
name: 'TreeItem'
}
</script>
<script setup lang="ts">
interface Tree {
name: string
children?: Tree[]
}
defineProps<{ data?: Tree[] }>()
</script>

用宏插件设置 name 的做法,3.3 起有了 defineOptions,不再必要。

递归模板与终止条件#

TreeItem.vue
<template>
<div class="tree" style="margin-left: 10px">
<div v-for="(item, index) in data" :key="index">
<div @click="clickItem(item)">{{ item.name }}</div>
<TreeItem v-if="item?.children?.length" :data="item.children" />
</div>
</div>
</template>

v-if="item?.children?.length" 就是终止条件。没有子节点时不渲染下一层,递归到此结束。

第 11 篇,共 19 篇本文属于系列

Vue 3 的全局组件、局部组件与递归组件
https://www.choria.top/posts/vue3-components/
作者
乔瑞雅·克林威尔
发布于
2026-04-14
许可协议
CC BY-NC-SA 4.0

分享文章

生成精美分享图或复制链接,与更多人分享本文。

继续阅读

沿着主题读

基于共同的标签与分类

换条路线

从其他文章中稳定抽取

最后更新于 ,距今已过 208 天

部分内容可能已过时