在昨天 Day7 的章節中,我們成功地為專案建立了現代化的開發環境。
今天,我們將正式開始享受這個環境帶來的好處,著手進行我們銷售系統的實作!
回想一下我們的 Protoype 我們在每個頁面都寫了像這樣的程式碼
<!-- 儲存按鈕 -->
<button class="save-button">儲存</button>
<!-- 刪除按鈕 -->
<button class="delete-button">刪除</button>
<!-- 一個樣式不太一樣的取消按鈕 -->
<button class="cancel-btn" style="background-color: grey;">取消</button>
每個按鈕的 class、樣式、甚至大小寫都不盡相同。
當專案變大時,要統一所有按鈕的樣式,或是一起修改它們的行為,簡直是一場災難。
元件化就是為了解決這個問題而生的。我們可以將按鈕這樣的 UI元素,抽象化成一個獨立、可複用、自成一體的「元件」。
未來在任何需要按鈕的地方,我們只需要使用這個元件即可,確保了外觀的一致性和程式碼的可維護性。
今天,我們的目標就是打造專案中的第一個元件:一個通用的 BaseButton.vue
元件。
在動手寫程式碼前,我們可以養成習慣先進行「元件設計」。
思考這個 BaseButton
元件需要具備哪些能力?
經過分析,我們需要它:
1. 內容要靈活:按鈕上的文字或圖示,應該由使用它的地方決定。
2. 外觀要多樣:需要有不同顏色來表達不同意圖(如:藍色代表主要、灰色代表次要、紅色代表
危險)。
3. 尺寸可調整:需要支援大、中、小等不同尺寸。
4. 可被禁用:需要有一個「禁用」狀態,讓它在特定條件下無法被點擊。
5. 能回報事件:當它被點擊時,需要能通知父層元件「嘿!我被點擊了!」。
以上這幾個項目,就是我們接下來實作的依據。
BaseButton.vue
的誕生首先讓我們先在 /src
建立 components
,用來存放我們的元件。
接著,在components
建立 BaseButton.vue
然後一步步將我們的需求實現。
<slot />
插槽如何讓按鈕的內容可以自訂?答案是 <slot />(插槽)
。
它就像一個佔位符,告訴元件:「請把父層塞給我的任何東西,都放在這個位置」。
<!-- BaseButton.vue -->
<template>
<button>
<slot />
</button>
</template>
如此一來,當我們這樣使用元件時:
<!-- Parent.vue -->
<BaseButton>登入</BaseButton>
<BaseButton>
<img src="/assets/google_icon.jpg" /> // 按鈕icon
使用 Google 登入
</BaseButton>
「登入」 這段文字,或是 <img>
和 使用 Google 登入,都會被精準地放置到 標籤的內部。
defineProps
接著,我們要讓按鈕的外觀和行為可以被控制。
這就要用到 props(屬性),它是父層向子層「單向傳遞資料」的橋樑。
在 <script setup>
中,我們使用 defineProps 來宣告這個元件可以接收哪些屬性。
<!-- BaseButton.vue -->
import { defineProps } from 'vue';
const props = defineProps({
variant: {
type: String,
default: 'primary', // primary, secondary, danger
},
size: {
type: String,
default: 'medium', // small, medium, large
},
disabled: {
type: Boolean,
default: false,
},
});
我們定義了 variant、size 和 disabled 三個屬性,並為它們設定了 type 和 default(預設值)。
為了讓 props 真正影響到外觀,我們需要使用 Vue 的動態 class 綁定 :class:
<!-- Parent.vue -->
<template>
<button :class="['btn', `btn-${variant}`, `btn-${size}`]">
<slot />
</button>
</template>
這段程式碼的意思是:
再搭配 <style scoped>
中對應的 CSS 規則,我們的按鈕就能千變萬化了!
defineEmits
Props 是父層「說話」給子層聽,那子層要如何「回應」父層呢?答案是 emit (發送事件)。
我們遵循「Props Down, Events Up」的黃金原則。
首先,一樣在 <script setup>
中,宣告我們的元件會發送哪些事件。
<!-- BaseButton.vue -->
import { defineEmits } from 'vue';
const emit = defineEmits(['click']);
function handleClick(event) {
// 在這裡可以先做一些判斷,例如是否被禁用
if (props.disabled) return;
// 發送 'click' 事件給父層
emit('click', event);
}
然後,將這個 handleClick 函式綁定到按鈕的原生點擊事件上:
<!-- Parent.vue -->
<template>
<button :class="['btn', `btn-${variant}`, `btn-${size}`]" @click="handleClick">
<slot />
</button>
</template>
現在,當使用者點擊這個按鈕時,它就會向父層發出 click 的信號。
父層可以用@click 來監聽這個信號,並執行相應的動作。
綜合以上所有部分,我們最終的 BaseButton.vue 程式碼如下:
<template>
<button
@click="handleClick"
:disabled="disabled"
:class="['btn', `btn-${variant}`, `btn-${size}`]"
>
<slot />
</button>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue'
const props = defineProps({
variant: {
type: String,
default: 'primary', // primary, secondary, danger
},
size: {
type: String,
default: 'medium', // small, medium, large
},
disabled: {
type: Boolean,
default: false,
},
})
const emit = defineEmits(['click'])
function handleClick(event) {
if (props.disabled) return
emit('click', event)
}
</script>
<style scoped>
//...
</style>
我們立刻將這第一個元件應用到我們的專案中!
打開 src/view/LoginView.vue,我們將原本的<button>
替換成 <BaseButton>
:
<!-- LoginView.vue -->
<BaseButton type="button"
variant="white"
class="btn-login"
@click="googleLogin"
:disabled="isLoading"
>
<svg ... </svg>
{{ isLoading ? '登入中...' : '使用 Google 登入' }}
</BaseButton>
<script setup>
import { ref } from 'vue'
import BaseButton from '@/components/BaseButton.vue'
const isLoading = ref(false)
async function googleLogin() { ... }
</script>
<style scoped> ... </style>
如此一來,我們不需要在 LoginView 中關心按鈕的樣式細節,這就是元件化的力量。
今天,我們從零到一,打造了可複用且易於維護的 BaseButton.vue
元件。
我們學習並實踐了 Vue 元件化開發的三大核心:
<slot />
:讓元件內容保持高度彈性。props
:實現由父到子的單向資料傳遞,客製化元件的外觀與行為。emit
:實現由子到父的事件通知,讓元件可以回應使用者的互動。明天,Day 9:[Componentの呼吸・貳之型] 測試Button - 學習Component Testing
我們將繼續沿用這個模式,說明我們如何測試我們打造的元件 。心を燃やせ 🔥!