2024-09-22|閱讀時間 ‧ 約 23 分鐘

EP14 - 組件基礎

Components Basics 看了這麼久才了解組件基礎!
看來還有很多不了解的啊~
等這些基礎內容看完再來做範例應該可以比較清楚吧www

組件允許我們將 UI 分割成獨立且可重複使用的部分,並以獨立的方式思考每個部分。應用程序通常組織成一個嵌套組件的樹狀結構:

raw-image

這與嵌套原生 HTML 元素非常相似,但 Vue 實現了自己的組件模型,使我們能夠在每個組件中封裝自定義內容和邏輯。Vue 也能很好地與原生 Web Components 協作。如果您對 Vue 組件和原生 Web Components 之間的關係感到好奇,請在此處閱讀更多

定義組件 - Defining a Component

在使用構建步驟時,我們通常在一個專用的文件中定義每個 Vue 組件,使用 .vue 擴展名,這被稱為單文件組件 (SFC):

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

const count = ref(0)
</script>

<template>
<button @click="count++">You clicked me {{ count }} times.</button>
</template>

在不使用構建步驟的情況下,可以將 Vue 組件定義為包含 Vue 特定選項的普通 JavaScript 物件:

import { ref } from 'vue'

export default {
setup() {
const count = ref(0)
return { count }
},
template: `
<button @click="count++">
You clicked me {{ count }} times.
</button>`
// 也可以針對 DOM 中的模板元素:
// template: '#my-template-element'
}

這裡的模板內嵌為 JavaScript 字串,Vue 會即時編譯。您也可以使用 ID 選擇器指向一個元素 (通常是原生的 <template> 元素) - Vue 將使用其內容作為模板來源。

上述示例定義了一個單一的組件並將其作為 .js 文件的默認導出,但您可以使用命名導出從同一文件中導出多個組件。

使用組件 - Using a Component

提示

在本文件其餘部分,我們將使用 SFC 語法,無論是否使用構建步驟,關於組件的概念都是相同的。在示例部分,展示了這兩種情況下的組件使用。

要使用子組件,我們需要在父組件中導入它。假設我們將計數器組件放在一個名為 ButtonCounter.vue 的文件中,該組件將作為該文件的默認導出:

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

<template>
<h1>Here is a child component!</h1>
<ButtonCounter />
</template>

使用 <script setup>,導入的組件會自動在模板中可用。

也可以全局註冊一個組件,使其在整個應用中的所有組件中都可用,而無需導入。在專門的組件註冊部分中討論了全局註冊與局部註冊的優缺點。

組件可以多次重用:

<template>
<h1>Here are many child components!</h1>
<ButtonCounter />
<ButtonCounter />
<ButtonCounter />
</template>

Try it in the playground

注意,當點擊按鈕時,每個按鈕都保持自己的獨立計數。這是因為每次使用組件時,會創建其新實例。

在 SFC 中,建議使用 PascalCase 標籤名來區分子組件和原生 HTML 元素。儘管原生 HTML 標籤名不區分大小寫,但 Vue SFC 是編譯格式,因此我們可以在其中使用區分大小寫的標籤名。我們還可以使用 / > 來關閉標籤。

如果您直接在 DOM 中編寫模板(例如作為原生 <template> 元素的內容),模板將受到瀏覽器原生 HTML 解析行為的約束。在這種情況下,您需要使用 kebab-case 和顯式閉合標籤來表示組件:

<!-- 如果此模板在 DOM 中編寫 -->
<button-counter></button-counter>
<button-counter></button-counter>
<button-counter></button-counter>

查看更多有關 DOM 模板解析的注意事項。

傳遞 Props - Passing Props

如果我們在構建一個部落格,我們可能需要一個代表部落格文章的組件。我們希望所有的部落格文章共享相同的視覺佈局,但內容不同。這樣的組件如果不能傳遞數據(例如我們要顯示的特定文章的標題和內容),那麼它就不會有用處。而這正是 props 的作用所在。

Props 是可以在組件上註冊的自定義屬性。要將標題傳遞給我們的部落格文章組件,我們必須在該組件接受的 props 列表中聲明它,使用 defineProps 巨集:

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

<template>
<h4>{{ title }}</h4>
</template>

defineProps 是一個僅在 <script setup> 中可用的編譯時巨集,不需要顯式導入。聲明的 props 將自動暴露給模板。defineProps 也返回一個包含所有傳遞給組件的 props 的對象,因此如果需要,我們可以在 JavaScript 中訪問它們:

const props = defineProps(['title'])
console.log(props.title)

另見: Typing Component Props

如果您不使用 <script setup>,則應使用 props 選項來聲明 props,並且 props 對象將作為第一個參數傳遞給 setup():

export default {
props: ['title'],
setup(props) {
console.log(props.title)
}
}

一個組件可以有任意多的 props,並且默認情況下,任何值都可以傳遞給任何 prop。

一旦註冊了 prop,您可以像這樣作為自定義屬性傳遞數據給它:

<template>
<BlogPost title="My journey with Vue" />
<BlogPost title="Blogging with Vue" />
<BlogPost title="Why Vue is so fun" />
</template>

然而,在一個典型的應用中,您可能在父組件中有一個帖子數組:

const posts = ref([
{ id: 1, title: 'My journey with Vue' },
{ id: 2, title: 'Blogging with Vue' },
{ id: 3, title: 'Why Vue is so fun' }
])

然後希望使用 v-for 為每個帖子渲染一個組件:

<template>
<BlogPost
v-for="post in posts"
:key="post.id"
:title="post.title"
/>
</template>

Try it in the playground

注意如何使用 v-bind 語法 (:title="post.title") 來傳遞動態 prop 值。這在您無法提前知道要渲染的確切內容時特別有用。

現在您需要了解的 props 就這麼多,但閱讀完本頁並對其內容感到滿意後,我們建議稍後回來閱讀 props 的完整指南。

監聽事件 - Listening to Events

在開發 <BlogPost> 組件時,一些功能可能需要與父組件進行通信。例如,可能會決定加入一個無障礙功能來放大部落格文章的文本,同時保持頁面其他部分默認大小。

在父組件中,我們可以通過添加 postFontSize 的 ref 來支持這個功能:

const posts = ref([
/* ... */
])

const postFontSize = ref(1)

這可以在模板中用來控制所有部落客文章的字體大小:

<template>
<div :style="{ fontSize: postFontSize + 'em' }">
<BlogPost
v-for="post in posts"
:key="post.id"
:title="post.title"
/>
</div>
</template>

現在我們在 <BlogPost> 組件的模板中添加一個按鈕:

<!-- BlogPost.vue, omitting <script> -->
<template>
<div class="blog-post">
<h4>{{ title }}</h4>
<button>Enlarge text</button>
</div>
</template>

按鈕目前還沒有功能,我們希望點擊按鈕時可以向父組件傳達應該放大所有文章的文本。為了解決這個問題,組件提供了一個自定義事件系統。父組件可以選擇使用 v-on@ 來監聽子組件實例上的任何事件,就像我們對待原生 DOM 事件一樣:

<template>
<BlogPost
...
@enlarge-text="postFontSize += 0.1"
/>
</template>

然後子組件可以通過調用內置的 $emit 方法來自己發出事件,傳遞事件名稱:

<!-- BlogPost.vue, omitting <script> -->
<template>
<div class="blog-post">
<h4>{{ title }}</h4>
<button @click="$emit('enlarge-text')">Enlarge text</button>
</div>
</template>

由於有 @enlarge-text="postFontSize += 0.1" 監聽器,父組件會接收到該事件並更新 postFontSize 的值。

Try it in the playground

我們可以選擇使用 defineEmits 巨集來聲明發出的事件:

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

這會記錄組件發出的所有事件並可選地對它們進行驗證。它還允許 Vue 避免隱式地將它們應用為子組件根元素的原生監聽器。

defineProps 類似,defineEmits 只能在 <script setup> 中使用,不需要導入。它返回的 emit 函數等同於 $emit 方法。它可以在組件的 <script setup> 部分用來發出事件,這裡無法直接訪問 $emit

<script setup>
const emit = defineEmits(['enlarge-text'])

emit('enlarge-text')
</script>

另見: Typing Component Emits

如果您不使用 <script setup>,可以使用 emits 選項來聲明發出的事件。您可以作為 setup 上下文的屬性(作為第二個參數傳遞給 setup())來訪問 emit 函數:

export default {
emits: ['enlarge-text'],
setup(props, ctx) {
ctx.emit('enlarge-text')
}
}

現在您需要了解的自定義組件事件就是這麼多,但閱讀完本頁並對其內容感到滿意後,我們建議稍後回來閱讀自定義事件的完整指南。

使用插槽的內容分發 - Content Distribution with Slots​

與 HTML 元素一樣,能夠將內容傳遞給組件通常是非常有用的,例如:

  <AlertBox>
Something bad happened.
</AlertBox>

這可能會渲染成這樣:

This is an Error for Demo Purposes

Something bad happened.

這可以透過 Vue 的自訂 <slot> 元素來實現:

<!-- AlertBox.vue -->
<template>
<div class="alert-box">
<strong>這是一個用於示範的錯誤訊息</strong>
<slot />
</div>
</template>

<style scoped>
.alert-box {
/* ... */
}
</style>

如上所示,我們使用 <slot> 作為內容的佔位符,就這樣,我們完成了!

Try it in the playground

這就是目前你需要知道的關於插槽的所有內容,但在你讀完這頁並對其內容感到熟悉後,我們建議你稍後再回來閱讀完整的插槽指南。

動態元件 - Dynamic Components

有時,動態切換元件是非常有用的,例如在分頁介面中:

Open example in the playground

上面的功能是透過 Vue 的 <component> 元素和特殊的 is 屬性來實現的:

<!-- 當 currentTab 改變時,元件會隨之改變 -->
<component :is="tabs[currentTab]"></component>

在上述範例中,傳遞給 :is 的值可以是:

  • 註冊元件的名稱字串,或
  • 實際匯入的元件物件

你也可以使用 is 屬性來創建常規的 HTML 元素。

在使用 <component :is="..."> 切換多個元件時,當切換離開時,該元件會被卸載。我們可以使用內建的 <KeepAlive> 元件來強制非活躍的元件保持「存活」狀態。

在 DOM 中的模板解析注意事項 - in-DOM Template Parsing Caveats​

如果你直接在 DOM 中編寫 Vue 模板,Vue 必須從 DOM 獲取模板字串。這會導致一些注意事項,因為瀏覽器的原生 HTML 解析行為。

提示

需注意,以下討論的限制僅適用於直接在 DOM 中編寫模板。如果你使用來自以下來源的字串模板,則不會受到這些限制的影響:

  • 單檔元件(Single-File Components)
  • 嵌入式模板字串(例如:template: '...'
  • <script type="text/x-template">

大小寫不敏感 - Case Insensitivity

HTML 標籤和屬性名稱對大小寫不敏感,因此瀏覽器會將任何大寫字元視為小寫。這意味著在使用 DOM 模板時,PascalCase 元件名稱和 camelCased 屬性名稱或 v-on 事件名稱都需要使用其 kebab-case(以連字符分隔)等效形式:

// JavaScript 中的 camelCase
const BlogPost = {
props: ['postTitle'],
emits: ['updatePost'],
template: `
<h3>{{ postTitle }}</h3>
`
}
<!-- HTML 中的 kebab-case -->
<blog-post post-title="hello!" @update-post="onUpdatePost"></blog-post>

自我閉合標籤 - Self Closing Tags

在之前的程式碼範例中,我們使用了自我閉合標籤來表示元件:

<MyComponent />

這是因為 Vue 的模板解析器會將 /> 視為結束任何標籤的指示,無論其類型如何。

然而,在 DOM 模板中,我們必須始終包含明確的結束標籤:

<my-component></my-component>

這是因為 HTML 規範僅允許少數特定元素省略結束標籤,最常見的為 <input><img>。對於所有其他元素,如果省略了結束標籤,原生 HTML 解析器會認為你從未終止開放標籤。例如,以下程式碼片段:

<my-component /> <!-- 我們打算在這裡關閉標籤... -->
<span>hello</span>

將被解析為:

<my-component>
<span>hello</span>
</my-component> <!-- 但瀏覽器會在這裡關閉它。 -->

元素放置限制 - Element Placement Restrictions

某些 HTML 元素,如 <ul><ol><table><select>,對其內部可以出現的元素有限制,而某些元素如 <li><tr><option> 僅能出現在特定的其他元素內。

這會在使用具有此類限制的元素的元件時導致問題。例如:

<table>
<blog-post-row></blog-post-row>
</table>

自訂元件 <blog-post-row> 將被提升為無效內容,導致最終渲染的輸出出現錯誤。我們可以使用特殊的 is 屬性作為變通方法:

<table>
<tr is="vue:blog-post-row"></tr>
</table>

提示

當在原生 HTML 元素上使用時,is 的值必須以 vue: 為前綴,才能被解釋為 Vue 元件。這是為了避免與原生自訂內建元素混淆。

這就是目前你需要了解的有關在 DOM 中的模板解析注意事項—實際上,這也是 Vue 基礎知識的結束。恭喜你!還有更多需要學習的內容,但首先,我們建議你休息一下,自己動手玩玩 Vue—建構一些有趣的東西,或者如果你還沒有的話,查看一些範例

一旦你對剛學到的知識感到熟悉,就可以繼續閱讀指南,深入了解元件的內容。

終於啃完基礎篇了嗎?一堆請你參考別的內容~
真的是會越看越頭大的文件,不是說好Vue比較簡單嗎?
看起來什麼東西都沒有這麼簡單www
文件看一看真的要開始做一些範例 不然好想睡覺唷~週末愉快!







分享至
成為作者繼續創作的動力吧!
© 2024 vocus All rights reserved.