侧边栏壁纸
博主头像
洋洋的技术笔记

行动起来,活在当下

  • 累计撰写 14 篇文章
  • 累计创建 2 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录
VUE

组件基础与注册方式

洋洋的技术笔记
2026-11-06 / 0 评论 / 0 点赞 / 3 阅读 / 0 字

组件基础与注册方式

组件不是把模板剪成几个文件,而是把页面拆成职责清晰、可以独立理解和复用的界面单元。掌握组件的定义、注册与组合,是进入 Vue 组件化开发的第一步。

上一篇文章中,我们通过 Class 与 Style 绑定让界面能够准确表达状态。随着页面功能增加,模板、逻辑和样式也会不断膨胀。本文将页面拆成多个小组件,学习单文件组件的基本结构、局部注册与全局注册的区别,并通过一个学习看板理解组件组合方式。

写作约定:本系列以 Vue 3 <script setup> 为主要写法,示例默认使用 Vue 3.2+ 和 Vite 创建的项目。本文重点是组件定义、注册和渲染,不涉及 Props、自定义事件等通信细节。


一、为什么需要组件

假设一个学习看板同时包含页面标题、学习进度、任务列表和底部提示。如果全部写在一个文件中,模板会越来越长:

<template>
  <main>
    <header>
      <p>Vue.js 从入门到精通</p>
      <h1>我的学习看板</h1>
    </header>

    <section>
      <h2>本周进度</h2>
      <progress value="7" max="10"></progress>
    </section>

    <section>
      <h2>学习任务</h2>
      <ul>
        <li>理解模板语法</li>
        <li>掌握事件处理</li>
        <li>完成组件拆分</li>
      </ul>
    </section>
  </main>
</template>

这个页面目前还能阅读,但继续加入筛选、样式和交互后,不同职责会混在一起。组件化可以把页面拆成以下结构:

App
├── PageHeader
├── ProgressPanel
├── TaskList
└── PageFooter

每个组件负责一块相对完整的界面。父组件负责组合,子组件负责自己的展示和局部行为。

组件化带来的主要收益是:

  • 职责清晰:看到一个文件名,就能大致判断它负责的界面。

  • 复用方便:相同按钮、卡片和列表项不必重复编写。

  • 修改集中:组件内部结构变化时,减少对页面其他部分的影响。

  • 协作容易:不同组件可以分别开发和检查。

但组件并不是拆得越细越好。一个只有一行文字、没有复用价值也没有独立职责的元素,通常不必单独建成组件。


二、认识单文件组件

Vue 项目中最常见的组件形式是单文件组件,也就是 .vue 文件。它把模板、逻辑和样式组织在同一个文件中:

<!-- PageHeader.vue -->
<template>
  <header class="page-header">
    <p class="eyebrow">Vue.js 从入门到精通</p>
    <h1>我的学习看板</h1>
    <p>把大页面拆成职责清晰的小组件。</p>
  </header>
</template>

<script setup>
const seriesName = 'Vue.js 从入门到精通'
</script>

<style scoped>
.page-header {
  padding: 24px;
  border-radius: 12px;
  background: #f0fdf4;
}
.eyebrow {
  color: #059669;
  font-weight: 600;
}
</style>

三个代码块分别承担不同职责:

代码块

作用

是否必需

<template>

描述组件渲染的界面结构

通常需要

<script setup>

编写组件状态与行为

按需要添加

<style>

编写组件样式

按需要添加

模板中可以直接使用 <script setup> 声明的变量和函数。scoped 表示当前样式主要作用于本组件,避免类名意外影响其他组件。

单文件组件需要由 Vite 等构建工具编译,不能直接把 .vue 文件当作普通 HTML 在浏览器中打开。


三、局部注册:按需引入组件

局部注册表示只有当前组件明确引入的子组件才能使用。它是日常开发中最常用、也最容易追踪来源的方式。

3.1 在 <script setup> 中使用

在 <script setup> 中导入的组件可以直接用于模板:

<!-- App.vue -->
<template>
  <main>
    <PageHeader />
  </main>
</template>

<script setup>
import PageHeader from './components/PageHeader.vue'
</script>

这里不需要额外的 components 选项。import 同时完成了组件引入和局部注册,模板中的 <PageHeader /> 会被编译为刚导入的组件。

一个组件可以渲染多次:

<template>
  <main>
    <PageHeader />
    <PageHeader />
  </main>
</template>

<script setup>
import PageHeader from './components/PageHeader.vue'
</script>

实际项目中当然不需要重复显示页面标题,这个例子只说明组件是可重复使用的构造单位,而不是一段只能出现一次的模板文本。

3.2 使用横线命名

导入的 PageHeader 在模板中也可以写成 <page-header>:

<template>
  <page-header />
</template>

<script setup>
import PageHeader from './components/PageHeader.vue'
</script>

PageHeader 与 page-header 可以对应。DOM 内模板对大小写和自闭合标签有限制,但在单文件组件中,推荐统一使用 PascalCase 组件名和自闭合写法:

<PageHeader />

这样更容易区分原生 HTML 元素与 Vue 组件。

3.3 Options API 的局部注册

如果项目使用 Options API,需要通过 components 选项注册:

<template>
  <PageHeader />
</template>

<script>
import PageHeader from './components/PageHeader.vue'

export default {
  components: {
    PageHeader
  }
}
</script>

components 对象的键是模板中使用的名字,值是导入的组件。以下写法可以自定义注册名:

export default {
  components: {
    DashboardHeader: PageHeader
  }
}

此时模板应使用 <DashboardHeader />。没有特殊兼容需求时,注册名与组件名保持一致更便于搜索。


四、全局注册:整个应用都能使用

全局注册会把组件注册到应用实例上,注册后不需要在每个组件中重复导入。

// main.js
import { createApp } from 'vue'
import App from './App.vue'
import BaseButton from './components/BaseButton.vue'

const app = createApp(App)

app.component('BaseButton', BaseButton)

app.mount('#app')

注册后,应用中的组件都可以直接使用:

<template>
  <BaseButton>保存</BaseButton>
</template>

也可以在 Options API 中通过应用实例连续注册:

app.component('BaseButton', BaseButton)
  .component('BaseCard', BaseCard)
  .component('BaseDialog', BaseDialog)

全局注册适合整个应用反复使用的基础组件,例如:

  • 通用按钮、输入框和图标;

  • 全局统一风格的卡片、弹窗;

  • 各页面都会出现的品牌元素。

全局注册也有代价:

  1. 模板里看到组件名时,不能直接通过当前文件的 import 找到来源。

  2. 即使某些页面没有使用,全局组件也可能进入最终构建产物,不利于构建工具按需优化。

  3. 注册过多会让基础组件与业务组件边界模糊。

因此,不要为了减少几行 import 就把所有组件都注册为全局组件。


五、局部注册与全局注册如何选择

对比项

局部注册

全局注册

使用范围

当前组件

整个应用

来源追踪

当前文件有明确 import

需要查找应用注册入口

常见场景

页面组件、业务组件

高频基础组件

构建优化

更容易按需分析

可能包含未使用组件

推荐程度

默认选择

少量、有意识地使用

可以采用一个简单的判断顺序:

  1. 只在一个页面或业务模块使用:局部注册。

  2. 只在一个功能目录使用:在该模块的父组件中局部注册。

  3. 跨多个模块频繁出现,并且风格统一:考虑全局注册。

有些项目使用自动导入插件生成局部导入。自动导入减少的是重复代码,并不等同于全局注册;组件能否按需打包仍取决于具体插件与构建配置。


六、组件命名与文件组织

6.1 使用明确的多单词名称

组件名应尽量避免与现有或未来的 HTML 元素冲突。Vue 官方风格指南建议使用多单词组件名:

推荐:BaseButton、UserCard、TaskList
不推荐:Button、Card、List

App 是常见的根组件例外。基础组件可以统一使用 Base、App 或项目约定前缀:

components/
├── base/
│   ├── BaseButton.vue
│   └── BaseCard.vue
├── dashboard/
│   ├── ProgressPanel.vue
│   └── TaskList.vue
└── layout/
    ├── PageHeader.vue
    └── PageFooter.vue

目录不必一开始就设计得很复杂。组件较少时可以先使用扁平结构,等业务边界稳定后再按功能拆分。

6.2 保持导入路径与命名一致

以下导入虽然可以运行,但会增加理解成本:

import UserCard from './components/ProfilePanel.vue'

更推荐让组件用途、文件名和导入名保持一致:

import UserCard from './components/UserCard.vue'

文件名建议使用 PascalCase.vue,模板中也使用相同名字。统一命名后,全局搜索和重构都会更可靠。

6.3 一个文件只定义一个主要组件

单文件组件通常只包含一个主要组件。如果两个组件始终共同完成一个职责,可以先放在同一目录;如果它们可以独立复用,应拆成不同文件。

拆分依据是职责和复用关系,而不是代码行数。


七、每次渲染都会创建独立的组件实例

组件最重要的特征之一,是每次使用都会创建独立实例。组件内部状态默认互不共享。

创建一个简单的计数按钮:

<!-- CounterButton.vue -->
<template>
  <button class="counter-button" @click="count++">
    当前计数:{{ count }}
  </button>
</template>

<script setup>
import { ref } from 'vue'

const count = ref(0)
</script>

<style scoped>
.counter-button {
  padding: 8px 16px;
  border: 0;
  border-radius: 6px;
  color: white;
  background: #42b883;
  cursor: pointer;
}
</style>

在父组件中使用三次:

<template>
  <section class="counter-list">
    <CounterButton />
    <CounterButton />
    <CounterButton />
  </section>
</template>

<script setup>
import CounterButton from './components/CounterButton.vue'
</script>

<style scoped>
.counter-list {
  display: flex;
  gap: 12px;
}
</style>

点击第一个按钮只会修改第一个实例的 count,其余两个按钮保持原值。每个实例都执行自己的组件初始化逻辑,并维护自己的响应式状态。

这也是组件与普通 JavaScript 对象的重要区别:组件定义描述“如何创建实例”,模板中的每次使用都会生成对应实例。

如果需要让多个组件使用同一份数据,后面可以通过 Props、自定义事件或状态管理完成,而不是依赖它们碰巧共享模块变量。


八、组件组合:父组件负责安排页面结构

组件组合是指父组件通过模板把多个子组件放在合适的位置。先创建两个职责单一的组件。

8.1 学习进度组件

<!-- ProgressPanel.vue -->
<template>
  <section class="panel">
    <div class="panel-heading">
      <h2>本周进度</h2>
      <strong>{{ percent }}%</strong>
    </div>
    <progress :value="completed" :max="total"></progress>
    <p>已完成 {{ completed }} / {{ total }} 个学习任务</p>
  </section>
</template>

<script setup>
import { computed, ref } from 'vue'

const completed = ref(7)
const total = ref(10)
const percent = computed(() => Math.round((completed.value / total.value) * 100))
</script>

<style scoped>
.panel {
  padding: 20px;
  border: 1px solid #d1d5db;
  border-radius: 12px;
}
.panel-heading {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
progress {
  width: 100%;
  accent-color: #42b883;
}
</style>

8.2 任务列表组件

<!-- TaskList.vue -->
<template>
  <section class="panel">
    <h2>学习任务</h2>
    <ul class="task-list">
      <li
        v-for="task in tasks"
        :key="task.id"
        :class="{ completed: task.done }"
      >
        <label>
          <input v-model="task.done" type="checkbox">
          {{ task.title }}
        </label>
      </li>
    </ul>
  </section>
</template>

<script setup>
import { ref } from 'vue'

const tasks = ref([
  { id: 1, title: '复习模板语法', done: true },
  { id: 2, title: '掌握事件处理', done: true },
  { id: 3, title: '完成组件拆分', done: false }
])
</script>

<style scoped>
.panel {
  padding: 20px;
  border: 1px solid #d1d5db;
  border-radius: 12px;
}
.task-list {
  display: grid;
  gap: 12px;
  padding: 0;
  list-style: none;
}
.completed label {
  color: #6b7280;
  text-decoration: line-through;
}
</style>

这里直接修改 v-for 当前项的 done,每个任务都有稳定且唯一的 id。任务状态只属于 TaskList 实例。

8.3 在根组件中组合

<!-- App.vue -->
<template>
  <main class="dashboard">
    <PageHeader />
    <div class="dashboard-grid">
      <ProgressPanel />
      <TaskList />
    </div>
  </main>
</template>

<script setup>
import PageHeader from './components/PageHeader.vue'
import ProgressPanel from './components/ProgressPanel.vue'
import TaskList from './components/TaskList.vue'
</script>

<style scoped>
.dashboard {
  max-width: 960px;
  margin: 0 auto;
  padding: 32px 20px;
}
.dashboard-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 20px;
  margin-top: 20px;
}
</style>

App.vue 不需要知道任务列表内部如何保存状态,也不需要复制进度卡片的样式。它只负责引入组件并安排页面布局。

这种组合方式也明确了后续通信的位置:当 PageHeader 需要展示动态标题,或者 ProgressPanel 需要读取 TaskList 的任务进度时,再引入 Props、自定义事件或共享状态,而不是提前把所有逻辑写回根组件。


九、组件样式与属性透传

9.1 scoped 减少样式冲突

组件中的样式默认是全局的。如果多个组件都定义 .title,后加载的样式可能影响其他组件:

<style>
.title {
  color: #42b883;
}
</style>

组件私有样式通常可以添加 scoped:

<style scoped>
.title {
  color: #42b883;
}
</style>

编译后,Vue 会为当前组件元素添加特殊属性,让选择器更有针对性。scoped 是样式隔离手段,不是 CSS 模块,也不意味着可以忽略选择器设计。

需要修改子组件内部元素或第三方组件时,应先确认组件是否提供样式接口,不要盲目使用全局选择器穿透其内部结构。

9.2 class 和 style 默认传到根元素

父组件可以像使用普通元素一样给单根组件添加类名:

<template>
  <PageHeader class="dashboard-header" />
</template>
<!-- PageHeader.vue -->
<template>
  <header class="page-header">
    <h1>我的学习看板</h1>
  </header>
</template>

最终根元素会合并 page-header 和 dashboard-header。style 也有类似的透传行为。

多根节点组件没有唯一根元素,无法自动决定属性落到哪里。此时可以关闭自动透传并显式指定目标:

<template>
  <header>页头</header>
  <section v-bind="$attrs">正文</section>
</template>

<script setup>
defineOptions({ inheritAttrs: false })
</script>

只有确实需要控制透传目标时才关闭 inheritAttrs。普通单根组件保持默认行为即可。


十、组件名与递归组件

组件可以通过 defineOptions 显式声明名称:

<!-- FileTree.vue -->
<template>
  <ul>
    <li>当前目录</li>
  </ul>
</template>

<script setup>
defineOptions({
  name: 'FileTree'
})
</script>

组件名有助于开发者工具识别和递归引用。一个具名单文件组件可以在自己的模板中通过该名称引用自身,例如树形目录、嵌套评论等场景:

<template>
  <ul>
    <li>
      当前节点
      <FileTree v-if="hasChildren" />
    </li>
  </ul>
</template>

<script setup>
import { ref } from 'vue'

defineOptions({ name: 'FileTree' })
const hasChildren = ref(false)
</script>

递归必须有能够结束的明确条件,否则会不断创建组件。本文示例默认 hasChildren 为 false,实际项目中应根据树节点数据判断是否还有子节点。

普通组件不需要为了“规范”全部添加 defineOptions。文件名和导入名已经足够清晰时,保持简洁即可。


十一、常见问题自查

问题

推荐处理

模板提示无法解析组件

检查是否导入、导入路径是否正确,以及模板名称是否对应

<script setup> 中又配置 components

通常不需要,导入后直接在模板使用

全局组件在某处不可用

检查是否在 app.mount() 前注册到同一个应用实例

不知道组件来自哪里

优先局部注册,或检查全局注册入口和自动导入配置

多个组件的状态互相影响

检查是否错误地共享模块级响应式对象;组件实例状态通常应独立

一个组件文件越来越长

按职责拆分,不要只按模板行数机械拆分

样式影响其他组件

检查是否误用全局样式,组件私有样式可使用 scoped

多根组件的 class 没有生效

关闭自动透传后,对目标根节点显式使用 v-bind="$attrs"

递归组件不断渲染

检查递归结束条件和子节点数据

把所有组件都全局注册

改为默认局部注册,只保留高频基础组件

还要特别留意 DOM 内模板的限制。如果直接在 HTML 文件中使用 Vue 模板,浏览器解析规则会影响大小写、自闭合组件和表格等元素中的标签位置。使用单文件组件和构建工具可以避开大多数这类问题。


十二、总结

组件化开发的重点不是文件数量,而是职责边界:

  1. 单文件组件用 <template>、<script setup> 和 <style> 组织界面、逻辑与样式。

  2. <script setup> 中导入的组件可以直接使用,不需要额外的 components 选项。

  3. 局部注册来源清晰、易于按需构建,应作为默认选择。

  4. 全局注册适合少量高频基础组件,不适合注册所有业务组件。

  5. 组件名、文件名和导入名应保持一致,并优先使用明确的多单词名称。

  6. 每次使用组件都会创建独立实例,组件内部状态默认互不影响。

  7. 父组件通过组合安排页面结构,子组件负责各自职责,私有样式可以使用 scoped。

下一站预告

下一篇《Props与组件通信》将为组件增加数据输入能力,学习 Props 声明、类型校验、单向数据流和默认值,让父组件能够把不同数据传给同一个子组件。


作者:洋洋技术笔记 发布日期:2026-11-06 系列:Vue.js从入门到精通 - 第8篇

0

评论区