使用组件
组件的创建
当使用构建步骤时,我们一般会将 Vue 组件定义在一个单独的
.vue 文件中,这被叫做单文件组件
(简称 SFC)
单文件组件的基本结构如下
1 | <template> |
总结一下就是,一个完整的 Vue 单文件组件(SFC)包含三个核心部分:
1 | <template> |
首先,组件有命名规范
- kebab-case
(短横线分隔):
<my-component>,推荐在模板中使用 - PascalCase
(大驼峰):
<MyComponent>,推荐在单文件组件和 JavaScript 中使用
所以说,在编写组件的时候,我们首先需要编写组件的 HTML
结构,也就是模板部分<template>,在这部分我们需要完成如下内容
- 定义组件的 HTML 结构
- 使用 Vue 的模板语法(插值、指令等)
- 必须有且仅有一个根元素(或使用
<template>作为根容器)
然后,需要编写这个组件需要做的事情,也就是脚本部分
<script>
- 定义组件的逻辑:数据、方法、Props、生命周期等
- 使用
setup语法糖(Vue 3 推荐)结合 TypeScript - 导出组件的配置选项
最后,你需要编写样式部分美化这个组件,也就是<style>部分
- 定义组件的样式
scoped属性:样式仅作用于当前组件- 可使用 CSS 预处理器(如 SCSS)
以下面组件为例,使用 Vue 3 的 <script setup>
语法糖创建组件是当前推荐的方式
1 | <script setup lang="ts"> |
关于 <script setup> 语法糖
下面细说,来看组件的脚本部分
1 | <script setup lang="ts"> |
import语句导入依赖1
2import { computed } from 'vue' // 导入 Vue 的响应式 API
import type { Word } from '../stores/wordbook' // 导入类型定义然后定义 Props(组件属性)
这是组件创建中非常重要的一环,用于接收父组件传递的数据:
1
2
3
4
5
6
7
8
9
10
11
12// 1. 定义 Props 接口(TypeScript 类型)
interface Props {
word: Word // 必选属性,Word 类型
showExample?: boolean // 可选属性,布尔类型
cardStyle?: 'default' | 'compact' | 'detailed' // 可选属性,联合类型
}
// 2. 使用 defineProps 定义属性并设置默认值
const props = withDefaults(defineProps<Props>(), {
showExample: true, // 默认显示例句
cardStyle: 'default' // 默认样式
})- 使用 TypeScript 接口定义类型,获得类型检查和智能提示
withDefaults()为可选属性设置默认值defineProps()是 Vue 提供的编译宏,无需导入即可使用
定义响应式数据和计算属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14// 计算属性:根据难度动态生成样式类
const difficultyClass = computed(() => {
return `difficulty-${props.word.difficulty}`
})
// 计算属性:根据难度动态生成颜色
const difficultyColor = computed(() => {
const colors = {
easy: '#27ae60',
medium: '#f39c12',
hard: '#e74c3c'
}
return colors[props.word.difficulty]
})
然后,我们会在 template 模板部分中使用这些内容
1 | <template> |
首先,我们定义了一个基础的 div ,作为模板的基本结构与根元素
1
2
3<div class="word-card" :class="[cardStyle, difficultyClass]">
<!-- 所有内容都包裹在这个根元素中 -->
</div>- Vue 模板必须有且仅有一个根元素(Vue 3 支持多根节点,但单一根元素仍是最佳实践)
- 根元素通常用于设置组件的基础样式和布局
- 这里结合了静态类和动态类的绑定,通过
v-bind: 指令,绑定了一个动态类数组,其中
cardStyle来自上面script部分的 Props 接口,difficultyClass来自计算属性
然后这里还使用了一些文本插值的内容,也算是之前的复习了
1
2<h3 class="word-title">{{ word.word }}</h3>
<p class="translation">{{ word.translation }}</p>使用了
{{ }},Mustache 语法,用于插入文本,然后也在里面访问 Props、data、计算属性等响应式数据,在模板中访问 Props 时可省略props.前缀(直接写word而非props.word)下面还有条件渲染的内容,使用 v-if 指令,意味着,只有当条件为真时,元素才会被渲染,注意 v-if 和 v-show 区别就好
1
2
3
4<!-- 条件渲染:使用 v-if 根据 showExample prop 决定是否显示 -->
<p v-if="showExample && word.example" class="example">
<strong>例句:</strong>{{ word.example }}
</p>绑定样式,
:style动态绑定内联样式,使用指令绑定,值为对象,键是 CSS 属性名,这里绑定了背景颜色,值来自计算属性difficultyColor1
2
3
4<span
class="difficulty-badge"
:style="{ backgroundColor: difficultyColor }"
>
最后就是组件样式的创建
1 | <style scoped> |
首先,组件的样式需要设置作用域
1 | <style scoped> |
使用 scoped 属性避免样式冲突,Vue
会自动为组件元素添加唯一属性,并修改选择器
可以发现,样式存在基础样式和它的变体
1 | /* 基础样式 */ |
我们通过组合类名实现不同的样式变体,它与 Props 中的
cardStyle 配合使用
组件之间如何打造成父子组件的关系
上期的例子中,WordList 组件和 WordCard
组件形成了典型的父子组件关系
- 父组件:
WordList组件(负责管理单词列表数据,控制整体展示逻辑) - 子组件:
WordCard组件(负责单个单词卡片的渲染,接收父组件传递的数据)
那么这种关系应该如何去维护
核心通信方式之一还是通过 Props 传递数据(父→子)
父组件(WordList)通过 Props
向子组件(WordCard)传递数据,子组件接收并使用这些数据。
子组件WordCard.vue通过 defineProps
明确声明需要接收的属性,并指定类型和默认值,确保数据规范:
1 | // WordCard.vue 中定义 Props |
word是必填属性(无默认值),类型为Word接口,其他属性为可选,通过withDefaults设置默认值。
父组件WordList 在使用子组件时,通过
属性绑定(:属性名)传递数据:
1 | <!-- WordList.vue 中使用 WordCard 并传递 Props --> |
:word="word":将父组件中遍历的word对象传递给子组件,子组件通过props.word使用(如模板中直接用{{ word.word }}显示单词)。
那么还存在一个核心通信方式就是 Events 传递事件(子→父)
子组件(WordCard)通过 事件
向父组件(WordList)传递交互信息,父组件监听并处理这些事件。
首先父组件定义并监听事件,WordList.vue父组件通过
defineEmits
声明可以接收的事件和事件出发后对应的操作,并在子组件触发时执行对应的事件逻辑
1 | // WordList.vue 中定义可触发的事件 |
然后在模板中通过 @事件名
监听子组件的交互(如按钮点击):
1 | <!-- WordList.vue 中监听子组件内部的交互 --> |
- 当子组件中的按钮被点击时,父组件的
handleEdit或handleDelete方法会被调用,并通过emit触发事件(如emit('deleteWord', id)),将数据传递给更上层组件。
子组件中的按钮点击事件可能会冒泡到父组件的卡片点击事件,因此使用
@click.stop 阻止冒泡:
插槽也是常用的方式,父组件通过 插槽 向子组件的指定位置插入内容,增强子组件的灵活性,也是父子组件通信的一种方式
子组件定义插槽WordCard.vue,在模板中预留插槽位置,供父组件插入内容:
而命名插槽 actions 明确指定了插入位置
父组件通过 <template #插槽名>
向子组件的插槽插入内容:
父组件根据自身需求,向子组件的 actions 插槽插入了 “编辑”
和 “删除” 按钮,子组件会将这些按钮渲染到预留位置。
父组件通过插槽向子组件插入自定义内容(如操作按钮),增强子组件的复用性。
使用 v-model 进行双向绑定
使用双向绑定有两种方式,不仅有传统的 “props + emit” 方式,也可以使用
Vue 3.4 新增的 defineModel() 简化实现。
所以来总结一下
传统方式的核心逻辑是:父组件通过 props
向子组件传递数据,子组件通过触发特定事件(默认
update:modelValue)通知父组件更新数据。
子组件需要这样实现,例如我们需要一个自定义输入框
1 | <!-- ChildInput.vue --> |
- 子组件通过
props.modelValue接收初始值 Props,通过emit('update:modelValue', 新值)通知父组件更新,形成双向绑定。
父组件使用
1 | <!-- Parent.vue --> |
- 父组件的
v-model="parentValue"等价于:<ChildInput :modelValue="parentValue" @update:modelValue="parentValue = $event" />
这个真不太好用,使用 defineModel()
是更常见的选择,内部自动处理了 props 和 emit
的逻辑,使代码更简洁。
还是一样的例子,来看看这次的子组件的实现
1 | <!-- ChildInput.vue --> |
defineModel()返回一个 ref 对象,该对象自动与父组件的v-model绑定:- 读取
inputValue.value时,等价于读取props.modelValue - 修改
inputValue.value时,会自动触发emit('update:modelValue', 新值)
- 读取
父组件的使用没什么多说的,使用还是一样的,在使用子组件的标签中带上
v-model
当需要绑定多个值时,可以通过自定义名称区分,两种方式均支持
传统方式的多值绑定
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23<!-- 子组件 -->
<script setup lang="ts">
const props = defineProps<{
title: string // 自定义名称 title
content: string // 自定义名称 content
}>()
const emit = defineEmits<{
(e: 'update:title', value: string): void
(e: 'update:content', value: string): void
}>()
</script>
<template>
<input
:value="title"
@input="emit('update:title', $event.target.value)"
/>
<textarea
:value="content"
@input="emit('update:content', $event.target.value)"
/>
</template>1
2
3
4
5<!-- 父组件使用 -->
<ChildComponent
v-model:title="formTitle"
v-model:content="formContent"
/>defineModel()方式对多值绑定的支持很好很简单1
2
3
4
5
6
7
8
9
10
11<!-- 子组件 -->
<script setup lang="ts">
// 自定义名称,通过参数指定
const title = defineModel<string>('title') // 对应 v-model:title
const content = defineModel<string>('content') // 对应 v-model:content
</script>
<template>
<input v-model="title" />
<textarea v-model="content" />
</template>1
2
3
4
5<!-- 父组件使用(与传统方式一致) -->
<ChildComponent
v-model:title="formTitle"
v-model:content="formContent"
/>
计算属性
使用计算属性
计算属性(computed)的使用非常常见,主要用于派生和处理基于原始数据的状态:
- 基于其他响应式数据计算得出
- 具有缓存机制,只有依赖变化时才重新计算
模板中的表达式虽然方便,但仅适用于简单操作。若在模板中写入过多逻辑,会导致模板臃肿、难以维护。例如,有一个包含嵌套数组的对象:
1 | const author = reactive({ |
若要根据 author
是否有书籍展示不同信息,直接在模板中编写逻辑如下:
1 | <p>Has published books:</p> |
这种写法在逻辑复杂的时候,模板内的标签会带上很长一个表达式,非常不优雅,还存在重复代码的问题
因此,推荐使用计算属性描述依赖响应式状态的复杂逻辑,重构后的示例如下:
1 | <script setup> |
computed()方法接收一个 getter 函数,返回值为计算属性 ref。- 可通过
publishedBooksMessage.value访问计算结果(与普通 ref 一致)。 - 模板中会自动解包计算属性 ref,无需添加
.value。 - Vue 会自动追踪响应式依赖:当
author.books变化时,所有依赖publishedBooksMessage的绑定都会同步更新
那么来以我们还是上面的例子,实际说明一下计算属性的使用
1 | const stats = computed(() => { |
计算属性通过 computed() 函数创建,接收一个 getter
函数,返回计算后的结果。它的核心作用是:
- 基于已有响应式数据(这里是
props.words)派生新状态 - 自动追踪依赖(当
props.words变化时,计算属性会自动重新计算) - 结果会被缓存,只有当依赖变化时才重新计算,提升性能
计算属性还可以存在依赖关系,第二个计算属性 percentages
依赖于第一个计算属性 stats:
1 | const percentages = computed(() => { |
这种依赖关系会形成响应式链条,当最底层的 props.words
变化时,会触发 stats 重新计算,进而触发
percentages 重新计算。
计算属性在模板中直接以属性形式使用(无需加 ())
1 | <div class="stat-value">{{ stats.total }}</div> |
模板会自动追踪计算属性的变化,当计算结果更新时,视图会同步刷新。
那么,计算属性缓存和方法我们比较一下
如果我们的功能不使用计算属性,通过函数调用也能实现与计算属性相同的结果,例如:
1 | <!-- 模板 --> |
1 | // 组件中 |
但两者的核心区别在于缓存机制:
| 特性 | 计算属性 | 方法 |
|---|---|---|
| 缓存机制 | 基于响应式依赖缓存结果 | 无缓存,每次调用重新执行 |
| 执行时机 | 仅依赖更新时重新计算 | 组件重渲染时必执行 |
| 性能优化 | 适合复杂计算/频繁访问场景 | 适合无需缓存的简单逻辑 |
可写计算属性
计算属性默认是只读的,这是因为我们通常只定义了 getter 函数(用于计算值)。当尝试直接修改计算属性时,Vue 会抛出运行时警告。
但在某些场景下,我们需要让计算属性支持 “写入” 操作,这时可以通过同时定义 getter 和 setter 来实现 “可写计算属性”。
可写计算属性的定义形式如下(以 Vue 3 组合式 API 为例):
1 | import { computed, ref } from 'vue' |
- getter:和普通计算属性一样,负责根据依赖的响应式数据(如
firstName、lastName)计算并返回当前值。 - setter:当对计算属性赋值时(如
fullName.value = '李 四'),Vue 会自动调用 setter 函数,并将赋值的 “新值” 作为参数传入。在 setter 中,我们需要手动更新依赖的源数据(如修改firstName和lastName),从而触发响应式更新。
假设我们上面的例子需要支持通过修改 “总数” 来批量调整单词难度(仅作示例,实际业务需谨慎),可定义如下可写计算属性:
1 | // 在 WordStats.vue 的 <script setup> 中 |
在模板中使用:
1 | <!-- 可直接赋值修改 --> |
- 必须更新源数据:setter 的核心作用是将 “对计算属性的修改” 映射到 “对依赖的响应式数据的修改”。如果只修改计算属性本身而不更新源数据,会导致数据不一致(因为 getter 仍会基于旧的源数据计算)。
- 与 props 配合:若依赖的源数据是 props(如
WordStats.vue 中的
props.words),由于 props 是单向数据流(子组件不能直接修改),需通过emit通知父组件更新(如示例中的update:words)。 - 适用场景:可写计算属性适用于
“需要通过一个属性同步控制多个源数据” 的场景(如示例中的
fullName同步firstName和lastName),避免手动编写多个同步逻辑。
对了,可写计算属性仅支持 Vue 3.4+
组件样式隔离
Scoped Styles
组件样式隔离就是确保组件内定义的样式仅对当前组件的模板元素生效,不会污染其他组件或全局样式。
它的核心目的是避免不同组件之间的 CSS 类名冲突(全局污染)。
Vue 提供了几种主要的机制来实现这一点,其中最常用的是
<style scoped>,其次是 CSS Modules。
首先,使用<style scoped>进行组件样式隔离是最常用的方式
当你在 .vue 文件中给 <style> 标签加上 scoped
属性时,Vue 的编译器(配合 PostCSS)会做两件事:
- HTML 层面:给当前组件模板中的所有 DOM 元素添加一个独一无二的自定义属性(Attribute),通常格式为 data-v-xxxxxxx(哈希值)。
- CSS 层面:利用 CSS 属性选择器,将写在






