第一部分:Vue 3 基础入门
第13集:Props:组件间数据传递
在Vue应用中,组件间的数据传递是非常常见的操作。Props是Vue中用于父组件向子组件传递数据的机制。在本集中,我们将学习Props的基本概念、使用方法、类型验证和最佳实践。
13.1 Props的基本概念
13.1.1 什么是Props?
Props(Properties的缩写)是Vue中用于父组件向子组件传递数据的自定义属性。子组件通过定义props来接收父组件传递的数据,然后在组件内部使用这些数据。
13.1.2 Props的特点
- 单向数据流:数据只能从父组件流向子组件,子组件不能直接修改props
- 类型安全:可以为props指定类型,Vue会在开发环境中进行类型检查
- 默认值支持:可以为props设置默认值
- 必需性验证:可以指定props是否为必需的
- 自定义验证:可以为props添加自定义验证函数
13.1.3 Props的使用场景
- 父组件向子组件传递配置信息
- 子组件需要根据父组件的数据动态渲染
- 组件复用,通过props定制组件行为
13.2 Props的基本使用
13.2.1 子组件定义Props
在Vue 3中,我们可以使用defineProps()宏来定义子组件的props。
示例:
<!-- src/components/ChildComponent.vue -->
<template>
<div class="child-component">
<h2>{{ title }}</h2>
<p>{{ message }}</p>
<p>计数: {{ count }}</p>
<p>是否激活: {{ isActive ? '是' : '否' }}</p>
</div>
</template>
<script setup>
// 定义props
const props = defineProps([
'title',
'message',
'count',
'isActive'
])
</script>
<style scoped>
.child-component {
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 16px;
margin-bottom: 16px;
}
</style>13.2.2 父组件传递Props
父组件可以通过在子组件标签上添加属性的方式来传递props。
示例:
<template>
<div class="parent-component">
<h1>父组件</h1>
<!-- 传递props给子组件 -->
<ChildComponent
title="子组件标题"
message="这是来自父组件的消息"
:count="count"
:is-active="isActive"
/>
<button @click="count++">增加计数</button>
<button @click="isActive = !isActive">切换激活状态</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
import ChildComponent from './components/ChildComponent.vue'
// 父组件数据
const count = ref(0)
const isActive = ref(true)
</script>
<style scoped>
.parent-component {
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
button {
padding: 8px 16px;
margin-right: 10px;
background-color: #42b883;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
</style>13.3 Props的类型验证
为了提高组件的健壮性和可维护性,我们可以为props添加类型验证。
13.3.1 使用对象形式定义Props
使用对象形式定义props可以添加类型验证、默认值、必需性验证等。
示例:
<!-- src/components/ValidatedComponent.vue -->
<template>
<div class="validated-component">
<h2>{{ title }}</h2>
<p>{{ description }}</p>
<p>数量: {{ quantity }}</p>
<p>价格: {{ price }}元</p>
<p>标签: {{ tags.join(', ') }}</p>
</div>
</template>
<script setup>
// 使用对象形式定义props,添加类型验证
const props = defineProps({
// 字符串类型
title: {
type: String,
required: true
},
// 字符串类型,可选
description: {
type: String,
default: '默认描述'
},
// 数字类型
quantity: {
type: Number,
default: 0
},
// 数字类型,带自定义验证
price: {
type: Number,
default: 0,
validator: (value) => {
return value >= 0 // 价格必须大于等于0
}
},
// 数组类型
tags: {
type: Array,
default: () => [] // 数组和对象的默认值需要使用工厂函数
},
// 对象类型
config: {
type: Object,
default: () => ({})
},
// 布尔类型
isVisible: {
type: Boolean,
default: false
},
// 函数类型
callback: {
type: Function,
default: () => {}
}
})
</script>
<style scoped>
.validated-component {
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 16px;
margin-bottom: 16px;
}
</style>13.3.2 Props支持的类型
Vue 3支持以下props类型:
- 基本类型:String, Number, Boolean, Symbol
- 引用类型:Array, Object, Function
- 自定义类型:Class, 构造函数
- 联合类型:使用数组指定多个可能的类型
示例:
const props = defineProps({
// 联合类型:可以是String或Number
id: {
type: [String, Number],
required: true
},
// 自定义类型
user: {
type: User, // 假设User是一个类
default: () => new User()
}
})13.4 Props的传递方式
13.4.1 静态传递
静态传递是指直接传递字符串字面量作为props值。
示例:
<ChildComponent title="静态标题" />13.4.2 动态传递
动态传递是指使用v-bind或:绑定父组件的数据作为props值。
示例:
<ChildComponent
:title="dynamicTitle"
:count="count"
:is-active="isActive"
/>13.4.3 传递对象
可以将一个对象的所有属性作为props传递给子组件。
示例:
<template>
<div>
<!-- 传递整个对象的属性作为props -->
<ChildComponent v-bind="user" />
</div>
</template>
<script setup>
import { reactive } from 'vue'
import ChildComponent from './components/ChildComponent.vue'
const user = reactive({
name: '张三',
age: 25,
email: 'zhangsan@example.com'
})
</script>13.4.4 传递数组
可以将数组作为props传递给子组件。
示例:
<template>
<div>
<h1>产品列表</h1>
<ProductList :products="products" />
</div>
</template>
<script setup>
import { ref } from 'vue'
import ProductList from './components/ProductList.vue'
const products = ref([
{ id: 1, name: '苹果', price: 5.5 },
{ id: 2, name: '香蕉', price: 3.0 },
{ id: 3, name: '橙子', price: 4.0 }
])
</script>13.5 Props的单向数据流
13.5.1 单向数据流的概念
Vue中的props遵循单向数据流原则:数据只能从父组件流向子组件,子组件不能直接修改props。如果子组件需要修改props,应该通过事件通知父组件,由父组件来修改数据。
为什么要遵循单向数据流?
- 保持数据的可预测性:数据的来源清晰,便于调试和维护
- 避免组件间的耦合:子组件不依赖于父组件的实现细节
- 提高组件的复用性:组件可以在不同的场景中使用
13.5.2 子组件如何响应Props变化
如果子组件需要根据props的变化执行某些操作,可以使用watch或watchEffect来监听props的变化。
示例:
<template>
<div class="child-component">
<h2>{{ title }}</h2>
<p>{{ message }}</p>
<p>计数: {{ localCount }}</p>
</div>
</template>
<script setup>
import { ref, watch } from 'vue'
const props = defineProps(['title', 'message', 'count'])
// 子组件本地状态
const localCount = ref(props.count)
// 监听props变化
watch(
() => props.count,
(newCount) => {
localCount.value = newCount
console.log('count props变化了:', newCount)
}
)
</script>13.5.3 子组件如何修改Props
如果子组件需要修改props,应该通过事件通知父组件,由父组件来修改数据。
示例:
<!-- 子组件 -->
<template>
<div class="child-component">
<h2>{{ title }}</h2>
<p>{{ message }}</p>
<p>计数: {{ count }}</p>
<button @click="increment">增加计数</button>
</div>
</template>
<script setup>
const props = defineProps(['title', 'message', 'count'])
const emit = defineEmits(['update:count'])
function increment() {
// 通过事件通知父组件修改数据
emit('update:count', props.count + 1)
}
</script>
<!-- 父组件 -->
<template>
<div class="parent-component">
<h1>父组件</h1>
<ChildComponent
title="子组件标题"
message="这是来自父组件的消息"
:count="count"
@update:count="count = $event"
/>
</div>
</template>
<script setup>
import { ref } from 'vue'
import ChildComponent from './components/ChildComponent.vue'
const count = ref(0)
</script>13.6 Props的最佳实践
13.6.1 命名规范
- props名称使用camelCase:在JavaScript中使用camelCase,如
isActive - 在模板中使用kebab-case:在HTML模板中使用kebab-case,如
is-active - 避免使用Vue保留字:如
key,ref,is等
13.6.2 类型安全
- 始终为props指定类型
- 为必需的props添加
required: true - 为props设置合理的默认值
- 复杂类型的默认值使用工厂函数
- 对props进行必要的验证
13.6.3 数据流动
- 遵循单向数据流原则
- 子组件不要直接修改props
- 使用事件通知父组件修改数据
- 复杂状态使用状态管理库(如Pinia或Vuex)
13.6.4 组件设计
- 组件应该是自包含的,props是组件的外部接口
- 组件不要依赖于props的具体实现
- 组件应该对props的变化具有鲁棒性
- 避免传递过多的props,考虑使用对象传递相关属性
13.6.5 性能优化
- 避免传递大型对象或数组作为props,考虑传递必要的属性
- 对于复杂的计算,考虑在父组件中计算后再传递
- 使用
v-memo优化频繁更新的组件
13.7 综合示例:创建一个可配置的卡片组件
让我们创建一个可配置的卡片组件,展示props的实际应用。
示例:
<!-- src/components/ConfigurableCard.vue -->
<template>
<div
:class="['configurable-card', variant]"
:style="cardStyle"
@click="handleClick"
>
<!-- 头部插槽 -->
<div v-if="header || $slots.header" class="card-header">
<slot name="header">{{ header }}</slot>
</div>
<!-- 内容插槽 -->
<div class="card-body">
<slot></slot>
</div>
<!-- 底部插槽 -->
<div v-if="footer || $slots.footer" class="card-footer">
<slot name="footer">{{ footer }}</slot>
</div>
<!-- 关闭按钮 -->
<button
v-if="closable"
class="close-button"
@click.stop="handleClose"
>
×
</button>
</div>
</template>
<script setup>
import { computed, defineProps, defineEmits } from 'vue'
const props = defineProps({
// 卡片变体
variant: {
type: String,
default: 'default',
validator: (value) => {
return ['default', 'primary', 'success', 'danger', 'warning'].includes(value)
}
},
// 卡片标题
header: {
type: String,
default: ''
},
// 卡片底部内容
footer: {
type: String,
default: ''
},
// 卡片宽度
width: {
type: String,
default: '100%'
},
// 卡片高度
height: {
type: String,
default: 'auto'
},
// 是否可关闭
closable: {
type: Boolean,
default: false
},
// 是否可点击
clickable: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['close', 'click'])
// 计算卡片样式
const cardStyle = computed(() => {
return {
width: props.width,
height: props.height,
cursor: props.clickable ? 'pointer' : 'default'
}
})
// 处理卡片点击
function handleClick() {
if (props.clickable) {
emit('click')
}
}
// 处理关闭按钮点击
function handleClose() {
emit('close')
}
</script>
<style scoped>
.configurable-card {
border: 1px solid #e0e0e0;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
transition: all 0.3s;
position: relative;
}
.configurable-card:hover {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
.card-header {
padding: 16px;
background-color: #f5f5f5;
border-bottom: 1px solid #e0e0e0;
font-size: 18px;
font-weight: bold;
}
.card-body {
padding: 16px;
}
.card-footer {
padding: 16px;
background-color: #f5f5f5;
border-top: 1px solid #e0e0e0;
font-size: 14px;
color: #666;
}
.close-button {
position: absolute;
top: 8px;
right: 8px;
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #999;
padding: 0;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
}
.close-button:hover {
color: #333;
}
/* 卡片变体样式 */
.configurable-card.primary {
border-color: #3498db;
}
.configurable-card.success {
border-color: #2ecc71;
}
.configurable-card.danger {
border-color: #e74c3c;
}
.configurable-card.warning {
border-color: #f39c12;
}
</style>使用ConfigurableCard组件:
<template>
<div class="app">
<h1>可配置卡片组件示例</h1>
<div class="card-container">
<!-- 基本卡片 -->
<ConfigurableCard
width="300px"
variant="primary"
header="基本卡片"
clickable
@click="handleCardClick"
>
<p>这是一个基本卡片,支持点击事件</p>
</ConfigurableCard>
<!-- 带底部内容的卡片 -->
<ConfigurableCard
width="300px"
variant="success"
header="带底部内容的卡片"
footer="2024-01-01"
>
<p>这是一个带底部内容的卡片</p>
</ConfigurableCard>
<!-- 带关闭按钮的卡片 -->
<ConfigurableCard
width="300px"
variant="danger"
header="带关闭按钮的卡片"
closable
@close="handleClose"
>
<p>这是一个带关闭按钮的卡片</p>
</ConfigurableCard>
<!-- 使用插槽自定义内容的卡片 -->
<ConfigurableCard
width="300px"
variant="warning"
header="使用插槽的卡片"
>
<template #header>
<div style="display: flex; align-items: center; gap: 8px;">
<span style="color: #f39c12;">⚠️</span>
<span>警告卡片</span>
</div>
</template>
<p>这是一个使用插槽自定义内容的卡片</p>
<template #footer>
<div style="display: flex; justify-content: flex-end; gap: 8px;">
<button @click="handleAction('cancel')">取消</button>
<button @click="handleAction('confirm')" style="background-color: #f39c12;">确认</button>
</div>
</template>
</ConfigurableCard>
</div>
</div>
</template>
<script setup>
import ConfigurableCard from './components/ConfigurableCard.vue'
function handleCardClick() {
alert('卡片被点击了!')
}
function handleClose() {
alert('关闭按钮被点击了!')
}
function handleAction(action) {
alert(`执行了${action}操作!`)
}
</script>
<style scoped>
.app {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.card-container {
display: flex;
gap: 20px;
flex-wrap: wrap;
margin-top: 20px;
}
button {
padding: 8px 16px;
background-color: #42b883;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
</style>本集小结
在本集中,我们学习了Vue 3中通过props在组件间传递数据的方法和最佳实践:
Props的基本概念:
- Props是父组件向子组件传递数据的机制
- 支持单向数据流
- 可以进行类型验证和默认值设置
Props的基本使用:
- 子组件使用
defineProps()定义props - 父组件通过属性传递props
- 子组件使用
Props的类型验证:
- 支持多种数据类型
- 可以设置默认值
- 可以添加自定义验证
Props的传递方式:
- 静态传递和动态传递
- 传递对象和数组
- 使用
v-bind传递多个属性
Props的单向数据流:
- 数据只能从父组件流向子组件
- 子组件不能直接修改props
- 子组件通过事件通知父组件修改数据
Props的最佳实践:
- 遵循命名规范
- 确保类型安全
- 遵循单向数据流
- 优化性能
综合示例:
- 创建了一个可配置的卡片组件
- 展示了props的实际应用
- 支持多种配置选项和插槽
Props是Vue组件间通信的基础,掌握好Props的使用对于开发复杂的Vue应用至关重要。在下一集中,我们将学习Props的类型验证和默认值设置的更多细节。