iT邦幫忙

2025 iThome 鐵人賽

DAY 8
0
Vue.js

打造銷售系統30天修練 - 全集中・Vue之呼吸系列 第 8

Day 8:[Componentの呼吸・壹之型] Button工坊 - 從零打造Button元件

  • 分享至 

  • xImage
  •  

在昨天 Day7 的章節中,我們成功地為專案建立了現代化的開發環境。

今天,我們將正式開始享受這個環境帶來的好處,著手進行我們銷售系統的實作!

元件化 (Componentization)。

回想一下我們的 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>

這段程式碼的意思是:

  • 這個按鈕永遠有一個基礎的 btn class。
  • 同時,根據傳入的 variant prop (例如 'primary'),動態加入 btn-primary class。
  • 根據傳入的 size prop (例如 'large'),動態加入 btn-large class。

再搭配 <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 元件化開發的三大核心:

  1. <slot />:讓元件內容保持高度彈性。
  2. props:實現由父到子的單向資料傳遞,客製化元件的外觀與行為。
  3. emit:實現由子到父的事件通知,讓元件可以回應使用者的互動。

明天,Day 9:[Componentの呼吸・貳之型] 測試Button - 學習Component Testing

我們將繼續沿用這個模式,說明我們如何測試我們打造的元件 。心を燃やせ 🔥!


上一篇
Day 7:[Vueの呼吸・陸之型] 開發環境 - 建立Vue專案與工具配置
下一篇
Day 9:[Componentの呼吸・貳之型] 測試Button - 學習Component Testing
系列文
打造銷售系統30天修練 - 全集中・Vue之呼吸9
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言