Vue 组件基础
组件是 Vue 的核心组织单元。一个 Vue 应用就是一棵组件树:根组件 App.vue 下面挂很多子组件,子组件下面还可以再嵌套子组件。每个组件封装自己的模板、数据、样式,可独立复用。本章讲清组件的基本用法。
1. 为什么需要组件?
想象你把整个网站写在一个文件里——那会是几千行的"巨型代码"。组件化就是把页面拆成独立、可复用的小块:
- 每个组件只关心自己的逻辑,代码内聚、易维护。
- 同一个组件可以复用(一个
Card组件在首页、列表页、详情页都能用)。 - 团队成员可以并行开发不同组件,互不干扰。
- 组件像"乐高积木",组合起来构建复杂页面。
2. 单文件组件(SFC)结构
Vue 的组件以 .vue 文件为单位,叫"单文件组件"(Single-File Component, SFC)。每个文件分三块:template、script、style。
<!-- Counter.vue —— 一个单文件组件 -->
<template>
<div class="counter">
<button @click="dec">-</button>
<span>{{ count }}</span>
<button @click="inc">+</button>
</div>
</template>
<script setup>
import { ref } from "vue";
const count = ref(0);
const inc = () => count.value++;
const dec = () => count.value--;
</script>
<style scoped>
.counter {
display: inline-flex;
gap: 12px;
align-items: center;
}
button {
padding: 6px 14px;
background: #42b883;
color: white;
border: none;
border-radius: 4px;
}
</style>三块的职责:
<template>:模板,管外观结构。一个根节点更清晰(Vue 3 也支持多根节点 Fragment)。<script setup>:逻辑,管数据和方法。这是 Vue 3 推荐写法,简洁、对 TS 友好。<style scoped>:样式。scoped属性让样式只作用于当前组件,不会泄漏。
3. 局部注册(推荐)
在 <script setup> 里 import 一个组件,就能在模板里用——这是局部注册,也是 90% 场景的写法。
<template>
<!-- 用 PascalCase 或 kebab-case 都行,推荐前者 -->
<Counter />
<Counter></Counter>
<counter /> <!-- 也能用,但不推荐 -->
<!-- 同一组件可复用多次,状态互相独立 -->
<Counter />
<Counter />
</template>
<script setup>
import Counter from "./Counter.vue"; // 局部注册:导入即可用
</script>命名规范:
- 组件名用 PascalCase(大驼峰):
BaseButton.vue、UserCard.vue、Counter.vue。在模板里以<Counter />形式使用。 - HTML 标签不能用:别起名
Header.vue、Footer.vue、Nav.vue——会和原生 HTML 元素冲突,模板里识别不出。加前缀避开(AppHeader、TheNav)。 - 基础组件加统一前缀:如
BaseButton、AppInput、IconXxx,便于一眼识别。
4. 全局注册
有些组件(如全局通用的按钮、图标)在到处都用,逐个 import 太烦。可以全局注册:
// main.js
import { createApp } from "vue";
import App from "./App.vue";
import BaseButton from "./components/BaseButton.vue";
const app = createApp(App);
// 全局注册:任何地方都能用,不需要 import
app.component("BaseButton", BaseButton);
app.component("BaseInput", () => import("./components/BaseInput.vue")); // 异步
app.mount("#app");
// 全局注册的代价:打包变大、不可被 tree-shake
// 推荐只在"基础 UI 组件"(按钮/图标)用全局,其他一律局部全局注册的代价:打包体积变大、不能被 tree-shake 优化。所以推荐只对少量基础组件用全局,其他一律局部注册。
5. 组件的复用
同一个组件可以用多次,每个实例都有独立的状态。看这个例子:
<template>
<!-- 用 PascalCase 或 kebab-case 都行,推荐前者 -->
<Counter />
<Counter></Counter>
<counter /> <!-- 也能用,但不推荐 -->
<!-- 同一组件可复用多次,状态互相独立 -->
<Counter />
<Counter />
</template>
<script setup>
import Counter from "./Counter.vue"; // 局部注册:导入即可用
</script>三个 <Counter /> 是三个独立实例,各自的 count 互不影响。这是组件化的核心特性——封装一次,实例化无限次。
6. 动态组件
有时需要根据数据动态切换显示哪个组件(比如标签页切换)。用 <component :is> 实现:
<template>
<!-- 用 component :is 切换组件 -->
<component :is="currentTab" />
<!-- 也可以传字符串(注册过的组件名) -->
<component :is="tabName" />
<!-- 异步组件:Vite 自动代码分割 -->
<component :is="asyncComp" />
</template>
<script setup>
import { ref, shallowRef } from "vue";
import Home from "./Home.vue";
import User from "./User.vue";
import Setting from "./Setting.vue";
const tabs = { Home, User, Setting };
const currentTab = shallowRef(Home);
// 异步组件(按需加载)
const asyncComp = shallowRef(() => import("./Heavy.vue"));
function switchTab(name) {
currentTab.value = tabs[name];
}
</script>
<!-- Vue 还提供了 <KeepAlive> 缓存组件状态 -->
<template>
<KeepAlive>
<component :is="currentTab" />
</KeepAlive>
</template>几个要点:
:is可以是组件对象、注册的组件名字符串、或异步组件函数。- 异步组件
() => import('./Heavy.vue')会被 Vite 自动代码分割,首次切换才加载,提升首屏速度。 <KeepAlive>包裹后,组件切换时不销毁,状态被保留(适合表单填写一半切走的场景)。- 用
shallowRef而不是ref装组件对象,避免不必要的深度响应式开销。
7. 插槽(slot)基础
组件除了接收数据(props),还能接收模板内容——这就是插槽。父组件把一段 HTML 传给子组件,子组件决定它在哪渲染。
<!-- Card.vue:用 slot 接收父组件传入的内容 -->
<template>
<div class="card">
<h3>{{ title }}</h3>
<!-- 默认插槽:父组件传进来的内容显示在这里 -->
<slot></slot>
</div>
</template>
<script setup>
defineProps(["title"]);
</script>
<!-- 父组件使用 -->
<template>
<Card title="用户信息">
<!-- 这段会出现在 <slot> 处 -->
<p>姓名:小明</p>
<p>年龄:20</p>
</Card>
<!-- 没传内容时,显示 slot 的默认值 -->
<Card title="空卡片" />
</template>插槽让组件复用性大幅提升:同一个 Card 组件能装任何内容。还有具名插槽(v-slot:header)和作用域插槽(子组件向父组件回传数据),属于进阶用法,需要时查文档即可。
8. 组件的组织
实际项目里,组件按层级组织:
- 基础组件(components/base/):按钮、输入框、图标。无业务逻辑,纯 UI。
- 业务组件(components/):用户卡片、商品列表。含业务逻辑。
- 页面组件(views/ 或 pages/):首页、用户页、详情页。组装基础+业务组件。
- 布局组件(layouts/):带导航栏和侧边栏的整体框架。
- 根组件(App.vue):最外层,只有
<router-view>和全局组件。
这种分层让代码可维护性大大提升。新手先记住"基础组件 + 页面组件"两层就够用。
9. 递归组件
一个有趣的高级用法:组件可以递归调用自己。常用于树形结构(菜单、组织架构、评论楼中楼):
<!-- TreeNode.vue —— 递归组件:渲染树形结构 -->
<template>
<div class="node">
<span>{{ node.name }}</span>
<!-- 递归调用自己,组件名即文件名 -->
<TreeNode
v-for="child in node.children"
:key="child.id"
:node="child"
/>
</div>
</template>
<script setup>
defineProps(["node"]);
</script>
<!-- 用法 -->
<!--
<Tree :root="{
name: '根',
children: [{ name: '子1', children: [...] }]
}" />
-->递归组件的组件名来自文件名(或 name 选项),所以模板里直接用 <TreeNode> 就能递归。注意要有终止条件(没有 children 时不再递归),否则会栈溢出。
10. 组件的命名规范
业界有两套常见约定(来自 Vue 官方风格指南):
- PascalCase 文件名:
UserCard.vue。和模板里<UserCard />一致,可读性好。Vue 社区主流。 - kebab-case 文件名:
user-card.vue。和 HTML 习惯一致(HTML 标签都小写)。 - 命名要有意义:别起
Comp1.vue、Box.vue这种宽泛名字。看到名字就该知道它做什么。
小结
这一章你掌握了 Vue 组件的基本用法:SFC 三块结构、局部与全局注册、命名规范、动态组件、插槽。下一章我们看父子组件之间如何通信——props 接收数据、emit 触发事件。
← 上一篇 Vue 表单与 v-model
下一篇 Vue Props 与 Emit →