Vue 元件基礎

元件(Component)是 Vue 最強大的功能之一。元件允許我們將 UI 分割成獨立、可重複使用的小單位,並且可以對每個單位進行獨立的思考和開發。

什麼是元件?

在 Vue 中,一個元件本質上是一個擁有預定義選項的 Vue 實例。你可以把元件想像成自訂的 HTML 元素,可以在模板中重複使用。

例如,你可能會有這樣的元件結構:

App
├── Header
├── Sidebar
│   ├── MenuItem
│   └── MenuItem
├── Content
│   ├── Article
│   └── Article
└── Footer

單文件元件(Single-File Components, SFC)

在使用建構工具的 Vue 專案中,我們通常使用一種特殊的檔案格式 .vue,稱為單文件元件(SFC)。

一個 .vue 檔案包含三個部分:

<!-- MyComponent.vue -->
<script setup>
// JavaScript 邏輯
import { ref } from 'vue'

const count = ref(0)
</script>

<template>
  <!-- HTML 模板 -->
  <button @click="count++">
    點擊次數:{{ count }}
  </button>
</template>

<style scoped>
/* CSS 樣式 */
button {
  font-size: 16px;
  padding: 10px 20px;
}
</style>

SFC 的優點

  • 關注點分離:模板、邏輯、樣式分開,但又在同一個檔案中
  • 作用域樣式scoped 屬性讓樣式只作用於當前元件
  • 建構工具優化:可以使用預處理器(Sass、TypeScript 等)
  • 熱更新:開發時修改會立即反映在瀏覽器中

定義元件

使用 <script setup>(推薦)

<script setup> 是 Vue 3.2+ 推薦的寫法,更簡潔且效能更好:

<!-- ButtonCounter.vue -->
<script setup>
import { ref } from 'vue'

const count = ref(0)
</script>

<template>
  <button @click="count++">點擊了 {{ count }} 次</button>
</template>

<script setup> 中,頂層的變數、函式、import 的內容都會自動暴露給模板使用,不需要額外的 return

不使用 <script setup>

如果需要更多控制,也可以使用傳統的 setup() 函式:

<script>
import { ref } from 'vue'

export default {
  setup() {
    const count = ref(0)

    function increment() {
      count.value++
    }

    // 需要 return 才能在模板中使用
    return {
      count,
      increment
    }
  }
}
</script>

<template>
  <button @click="increment">點擊了 {{ count }} 次</button>
</template>

使用元件

要使用一個元件,首先需要引入它,然後在模板中使用:

<!-- App.vue -->
<script setup>
import ButtonCounter from './components/ButtonCounter.vue'
</script>

<template>
  <h1>這是我的 App</h1>

  <!-- 使用元件 -->
  <ButtonCounter />

  <!-- 可以多次使用同一個元件 -->
  <ButtonCounter />
  <ButtonCounter />
</template>

每個元件實例都有自己獨立的狀態。當你點擊其中一個按鈕時,只有那個按鈕的計數會增加。

元件命名慣例

Vue 元件有兩種命名風格:

  1. PascalCase(推薦):<ButtonCounter />
  2. kebab-case<button-counter />

在 SFC 中,推薦使用 PascalCase,因為:

  • 可以和原生 HTML 元素區分開來
  • 與 JavaScript 類別的命名慣例一致

元件註冊

局部註冊(推薦)

<script setup> 中,引入的元件可以直接在模板中使用,這稱為局部註冊

<script setup>
import ComponentA from './ComponentA.vue'
import ComponentB from './ComponentB.vue'
</script>

<template>
  <ComponentA />
  <ComponentB />
</template>

局部註冊的好處:

  • Tree-shaking:未使用的元件不會被打包
  • 依賴關係明確:可以清楚知道元件從哪裡來

全域註冊

如果某個元件需要在多個地方使用,可以全域註冊:

// main.js
import { createApp } from 'vue'
import App from './App.vue'
import GlobalButton from './components/GlobalButton.vue'

const app = createApp(App)

// 全域註冊
app.component('GlobalButton', GlobalButton)

app.mount('#app')

全域註冊後,可以在任何元件的模板中使用,不需要額外引入:

<template>
  <!-- 任何元件中都可以直接使用 -->
  <GlobalButton />
</template>
全域註冊會讓所有元件都被打包,即使沒有使用,因此建議優先使用局部註冊。

傳遞 Props

元件可以接收外部傳入的資料,稱為 props

<!-- BlogPost.vue -->
<script setup>
// 定義接收的 props
defineProps(['title', 'author'])
</script>

<template>
  <article>
    <h2>{{ title }}</h2>
    <p>作者:{{ author }}</p>
  </article>
</template>

父元件傳遞 props:

<!-- 父元件 -->
<script setup>
import BlogPost from './BlogPost.vue'
</script>

<template>
  <BlogPost title="Vue 入門教學" author="小明" />
  <BlogPost title="Composition API 詳解" author="小華" />
</template>

更多 Props 的用法請參考 Props 屬性傳遞

監聽事件

子元件可以向父元件發送事件:

<!-- BlogPost.vue -->
<script setup>
defineProps(['title'])

// 定義發送的事件
const emit = defineEmits(['enlarge-text'])

function handleClick() {
  emit('enlarge-text')
}
</script>

<template>
  <article>
    <h2>{{ title }}</h2>
    <button @click="handleClick">放大字體</button>
  </article>
</template>

父元件監聽事件:

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

const fontSize = ref(16)
</script>

<template>
  <div :style="{ fontSize: fontSize + 'px' }">
    <BlogPost
      title="Vue 入門"
      @enlarge-text="fontSize += 2"
    />
  </div>
</template>

更多事件的用法請參考 事件處理與 Emit

插槽(Slots)

插槽讓父元件可以傳遞模板內容給子元件:

<!-- AlertBox.vue -->
<template>
  <div class="alert-box">
    <strong>注意!</strong>
    <slot></slot>
  </div>
</template>

<style scoped>
.alert-box {
  padding: 15px;
  background-color: #f8d7da;
  border: 1px solid #f5c6cb;
  border-radius: 4px;
}
</style>

使用插槽:

<template>
  <AlertBox>
    這是一則重要的警告訊息!
  </AlertBox>
</template>

渲染結果:

<div class="alert-box">
  <strong>注意!</strong>
  這是一則重要的警告訊息!
</div>

更多插槽的用法請參考 Slots 插槽

動態元件

有時候我們需要在多個元件之間動態切換,可以使用 <component> 元素和 :is 屬性:

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

const tabs = {
  home: TabHome,
  posts: TabPosts,
  archive: TabArchive
}

const currentTab = ref('home')
</script>

<template>
  <div class="tabs">
    <button
      v-for="(_, tab) in tabs"
      :key="tab"
      :class="{ active: currentTab === tab }"
      @click="currentTab = tab"
    >
      {{ tab }}
    </button>
  </div>

  <!-- 動態元件 -->
  <component :is="tabs[currentTab]"></component>
</template>

更多動態元件的用法請參考 動態元件

元件的組織結構建議

一個典型的 Vue 專案元件結構:

src/
├── components/          # 可重複使用的通用元件
│   ├── ui/              # UI 基礎元件
│   │   ├── BaseButton.vue
│   │   ├── BaseInput.vue
│   │   └── BaseCard.vue
│   ├── layout/          # 佈局元件
│   │   ├── TheHeader.vue
│   │   ├── TheSidebar.vue
│   │   └── TheFooter.vue
│   └── feature/         # 功能性元件
│       ├── UserProfile.vue
│       └── SearchBar.vue
├── views/               # 頁面級元件(配合 Vue Router)
│   ├── HomeView.vue
│   ├── AboutView.vue
│   └── UserView.vue
└── App.vue              # 根元件

元件命名建議

  • 基礎元件:以 BaseAppV 開頭,如 BaseButtonAppTable
  • 單例元件:以 The 開頭,如 TheHeaderTheSidebar
  • 緊密耦合:以父元件名為前綴,如 TodoListTodoListItem

元件的 CSS

Scoped CSS

使用 scoped 屬性讓樣式只作用於當前元件:

<style scoped>
/* 只會影響這個元件內的 .title */
.title {
  color: red;
}
</style>

深度選擇器

如果需要影響子元件的樣式,可以使用 :deep() 選擇器:

<style scoped>
/* 影響子元件內的 .child-class */
:deep(.child-class) {
  color: blue;
}
</style>

CSS Modules

Vue 也支援 CSS Modules:

<template>
  <p :class="$style.red">這是紅色文字</p>
</template>

<style module>
.red {
  color: red;
}
</style>

使用預處理器

可以在 <style> 標籤指定預處理器:

<style lang="scss" scoped>
$primary-color: #42b883;

.button {
  background-color: $primary-color;

  &:hover {
    background-color: darken($primary-color, 10%);
  }
}
</style>
使用預處理器需要先安裝對應的套件,例如 npm install -D sass