☕ NEW! 完成新手任務即可參加抽獎!LINE 星巴克禮券等你拿,名額有限!        🎉 推廣活動:邀請好友註冊 DevLearn,累積推薦抽 LINE 星巴克禮券! 活動詳情 →        🔥 活動期間 2026/4/1 - 5/31 |已有 0 人參加       
vue 中級

🧩 Vue 元件系統:Props、Emit 與插槽

📌 為什麼要拆元件?

元件就像樂高積木——把大型 UI 拆成可重複使用的小塊。

App.vue
├── Header.vue          (導航列)
├── Sidebar.vue         (側邊欄)
├── MainContent.vue     (主要內容)
│   ├── ArticleCard.vue (文章卡片 × N)
│   └── Pagination.vue  (分頁)
└── Footer.vue          (頁尾)

不拆元件 vs 拆元件

<!-- ❌ 不拆元件:全部寫在一個檔案,500+ 行 -->
<template>
  <div>
    <nav>...</nav>         <!-- 50 行 -->
    <aside>...</aside>     <!-- 80 行 -->
    <main>...</main>       <!-- 200 行 -->
    <footer>...</footer>   <!-- 30 行 -->
  </div>
</template>

<!-- ✅ 拆元件:每個檔案只負責一個功能 -->
<template>
  <div>
    <AppHeader />
    <AppSidebar />
    <MainContent />
    <AppFooter />
  </div>
</template>

📌 defineProps — 父傳子

父元件透過屬性 (props) 把資料傳給子元件。

子元件:UserCard.vue

<template>
  <div class="card">
    <h3>{{ name }}</h3>
    <p>年齡:{{ age }}</p>
    <p v-if="email">信箱:{{ email }}</p>
    <span :class="['badge', levelClass]">{{ level }}</span>
  </div>
</template>

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

// defineProps 是 Vue 的編譯器巨集 (compiler macro)
// 不需要 import!(這也不是 JS 原生語法)
const props = defineProps({
  name: {
    type: String,
    required: true
  },
  age: {
    type: Number,
    default: 0
  },
  email: {
    type: String,
    default: ''
  },
  level: {
    type: String,
    default: 'beginner',
    validator: (value) => ['beginner', 'intermediate', 'advanced'].includes(value)
  }
})

const levelClass = computed(() => {
  return {
    beginner: 'bg-green',
    intermediate: 'bg-blue',
    advanced: 'bg-red'
  }[props.level]
})
</script>

父元件使用

<template>
  <div>
    <!-- 靜態 props -->
    <UserCard name="小明" :age="25" email="ming@example.com" />

    <!-- 動態 props -->
    <UserCard
      v-for="user in users"
      :key="user.id"
      :name="user.name"
      :age="user.age"
      :level="user.level"
    />
  </div>
</template>

<script setup>
import { ref } from 'vue'
import UserCard from './UserCard.vue'

const users = ref([
  { id: 1, name: '小明', age: 25, level: 'beginner' },
  { id: 2, name: '小華', age: 30, level: 'intermediate' },
  { id: 3, name: '小美', age: 28, level: 'advanced' }
])
</script>

📌 defineEmits — 子傳父

子元件透過事件 (emit) 把資料傳回父元件。

子元件:SearchBox.vue

<template>
  <div class="search-box">
    <input
      :value="modelValue"
      @input="$emit('update:modelValue', $event.target.value)"
      placeholder="搜尋..."
    />
    <button @click="handleSearch">搜尋</button>
    <button @click="$emit('clear')">清除</button>
  </div>
</template>

<script setup>
// defineEmits 也是 Vue 編譯器巨集
const emit = defineEmits(['update:modelValue', 'search', 'clear'])

const props = defineProps({
  modelValue: { type: String, default: '' }
})

function handleSearch() {
  // 觸發自訂事件,把搜尋關鍵字傳給父元件
  emit('search', props.modelValue)
}
</script>

父元件使用

<template>
  <div>
    <!-- v-model 是 :modelValue + @update:modelValue 的語法糖 -->
    <SearchBox
      v-model="keyword"
      @search="onSearch"
      @clear="keyword = ''"
    />
    <p>目前關鍵字:{{ keyword }}</p>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import SearchBox from './SearchBox.vue'

const keyword = ref('')

function onSearch(query) {
  console.log('搜尋:', query)
  // 呼叫 API...
}
</script>

📌 Slot 插槽 — 內容分發

插槽讓父元件可以「塞內容」到子元件裡。

基本插槽

<!-- Card.vue(子元件) -->
<template>
  <div class="card">
    <div class="card-header">
      <slot name="header">預設標題</slot>
    </div>
    <div class="card-body">
      <slot>預設內容</slot>
    </div>
    <div class="card-footer">
      <slot name="footer"></slot>
    </div>
  </div>
</template>

<!-- 父元件使用 -->
<template>
  <Card>
    <template #header>
      <h2>自訂標題</h2>
    </template>

    <!-- 預設插槽 -->
    <p>這是卡片的主要內容</p>

    <template #footer>
      <button>確認</button>
      <button>取消</button>
    </template>
  </Card>
</template>

作用域插槽 (Scoped Slots)

<!-- DataList.vue(子元件) -->
<template>
  <ul>
    <li v-for="item in items" :key="item.id">
      <!-- 把 item 傳給父元件的插槽 -->
      <slot :item="item" :index="items.indexOf(item)">
        {{ item.name }}
      </slot>
    </li>
  </ul>
</template>

<script setup>
defineProps({
  items: { type: Array, required: true }
})
</script>

<!-- 父元件使用 -->
<template>
  <DataList :items="products">
    <!-- 透過 v-slot 接收子元件傳來的資料 -->
    <template #default="{ item, index }">
      <span>{{ index + 1 }}. {{ item.name }} — ${{ item.price }}</span>
    </template>
  </DataList>
</template>

📌 動態元件 & keep-alive

<template>
  <div>
    <!-- 頁籤切換 -->
    <button
      v-for="tab in tabs"
      :key="tab"
      @click="currentTab = tab"
      :class="{ active: currentTab === tab }"
    >
      {{ tab }}
    </button>

    <!-- component :is 動態切換元件 -->
    <!-- keep-alive 保留元件狀態(不會被銷毀) -->
    <keep-alive>
      <component :is="tabComponents[currentTab]" />
    </keep-alive>
  </div>
</template>

<script setup>
import { ref, shallowRef } from 'vue'
import TabHome from './TabHome.vue'
import TabProfile from './TabProfile.vue'
import TabSettings from './TabSettings.vue'

const tabs = ['首頁', '個人檔案', '設定']
const currentTab = ref('首頁')

const tabComponents = {
  '首頁': TabHome,
  '個人檔案': TabProfile,
  '設定': TabSettings
}
</script>

📌 實作範例:可重用的表單元件

<!-- FormInput.vue -->
<template>
  <div class="form-group">
    <label :for="id">{{ label }}</label>
    <input
      :id="id"
      :type="type"
      :value="modelValue"
      :placeholder="placeholder"
      @input="$emit('update:modelValue', $event.target.value)"
      :class="{ error: errorMessage }"
    />
    <p v-if="errorMessage" class="error-text">{{ errorMessage }}</p>
  </div>
</template>

<script setup>
defineProps({
  modelValue: { type: String, default: '' },
  label: { type: String, required: true },
  type: { type: String, default: 'text' },
  placeholder: { type: String, default: '' },
  id: { type: String, default: () => `input-${Math.random().toString(36).slice(2)}` },
  errorMessage: { type: String, default: '' }
})

defineEmits(['update:modelValue'])
</script>

<!-- 使用 -->
<template>
  <form @submit.prevent="submitForm">
    <FormInput v-model="form.name" label="姓名" placeholder="請輸入姓名" />
    <FormInput v-model="form.email" label="Email" type="email"
               :error-message="errors.email" />
    <FormInput v-model="form.password" label="密碼" type="password" />
    <button type="submit">送出</button>
  </form>
</template>

💡 常見陷阱

  • ❌ 直接修改 props → 改用 emit 通知父元件
  • ❌ 忘記加 :key → 動態列表一定要有唯一 key
  • ❌ 把所有東西放同一個元件 → 超過 200 行就該考慮拆分
  • definePropsdefineEmits 不需要 import(編譯器巨集)

💡 大家的想法 · 0

載入中...
💬 即時聊天室 🟢 0 人在線
😀 😎 🤓 💻 🎮 🎸 🔥
➕ 新問題
📋 我的工單
💬 LINE 社群
🔒
需要註冊才能使用此功能
註冊帳號即可解鎖測驗、遊戲、簽到、筆記下載等所有功能,完全免費!
免費註冊