第一部分: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的变化执行某些操作,可以使用watchwatchEffect来监听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的类型验证和默认值设置的更多细节。

« 上一篇 全局组件 vs 局部组件 下一篇 » Props类型验证与默认值