Vue Props 屬性傳遞

Props 是 Vue 元件之間傳遞資料的主要方式。父元件可以透過 props 將資料傳遞給子元件,實現元件之間的通訊。

定義 Props

<script setup> 中,使用 defineProps() 來定義元件接收的 props:

陣列語法

最簡單的方式是使用字串陣列:

<script setup>
const props = defineProps(['title', 'author', 'likes'])
</script>

<template>
  <h2>{{ title }}</h2>
  <p>作者:{{ author }}</p>
  <p>按讚數:{{ likes }}</p>
</template>

物件語法(推薦)

使用物件語法可以為每個 prop 指定型別:

<script setup>
const props = defineProps({
  title: String,
  author: String,
  likes: Number,
  isPublished: Boolean
})
</script>

傳遞 Props

靜態 Props

<template>
  <BlogPost title="Vue 入門教學" author="小明" />
</template>

動態 Props

使用 v-bind(或簡寫 :)傳遞動態值:

<script setup>
import { ref } from 'vue'

const post = ref({
  title: 'Vue 入門教學',
  author: '小明',
  likes: 42
})
</script>

<template>
  <BlogPost
    :title="post.title"
    :author="post.author"
    :likes="post.likes"
  />
</template>

傳遞不同型別的值

<script setup>
import { ref } from 'vue'

const count = ref(100)
const isActive = ref(true)
const tags = ref(['vue', 'javascript'])
const author = ref({ name: '小明', email: 'ming@example.com' })
</script>

<template>
  <!-- 傳遞數字 -->
  <MyComponent :count="count" />
  <MyComponent :count="42" />

  <!-- 傳遞布林值 -->
  <MyComponent :is-active="isActive" />
  <MyComponent is-active />  <!-- 等同於 :is-active="true" -->
  <MyComponent :is-active="false" />

  <!-- 傳遞陣列 -->
  <MyComponent :tags="tags" />
  <MyComponent :tags="['vue', 'react']" />

  <!-- 傳遞物件 -->
  <MyComponent :author="author" />
  <MyComponent :author="{ name: '小華' }" />
</template>

一次傳遞所有屬性

如果你想把物件的所有屬性當作 props 傳入,可以使用不帶參數的 v-bind

<script setup>
import { reactive } from 'vue'

const post = reactive({
  id: 1,
  title: 'Vue 入門',
  author: '小明'
})
</script>

<template>
  <!-- 等同於 <BlogPost :id="post.id" :title="post.title" :author="post.author" /> -->
  <BlogPost v-bind="post" />
</template>

Props 驗證

使用物件語法可以對 props 進行更詳細的驗證:

<script setup>
const props = defineProps({
  // 基本型別檢查(null 和 undefined 會通過任何型別驗證)
  propA: Number,

  // 多種可能的型別
  propB: [String, Number],

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

  // 有預設值的數字
  propD: {
    type: Number,
    default: 100
  },

  // 有預設值的物件
  propE: {
    type: Object,
    // 物件或陣列的預設值必須從工廠函式返回
    default() {
      return { message: 'hello' }
    }
  },

  // 自訂驗證函式
  propF: {
    validator(value) {
      // 值必須是以下字串之一
      return ['success', 'warning', 'danger'].includes(value)
    }
  },

  // 有預設值的函式
  propG: {
    type: Function,
    // 不像物件或陣列,這不是工廠函式
    // 這就是預設值本身
    default() {
      return 'Default function'
    }
  }
})
</script>

型別檢查

type 可以是以下原生建構函式:

  • String
  • Number
  • Boolean
  • Array
  • Object
  • Date
  • Function
  • Symbol

也可以是自訂的類別或建構函式:

<script setup>
class Person {
  constructor(firstName, lastName) {
    this.firstName = firstName
    this.lastName = lastName
  }
}

const props = defineProps({
  author: Person
})
</script>

單向資料流

Props 形成了單向資料流:父元件的更新會向下流動到子元件,但反過來則不行。這是為了防止子元件意外修改父元件的狀態。

不要在子元件中直接修改 props! Vue 會在控制台發出警告。

想要修改 prop 的情況

情況一:prop 用來傳遞初始值,之後子元件想要將其作為本地資料使用

<script setup>
import { ref } from 'vue'

const props = defineProps(['initialCounter'])

// 用 prop 的值初始化一個本地 ref
const counter = ref(props.initialCounter)
// 之後修改 counter 不會影響父元件
</script>

情況二:需要對 prop 進行轉換

<script setup>
import { computed } from 'vue'

const props = defineProps(['size'])

// 使用 computed 進行轉換
const normalizedSize = computed(() => props.size.trim().toLowerCase())
</script>

Props 命名慣例

在元件定義中使用 camelCase

<!-- 子元件 -->
<script setup>
defineProps({
  greetingMessage: String
})
</script>

在模板中使用 kebab-case

<!-- 父元件 -->
<template>
  <!-- 在 HTML 中使用 kebab-case -->
  <MyComponent greeting-message="hello" />
</template>

Vue 會自動轉換,所以 greetingMessagegreeting-message 是對應的。

使用 TypeScript 定義 Props

如果你使用 TypeScript,可以使用純型別聲明:

<script setup lang="ts">
// 使用型別
interface Props {
  title: string
  author?: string  // 可選
  likes?: number
}

// 方式一:使用泛型
const props = defineProps<Props>()

// 方式二:使用泛型並指定預設值
const props = withDefaults(defineProps<Props>(), {
  author: '匿名',
  likes: 0
})
</script>

更多 TypeScript 的用法請參考 Vue + TypeScript

Boolean Props 的特殊行為

布林型別的 props 有特殊的轉換規則:

<!-- 子元件 -->
<script setup>
defineProps({
  disabled: Boolean
})
</script>
<!-- 父元件 -->
<template>
  <!-- 等同於 :disabled="true" -->
  <MyComponent disabled />

  <!-- 等同於 :disabled="false" -->
  <MyComponent />
</template>

當 prop 可以是多種型別時:

<script setup>
// disabled prop 可以接受空字串或布林值
defineProps({
  disabled: [Boolean, String]
})
</script>
<template>
  <!-- 等同於 :disabled="true" -->
  <MyComponent disabled />

  <!-- 等同於 :disabled="''" (空字串) -->
  <MyComponent disabled="" />
</template>

存取 Props

<script setup> 中,defineProps() 返回的物件可以用來存取 props:

<script setup>
const props = defineProps(['title'])

// 在 JavaScript 中使用
console.log(props.title)
</script>

<template>
  <!-- 在模板中可以直接使用,不需要 props 前綴 -->
  <h1>{{ title }}</h1>
</template>

實際範例

文章卡片元件

<!-- ArticleCard.vue -->
<script setup>
defineProps({
  title: {
    type: String,
    required: true
  },
  excerpt: {
    type: String,
    default: ''
  },
  author: {
    type: Object,
    default: () => ({ name: '匿名' })
  },
  publishedAt: {
    type: Date,
    default: () => new Date()
  },
  tags: {
    type: Array,
    default: () => []
  },
  featured: {
    type: Boolean,
    default: false
  }
})
</script>

<template>
  <article :class="{ featured }">
    <h2>{{ title }}</h2>
    <p class="excerpt">{{ excerpt }}</p>
    <div class="meta">
      <span class="author">{{ author.name }}</span>
      <time :datetime="publishedAt.toISOString()">
        {{ publishedAt.toLocaleDateString() }}
      </time>
    </div>
    <div class="tags" v-if="tags.length">
      <span v-for="tag in tags" :key="tag" class="tag">{{ tag }}</span>
    </div>
  </article>
</template>

<style scoped>
article {
  border: 1px solid #ddd;
  padding: 20px;
  border-radius: 8px;
}

article.featured {
  border-color: #42b883;
  background-color: #f0fff4;
}

.excerpt {
  color: #666;
}

.meta {
  font-size: 14px;
  color: #999;
}

.tag {
  background-color: #eee;
  padding: 2px 8px;
  border-radius: 4px;
  margin-right: 5px;
}
</style>

使用這個元件:

<script setup>
import ArticleCard from './ArticleCard.vue'

const article = {
  title: 'Vue 3 完整教學',
  excerpt: '從基礎到進階,一步步學會 Vue 3...',
  author: { name: '小明', email: 'ming@example.com' },
  publishedAt: new Date('2024-01-15'),
  tags: ['Vue', 'JavaScript', '前端'],
  featured: true
}
</script>

<template>
  <ArticleCard
    :title="article.title"
    :excerpt="article.excerpt"
    :author="article.author"
    :published-at="article.publishedAt"
    :tags="article.tags"
    featured
  />

  <!-- 或使用 v-bind 一次傳入 -->
  <ArticleCard v-bind="article" />
</template>