# 组件的艺术：构建可复用片段


## 概述

上一篇文章中，所有代码都写在同一个文件里。

页面一复杂，这个文件就会变得很长，难以维护。

组件就是解决这个问题的方法。你可以把页面拆成多个独立的小块，每个块只负责自己的事。最后拼在一起，组成完整的页面。

## 什么是组件

组件可以理解为一个"**可复用的自定义标签**"。

比如，一个按钮在很多地方都用到了相同的样式和逻辑。可以把它提取成一个组件：

```vue
<!-- SubmitButton.vue -->
<template>
  <button
    class="px-4 py-2 rounded bg-blue-500 text-white text-sm"
    @click="handleClick"
  >
    提交
  </button>
</template>

<script setup lang="ts">
function handleClick() {
  console.log('提交按钮被点击了')
}
</script>
```

在其他组件中引用它：

```vue
<template>
  <SubmitButton />
  <SubmitButton></SubmitButton>  <!-- 两种写法都可以 -->
</template>

<script setup lang="ts">
import SubmitButton from '@/components/SubmitButton.vue'
</script>
```

## 命名规范

Vue 3 对组件名有两个约定：

| 规则 | 说明 | 示例 |
|------|------|------|
| **PascalCase** | 首字母大写 | `SubmitButton`、`UserCard` |
| **至少两个单词** | 避免和 HTML 原生标签冲突 | ✅ `SubmitButton` ❌ `Button` |

单项导出的组件和 TypeScript 功能类似，可以这样写：

```vue
<script setup lang="ts">
import SubmitButton from '@/components/SubmitButton.vue'
</script>
```

## 组件注册

在 Vue 3 中，组件默认是**局部注册**的。

什么叫局部注册？就是你用 `import` 导入组件后，只能在当前文件中使用它。不同的页面即使用到同一个组件，也需要各自导入。

这样做的优点是"用了才加载"，不会额外增加其他页面的体积。

> [!info] 说明
> 全局注册也可以做到，但很少用。全局注册会让组件永远被打包，即使有些页面用不到它。

## 组件拆分的原则

什么时候该拆成一个新组件？看几个信号：

| 信号 | 说明 |
|------|------|
| 代码太长 | 单个文件超过 200~300 行，考虑拆分 |
| 可复用 | 同一段代码在多个地方出现 |
| 功能独立 | 某块逻辑可以独立描述（如用户卡片、评论列表） |
| 测试需要 | 某个功能需要单独测试 |

一个常见的拆分方法是按"**页面 → 区块 → 小组件**"的层次来拆分：

```
页面组件（src/pages/Home.vue）
  ├─ 头部区域（Header.vue）
  ├─ 文章列表（ArticleList.vue）
  │   └─ 文章卡片（ArticleCard.vue）
  └─ 底部区域（Footer.vue）
```

> [!tip] 技巧
> 刚开始练习时，不用追求完美拆分。先写在一起，等发现某个部分重复使用了，再把它提取出来。重构是正常的开发过程。

## 生命周期钩子在实践中的应用

上一篇介绍了生命周期钩子的基本用法。这里用几个实际场景加深理解：

### 场景一：进入页面时获取数据

这是最常用的模式。在 `onMounted` 中调用 API：

```vue
<script setup lang="ts">
import { ref, onMounted } from 'vue'

const articles = ref<Article[]>([])

onMounted(async () => {
  const response = await fetch('/api/articles')
  articles.value = await response.json()
})
</script>
```

### 场景二：离开页面时清理

`onUnmounted` 用来清理定时器、取消订阅：

```vue
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'

const now = ref(new Date().toLocaleString())
let timer: number

onMounted(() => {
  timer = setInterval(() => {
    now.value = new Date().toLocaleString()
  }, 1000)
})

onUnmounted(() => {
  clearInterval(timer)
})
</script>

<template>
  <p>当前时间：{{ now }}</p>
</template>
```

### 场景三：数据变化时做额外操作

```vue
<script setup lang="ts">
import { ref, watch, onUpdated } from 'vue'

const items = ref<string[]>([])

// 1. 用 watch 监听数据变化
watch(items, (newList) => {
  console.log('列表长度变化：', newList.length)
})

// 2. 用 onUpdated 在 DOM 更新后操作
onUpdated(() => {
  console.log('DOM 已经更新')
})
</script>
```

---

组件是 Vue 3 中组织代码的基本单元。

现在你有了多个组件，但每个组件是独立的。

下一步要解决的矛盾是：**父组件怎么把数据传给子组件？子组件怎么通知父组件？**


---

> 作者: Aphros  
> URL: https://blog.papergate.top/posts/%E7%BB%84%E4%BB%B6%E7%9A%84%E8%89%BA%E6%9C%AF%E6%9E%84%E5%BB%BA%E5%8F%AF%E5%A4%8D%E7%94%A8%E7%89%87%E6%AE%B5/  

