Vue.js组件 Props

本章重点:Props是组件之间通信的桥梁。掌握Props的类型验证、默认值和自定义验证,是构建健壮组件的基础。

什么是Props?

Props(Properties)是父组件向子组件传递数据的方式。子组件通过props选项声明它可以接收的数据。

父组件
传递数据
Props
数据通道
子组件
接收数据
基本使用示例

<!-- 父组件 -->
<template>
  <child-component
    title="欢迎学习Vue.js"
    :count="totalCount"
    :is-active="true"
    :items="itemList"
  />
</template>

<script>
export default {
  data() {
    return {
      totalCount: 42,
      itemList: ['Vue', 'React', 'Angular']
    }
  }
}
</script>
                            

// 子组件
export default {
  // 声明props
  props: {
    title: String,
    count: Number,
    isActive: Boolean,
    items: Array
  },
  template: `
    <div>
      <h2>{{ title }}</h2>
      <p>计数: {{ count }}</p>
      <p v-if="isActive">状态: 激活</p>
      <ul>
        <li v-for="item in items" :key="item">{{ item }}</li>
      </ul>
    </div>
  `
}
                            

Props类型

Vue支持多种Prop类型,可以提供类型检查:

类型 语法示例 描述 验证示例
String title: String 字符串类型 "Hello World"
Number age: Number 数字类型 25, 3.14
Boolean isActive: Boolean 布尔类型 true, false
Array items: Array 数组类型 [1, 2, 3], ["a", "b"]
Object user: Object 对象类型 {name: "John", age: 30}
Function callback: Function 函数类型 () => console.log("hi")
Symbol key: Symbol Symbol类型 Symbol('id')
多种类型: prop: [String, Number] - 可以是字符串或数字

Props验证

通过详细的验证配置,可以确保组件接收正确的数据:

类型检查

验证数据类型

必需性

是否必须传递

默认值

未传递时的默认值


props: {
  // 基础的类型检查 (`null` 和 `undefined` 会通过任何类型验证)
  propA: Number,

  // 多个可能的类型
  propB: [String, Number],

  // 必填的字符串
  propC: {
    type: String,
    required: true
  },

  // 带有默认值的数字
  propD: {
    type: Number,
    default: 100
  },

  // 带有默认值的对象
  propE: {
    type: Object,
    // 对象或数组默认值必须从一个工厂函数返回
    default: function() {
      return { message: 'hello' }
    }
  },

  // 自定义验证函数
  propF: {
    validator: function(value) {
      // 这个值必须匹配下列字符串中的一个
      return ['success', 'warning', 'danger'].indexOf(value) !== -1
    }
  },

  // 带有默认值的函数
  propG: {
    type: Function,
    // 与对象或数组默认值不同,这不是一个工厂函数
    default: function() {
      return 'Default function'
    }
  }
}
                        

高级Props配置

必需Prop

props: {
  username: {
    type: String,
    required: true  // 必须传递此prop
  }
}
                                

使用场景:关键数据,没有默认值的情况

默认值

props: {
  // 基本类型默认值
  size: {
    type: String,
    default: 'medium'
  },

  // 对象/数组默认值必须是工厂函数
  config: {
    type: Object,
    default: () => ({
      color: 'blue',
      size: 'md'
    })
  }
}
                                

注意:对象和数组必须使用工厂函数

自定义验证器

props: {
  status: {
    type: String,
    validator: function(value) {
      // 必须是指定的值之一
      return ['pending', 'active', 'archived'].includes(value)
    },
    default: 'pending'
  },

  // 更复杂的验证
  score: {
    type: Number,
    validator: function(value) {
      return value >= 0 && value <= 100
    }
  }
}
                                

用途:限制输入值的范围或格式

Props绑定方式

Props可以通过不同方式从父组件传递:


<!-- 静态Prop(字符串) -->
<my-component title="静态标题"></my-component>

<!-- 静态Prop(数字) -->
<my-component :count="42"></my-component>

<!-- 静态Prop(布尔值) -->
<my-component :is-visible="true"></my-component>
<my-component is-visible></my-component> <!-- 等价于 :is-visible="true" -->
                                

<template>
  <my-component
    :title="dynamicTitle"
    :count="userCount"
    :is-active="isUserActive"
  />
</template>

<script>
export default {
  data() {
    return {
      dynamicTitle: '动态标题',
      userCount: 100,
      isUserActive: true
    }
  }
}
</script>
                                

<!-- 传递整个对象 -->
<user-profile :user="userObject"></user-profile>

<!-- 等价于: -->
<user-profile
  :name="userObject.name"
  :age="userObject.age"
  :email="userObject.email"
></user-profile>
                                

<!-- 布尔Prop的特殊用法 -->
<!-- 以下三种写法是等价的 -->
<my-modal visible></my-modal>
<my-modal :visible="true"></my-modal>
<my-modal visible="true"></my-modal>

<!-- 传递false必须使用动态绑定 -->
<my-modal :visible="false"></my-modal>

<!-- 以下写法会传递字符串"false"而不是布尔值false -->
<!-- ❌ 错误写法 -->
<my-modal visible="false"></my-modal>
                                
重要提示:
  • 当传递布尔Prop时,如果使用属性语法(不带:),Vue会将其解析为true
  • 要传递false,必须使用:prop-name="false"语法
  • 对象和数组的默认值必须使用工厂函数返回
  • Props是单向数据流,不要在子组件中直接修改Prop的值

命名约定

Vue遵循特定的命名约定来处理Props:


// 在JavaScript中使用camelCase(驼峰命名法)
props: {
  greetingText: String,
  userName: String,
  isUserActive: Boolean
}

// 在HTML中使用kebab-case(短横线分隔命名法)
<!-- 父组件模板 -->
<my-component
  greeting-text="Hello"
  user-name="John"
  :is-user-active="true"
></my-component>
                        
最佳实践:
  • 在JavaScript中使用camelCase声明Props
  • 在HTML模板中使用kebab-case传递Props
  • 使用描述性的Prop名称,避免缩写
  • 为所有非必需Props提供默认值
  • 使用详细的对象语法进行Props验证

完整示例:用户卡片组件

创建一个带有完整Props验证的用户卡片组件:


// UserCard.vue - 子组件
export default {
  name: 'UserCard',

  props: {
    // 必需的用户信息
    user: {
      type: Object,
      required: true,
      validator: function(value) {
        // 验证user对象是否包含必需字段
        return value &&
               typeof value.name === 'string' &&
               typeof value.email === 'string'
      }
    },

    // 可选的显示选项
    showAvatar: {
      type: Boolean,
      default: true
    },

    avatarSize: {
      type: String,
      default: 'medium',
      validator: function(value) {
        return ['small', 'medium', 'large'].includes(value)
      }
    },

    showActions: {
      type: Boolean,
      default: false
    },

    status: {
      type: String,
      default: 'active',
      validator: function(value) {
        return ['active', 'inactive', 'pending'].includes(value)
      }
    },

    // 数字验证
    rating: {
      type: Number,
      default: 0,
      validator: function(value) {
        return value >= 0 && value <= 5
      }
    },

    // 自定义类名
    customClass: {
      type: String,
      default: ''
    },

    // 回调函数
    onEdit: {
      type: Function,
      default: () => {}
    },

    onDelete: {
      type: Function,
      default: () => {}
    }
  },

  computed: {
    // 根据status计算样式类
    statusClass() {
      return {
        'active': this.status === 'active',
        'inactive': this.status === 'inactive',
        'pending': this.status === 'pending'
      }
    },

    // 根据avatarSize计算尺寸
    avatarSizeClass() {
      return `avatar-${this.avatarSize}`
    }
  },

  methods: {
    handleEdit() {
      this.onEdit(this.user.id)
    },

    handleDelete() {
      this.onDelete(this.user.id)
    }
  },

  template: `
    <div class="user-card" :class="[customClass, statusClass]">
      <div class="user-header">
        <div v-if="showAvatar" class="user-avatar" :class="avatarSizeClass">
          {{ user.name.charAt(0).toUpperCase() }}
        </div>
        <div class="user-info">
          <h3 class="user-name">{{ user.name }}</h3>
          <p class="user-email">{{ user.email }}</p>
          <div class="user-rating">
            <span v-for="i in 5" :key="i"
                  :class="i <= rating ? 'star-filled' : 'star-empty'">
              ★
            </span>
          </div>
        </div>
      </div>

      <div v-if="showActions" class="user-actions">
        <button @click="handleEdit" class="btn-edit">
          编辑
        </button>
        <button @click="handleDelete" class="btn-delete">
          删除
        </button>
      </div>
    </div>
  `
}
                            

<!-- 父组件使用示例 -->
<template>
  <div>
    <!-- 使用默认值 -->
    <user-card :user="currentUser" />

    <!-- 自定义所有选项 -->
    <user-card
      :user="currentUser"
      :show-avatar="false"
      avatar-size="large"
      :show-actions="true"
      status="active"
      :rating="4"
      custom-class="highlight-card"
      :on-edit="handleEditUser"
      :on-delete="handleDeleteUser"
    />

    <!-- 使用对象绑定 -->
    <user-card :user="{
      name: '张三',
      email: 'zhangsan@example.com'
    }" />
  </div>
</template>

<script>
import UserCard from './UserCard.vue'

export default {
  components: {
    UserCard
  },

  data() {
    return {
      currentUser: {
        id: 1,
        name: '李四',
        email: 'lisi@example.com'
      }
    }
  },

  methods: {
    handleEditUser(userId) {
      console.log('编辑用户:', userId)
    },

    handleDeleteUser(userId) {
      console.log('删除用户:', userId)
    }
  }
}
</script>
                            

常见模式

.sync修饰符

<!-- 父组件 -->
<child-component :title.sync="pageTitle"></child-component>

<!-- 等价于 -->
<child-component
  :title="pageTitle"
  @update:title="pageTitle = $event"
></child-component>

<!-- 子组件 -->
<button @click="$emit('update:title', newTitle)">
  更新标题
</button>
                                
传递所有Props

<!-- 使用v-bind传递对象的所有属性 -->
<user-card v-bind="userData"></user-card>

<!-- 等价于 -->
<user-card
  :name="userData.name"
  :age="userData.age"
  :email="userData.email"
></user-card>
                                
禁止修改Props

// ❌ 错误做法:直接修改Prop
this.title = '新标题'

// ✅ 正确做法:使用本地数据
data() {
  return {
    localTitle: this.title
  }
}

// ✅ 正确做法:触发事件通知父组件
this.$emit('update-title', '新标题')

// ✅ 正确做法:使用计算属性
computed: {
  formattedTitle() {
    return this.title.toUpperCase()
  }
}
                                

Props检查清单

Props练习

尝试为以下组件定义Props:


// 为这个产品卡片组件定义合适的Props
Vue.component('ProductCard', {
  props: {
    // TODO: 在这里定义Props
    // 应该包括:产品信息、价格、图片、状态等
  },

  template: `
    <div class="product-card">
      <img :src="image" :alt="name">
      <h3>{{ name }}</h3>
      <p>{{ description }}</p>
      <div class="price">¥{{ price }}</div>
      <div v-if="inStock" class="in-stock">有货</div>
      <div v-else class="out-of-stock">缺货</div>
    </div>
  `
});
                            

提示:考虑必需性、类型验证、默认值等。