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

第 1 节 概述

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

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

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

第 2 节 什么是组件

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<!-- 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>

在其他组件中引用它:

1
2
3
4
5
6
7
8
<template>
  <SubmitButton />
  <SubmitButton></SubmitButton>  <!-- 两种写法都可以 -->
</template>

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

第 3 节 命名规范

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

规则说明示例
PascalCase首字母大写SubmitButtonUserCard
至少两个单词避免和 HTML 原生标签冲突SubmitButtonButton

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

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

第 4 节 组件注册

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

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

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

说明

全局注册也可以做到,但很少用。全局注册会让组件永远被打包,即使有些页面用不到它。

第 5 节 组件拆分的原则

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

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

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

1
2
3
4
5
页面组件(src/pages/Home.vue)
  ├─ 头部区域(Header.vue)
  ├─ 文章列表(ArticleList.vue)
  │   └─ 文章卡片(ArticleCard.vue)
  └─ 底部区域(Footer.vue)
技巧

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

第 6 节 生命周期钩子在实践中的应用

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

6.1 场景一:进入页面时获取数据

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<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>

6.2 场景二:离开页面时清理

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<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>

6.3 场景三:数据变化时做额外操作

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<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 中组织代码的基本单元。

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

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