Vue 的整個大架構核心是元件 (component),藉由把會重複利用的邏輯、樣式、內容封裝到一個個組件之中,來提高程式碼複用性以及開發效率。簡單來說,一個 Vue 建立的網頁就是由許多組件一個個拼出來的,然後資料在各個組件中穿梭來改變渲染我們的畫面。
所以本篇核心為 Vue 的元件以及資料傳遞。
讓我們先在 src/components 下建立一個 PeopleBlock.vue 的檔案,這之後就會是我們第一個元件。
我們先在 PeopleBlock.vue 中寫一點東西:
<script setup>
</script>
<template>
<h1>I am a component</h1>
</template>
然後回到 App.vue 中來引入剛剛建立的元件,此時 App.vue 相對於 PeopleBlock.vue 來說就是父元件。引入成功會看到畫面改變啦!
<script setup>
import PeopleBlock from './components/PeopleBlock.vue';
</script>
<template>
<PeopleBlock />
</template>
這種是元件引入的基本用法,但如果說今天 PeopleBlock.vue 要在很多元件中引入呢?每個元件都寫一次 import
好麻煩,所以針對這類情形可以直接修改 main.js。
原本的 main.js 內部長這樣:
import './assets/main.css'
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
現在我們要把它拆開改成這樣:
import './assets/main.css'
import { createApp } from 'vue'
import App from './App.vue'
import PeopleBlock from './components/PeopleBlock.vue'
const app = createApp(App)
app.component('PeopleBlock', PeopleBlock)
app.mount('#app')
這樣我們可以直接在 App.vue 中直接使用 PeopleBlock.vue 而不用 import
。
如果你跟我一樣從 React 過來的,那看到這個名詞會覺得很熟悉,這就是父元件將資料傳給子元件的方式,注意,是父元件傳給子元件。
現在假設我們父元件 App.vue 中有一筆資料要傳到 PeopleBlock.vue 中套用它的格式,那麼在兩個檔案中我該如何寫?
App.vue:
<template>
<PeopleBlock name="Jeremy"/>
</template>
PeopleBlock.vue:
<script setup>
const props = defineProps(['name'])
</script>
<template>
<h1>My name is: {{ props.name }}</h1>
</template>
你會發現父元件中給的名字 Jeremy 成功傳給子元件做出了渲染。
在 Vue 中靠 defineProps
在子組件中定義接收到父元件資料,可以以陣列或物件的方式呈現。物件的寫法比較嚴謹一些,剛剛的宣告改成物件可以這樣寫:
const props = defineProps({
name:{
type: String,
default: ''
}
})
在講 emit
之前,我要先提一個我稍早跟 senior 討論的問題:到底能不能把 function 在 Vue 中作為 props
傳遞給子元件?
從 React 過來的人應該通常都有這問題吧!因為 React 就是這樣做啊,我為什麼還要用一個 emit
來做傳遞?而且實際操作也可以喔,請看:
App.vue:
<script setup>
import { ref } from 'vue';
const name = ref('Jeremy')
const changeName = (newName)=> name.value = newName
</script>
<template>
<PeopleBlock :name="name" :change-name="changeName"/>
</template>
PeopleBlock.vue:
<script setup>
const props = defineProps({
name:{
type: String,
default: ''
},
changeName:{
type: Function,
default: () => {}
}
})
</script>
<template>
<h1>My name is: {{ props.name }}</h1>
<button @click="changeName('Joanna')">click</button>
</template>
會發現實際上當我點擊了按鈕後,名字從 Jeremy 變成 Joanna 了,一切貌似運作正常。而且 Mike 老師也在影片中特別說這樣單向資料流的方式便於維護管理。
但是喔,就是這個但是,Vue 不推薦你這樣做。部分文章也指出這樣做第一個有效能的問題,第二個是 Vue 看心情可能會渲染不出來改變的資料,所以為了避免這種賭運氣的事件,公司的 senior 還是統一用 emit
了。
所以現在來看看 Vue 不同於 React 的做法,有一個口訣:
Props in, Events out
Props
大家很熟悉,events out 靠的就是 emit
,是由子組件把事件傳給父元件,然後由父組件去監聽這個事件來決定要幹什麼。
比如剛剛的 code,改成 emit
操作會是這樣的:
先改 PeopleBlock.vue:
<script setup>
const props = defineProps({
name:{
type: String,
default: ''
}
})
defineEmits(['update-name'])
</script>
<template>
<h1>My name is: {{ props.name }}</h1>
<button @click="$emit('update-name')">click</button>
</template>
解釋一下就是我在子元件中建立一個客製化事件叫做 update-name
,並透過 $emit
將這個事件傳給父元件。
App.vue 修改如下:
<script setup>
import { ref } from 'vue';
const name = ref('Jeremy')
const changeName = (newName)=> name.value = newName
</script>
<template>
<PeopleBlock :name="name" @update-name="changeName('Joanna')"/>
</template>
在父元件中透過 @update-name
接收從子元件傳來的事件,若事件被觸發就套用父元件本身設定的 changeName
函式更新名字。
這就是 Vue 有別 React 的資料傳遞方式,這種模式還會在之後的 v-model
在元件中的傳遞也息息相關,是一個必須要搞懂的地方。
過往在取得表單的資料通常都要寫一大串的 DOM 抓取或是監聽 change 或 input 事件,而在 Vue 中,這些事情變得非常簡單,靠的就是 v-model
。
我們來建立一下一個 <input>
嘗試看看:
<script setup>
import { ref } from 'vue';
const name = ref('Jeremy')
</script>
<template>
<input type="text" v-model="name">
<h1>{{ name }}</h1>
</template>
可以輕鬆發現隨著我在輸入框的改變,<h1>
渲染的畫面也會跟著改變,這表示 v-model
確實地把這個 <input>
跟我的 name
這個 ref
項目綁在一起了,這就是 Vue 在表單處理上強大的地方。
你甚至可以把 v-model
用在 check box、textarea、radio…等地方。
而且 v-model 還提供三種修飾福來幫助處理表單:
.lazy
:這個會把 Vue 的同步更新事件從預設的 input
改成 change
,也就是你在輸入的時候並不會立即觸發畫面更新。.number
:把輸入資料轉成數字,但如果開頭為0,他會幫你拿掉。.trim
:我感覺最有用的地方,去除首尾空白!再也不用取出input value 後才寫 trim 了!但是最前面講到的,Vue 是一個元件組成的事件,有時甚至會把這樣一個 <input>
單獨寫成一個元件,那 v-model
勢必得在元件中傳遞,該怎麼做呢?回歸剛剛講到的 Props in, Events out,來看一下如何利用這個口訣解決問題。
先創個 InputArea.vue 元件然後先不寫任何資料:
InputArea.vue:
<script setup >
</script>
<template>
<input type="text">
<h1></h1>
</template>
App.vue:
<script setup>
import InputArea from './components/InputArea.vue'
</script>
<template>
<InputArea/>
</template>
現在我們來建立資料進行傳遞:
App.vue:
<script setup>
import { ref } from 'vue';
import InputArea from './components/InputArea.vue'
const name = ref('Jeremy')
</script>
<template>
<InputArea v-model="name"/>
</template>
父元件的操作很簡單,綁 v-model
就對了。
InputArea.vue:
<script setup >
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>
<template>
<input type="text" :value="modelValue" @input="$emit('update:modelValue', $event.target.value)">
<h2>{{ modelValue }}</h2>
</template>
我們在子元件上定義接收資料的 props
與傳送事件的 emit
,然後在 <input>
v-vind
上 value
屬性和 input
事件,input
事件中帶上我們自定義的事件,這樣就成功讓 v-model
在兩個元件中傳遞。
那能不能綁定多個 v-model
呢?讓我抄一下官網的 code :
App.vue:
<script setup>
import { ref } from 'vue';
import InputArea from './components/InputArea.vue'
const first = ref('Jeremy')
const last = ref('Ho')
</script>
<template>
<InputArea
v-model:first-name="first"
v-model:last-name="last"/>
</template>
InputArea.vue:
<script setup>
defineProps({
firstName: String,
lastName: String
})
defineEmits(['update:firstName', 'update:lastName'])
</script>
<template>
<input
type="text"
:value="firstName"
@input="$emit('update:firstName', $event.target.value)"
/>
<input
type="text"
:value="lastName"
@input="$emit('update:lastName', $event.target.value)"
/>
<h1>{{ firstName }} {{ lastName }}</h1>
</template>
你可能注意到,上面兩個 v-model
的範例中,App.vue 中對 v-model
的綁定寫法不太一樣。一個是直接 v-model="變數"
,一個是 v-model:名字="變數"
,對於前面那種作法,你在子元件中 defineProps
時就只能用 modelValue
,而後面那一種允許你寫自己定義的名字,這是差別所在。