Andy's blog

If you always do what you've always done, you'll always get what you've always got.

0%

Vue.js元件(完整版)

參考資料:
Vue.js: 元件 Components 簡介 - 註冊與使用
Vue.官方文件
Summer 夏天Vue.js: Slot


元件(components)簡介:

什麼是元件?

每個 Vue.js 的應用程式都是從Vue建構式 (vue constructor) 建立根實體 (root vue instance) 開始,再一個個將元件 (Components) 搭建上去而來的,透過元件的方式能讓開發者將程式碼封裝而且更好重複利用


元件特性?
一、元件資料都是獨立的
1.透過props向內部組件傳遞數據
2.透過emit event觸發事件將資料往外送

二、data必須是一個函數 額外練習:官網範例連結

1
2
3
4
5
6
7
Vue.component("button-counter", {
data: function() {
return {
count: 0
};
}
});

Q:為何子元件中data必須是函式?
A:因為 JavaScript 切分變數有效範圍的最⼩單位為 function,為了避免子元件資料互相污染,Vue強置規定子元件data必須是函式

Component(全域註冊)

下面將練習題目拆分成不同部分講解
寫法:
Vue.component(‘自定義名稱’, {Function | Object})
逗點後方可以使用function或object
範例如下(請先忽略props,後面會介紹到)

1
2
3
4
Vue.component('row-component', {
template: '#rowComponentTemplate',
props:['person']
})

使用X-template建立元件

此時,因為我們使用到X-template建立元件,因此我們必須額外新一個script,並指定一個 id來來使⽤

1
2
3
4
5
6
7
<script type="text/x-template" id="rowComponentTemplate">
<tr>
<td>{{ person.name }}</td>
<td>{{ person.cash }}</td>
<td>{{ person.icash }}</td>
</tr>
</script>

說明:這邊script是另外撰寫喔!
補充:放在codepen上時,X-template會將上面一段script寫在html中

接著,是HTML結構部分

1
2
3
4
<row-component v-for="(item, key) in data" :person="item" :key="key"></row-component>

//此時你會發現網頁上tr結構似乎錯誤,
//這是因為html結構上,tbody一定要包著tr

示意圖如下

  • 該如何改善?(使用is動態載入template)
    1
    2
    3
    <tr is="row-component" v-for="(item, key) in data" :person="item" :key="key"></tr>

    //使用is動態載入template
    正常版
  • 最後,前面提到的props功能為何?
    將v-for中的data資料傳進template元件中。示意圖如下

區域註冊介紹(範例如下)

寫法:var ComponentA = { /* … */ } 官網介紹
說明:ComponentA,為自定義名稱

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
   var child = {
props: ['person'],
template: '#rowComponentTemplate'
}
var app = new Vue({
el: '#app',
data: {...略},
components:{
"row-component":child
//row-component必須與html上載入組件名稱相同
}
});
</script>
說明:html結構相同,這邊就不另外撰寫
注意:我們在組建下新增一個components物件

提醒:components,有s

基礎建立元件方式(官網教學內容)

官網範例連結

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 提醒:Vue.component必須寫在new Vue前方,原因是元件必須先定義才能被使用。參閱下方討論連結
Vue.component("button-counter", {
data: function() {
return {
count: 0
};
},
template:
`<button class="btn btn-outline-primary"
v-on:click="count+=2">
You clicked me {{ count }} times.
</button>`
});

new Vue({
el: "#components-demo"
});

基礎建立元件跟使用X-template差異僅在於template放置位置
Vue.component new Vue 擺放順序

結論

1.由於 HTML 不分⼤⼩寫的特性,使用⾃定義的標籤時限定全⼩
(可加入破折號 - ) 的標籤名稱
2.元件註冊分為全域區域
3.區域元件離開指定實體後就不能取用
4.全域元件則可以提供多組實體使用

Q:子元件中的data是否可以使用箭頭函式?
A:不行,因為這會影響到this作用域。會造成無法取得data資料。其實像是computed、methods、watch也都不行使用箭頭函式
But,僅有Filter可以使用arrow function,因為filter無法取得實體資料!
Q:下面寫法是全域註冊還是區域註冊?

1
2
3
var CustomBlock = Vue.component('custom-block', {
template: `<div class="block">B</div>`,
});>

A:是全域註冊喔,別搞混了!

使用Props由外到內傳遞資料

說明:
我們之所以需要使用props的目的,在於Vue元件中所有元件都是獨立的,因此資料不能互相取用,這也意味著你不能 (也不應該)在子元件的模組直接引⽤⽗元件的資料。而需要透過props將資料從外部進行傳遞

我們先來看一個簡單範例:

codepen連結
提醒:使用 DOM 中的模板時,camelCase (駝峰命名法) 的 prop 名需要使用其等價的 kebab-case (短橫線分隔命名) 命名
簡單來說:JS上用小駝峰parentMSg ,則html上要用短橫線分隔 如parent-msg
畫面如下:

靜態傳入與動態傳入差異

六角課程範例
動態傳入:傳入的是實體內內容

靜態傳入:傳入的是純字串

小結論:動態傳入跟靜態傳入寫法差異僅在於是否有
只要沒有即使透過number傳入也會是string喔!同學討論連結

Props驗證與預設值

我們知道Props是將外部資料傳入元件內,但你怎麼知道傳入的東西是否符合需求或安全呢?這時就需要很常使用到驗證
首現先介紹型別:

1
2
3
4
5
6
7
8
String 
Number
Boolean
Object
Array
Date
Function
Symbol

提醒:型別字首要大寫 如:type: Number

範例:

這邊僅列出幾個例子方便我日後快速回覆記憶,完整內容請看codepen

1
2
3
4
5
6
7
8
9
10
props: {
parentMsg: null, // null 代表不檢查型別
propA: Number, // 限定數字(Number要大寫)
propB: [String, Number], // 多種條件可用 [ ] 隔開
propC: {
// 必要欄位,且限定字串型別
type: String,
required: true
},
}

提醒:props本來就應該用物件包裝起來,因為要對傳進來物件進行驗證
這邊特別注意:
1.驗證object時,default必須為一個function 如下

1
2
3
4
5
6
7
8
9
propE: {
// Object 型別,代表可接受的是個物件型別
type: Object,
default: function () {
return {
message: 'hello'
}
}
},

2.這兩種寫法相同喔!都是對型別不做任何驗證

1
2
3
4
props:['parentMsg']
props:{
parentMsg:null
}

Props使用注意事項

單向數據流

什麼意思呢?Props是將資料由外部往內傳遞,而在老師範例中可以清楚看到如果我們從內部修改外部傳進來內容,便會造成錯誤!
後記:仔細想想也蠻有道理,假設我們可以從子層更動父層實體內容,如此一來父層實體內容便會無法管理(或是說資料會不知道被誰更動過!)
這時候該如何解決呢?
很簡單,只要新宣告一個參數來接受外部修改資料就好~寫法如下
或是在元件中使用computedgetset
這兩種寫法目的:都是讓資料獨立存在於子層而非父層

1
2
3
4
5
6
7
8
9
Vue.component('photo', {
props: ['imgUrl'],
template: '#photo',
data: function () {
return {
newUrl: this.imgUrl
}
}
})

當V-model遇到props

範例如下:
透過上方圖片講解,重點在於第四點,在component底下的data資料是無法改變上層父元素內容!也就是下方圖片中更改4號無法更改1、2、3的資料
codepen示範

尚未宣告變數(就是AJAX資料時間差)

老師這邊提到的目的是,因為在new vue中我們使用AJAX傳遞資料,而AJAX特性就是非同步,簡單來說就是網頁載入時,AJAX資料可能還未載入,因此網頁上就空空的或是圖片破版。該如何解決呢?

1
2
3
老師這邊提供一個解法就是v-if,當v-if中內容為truthy時,再將內容載入
<card :user-data="user" v-if="user.gender" ></card>

父子元件之間溝通方法

總共區分為監聽事件、觸發事件兩種:
監聽事件:使用V-on 或$on(後者主要會用在event bus)
觸發事件:透過$emit

元件父與子

子元件可以透過this.$parent取得父層實體內容 範例如下

1
2
3
4
5
Vue.component('my-component',{
template:'#my-component',
mounted(){
console.log("$parent:",this.$parent.msg); //MsgofParent!
});

這邊的parent,指的是id=“app”這個區塊

父组件則是透過 this.$children這個成員 (陣列), 來存取他的⼦組件。但是要注意this.$children 的順序會受到 v-if 的影響,建議先以ref給子組件設定別名以確保不受組件的順序影響。 範例如下:

1
2
3
4
5
6
7
8
9
10
11
 newVue({
el:'#app',
data:{
msg:'MsgofParent!'
},
mounted(){
console.log('$children2:',this.$refs.comp2.msg);
console.log('$children3:',this.$refs.comp3.msg);
}
});

使用時機

1.debug除錯
2.實務上建議不要這樣直接複寫父層內容(因為子層無法脫離父層內容)

觸發事件:透過emit 向外傳遞事件

上面當V-model遇到props 範例中,我們發現子層資料無法更動父層資料。如果要更動上層資料就會使用到$emit,將資料由內傳到外。
我們先來看個簡單例子:

示意圖如下:
codepen練習檔案

說明:只要更動4號欄位中內容,其餘1、2、3資料也會同步更新喔!

範例2:
課程範例連結
我們的目的如下:點擊內部元件後,改變外部資料數值

以下摘錄轉寫重點

1
2
3
4
5
<button-counterincrement="incrementTotal">
</button-counter>

// Step1.先自定義事件increment(內層元素)
// incrementTotal(外層元素)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Vue.component('buttonCounter',{
methods: {
incrementCounter:function(){
// Step3.內部事件觸發外部資料,使用$emit
this.$emit('increment',Number(this.counter))
// 我們這邊為避免型別出現錯誤使用Number
}
}
})

Step3.Vue instance中使用newMoney接受內部傳遞資料
methods: {
// Step2.使用newMoney接受incrementCounter傳入參數
incrementTotal:function(newMoney){
this.cash += newMoney;
}
}

.sync修飾符傳遞

可以幫助我們達到資料雙向綁定 官網介紹
補充:雖然我們看似寫了.sync後就達到雙向綁定,實際上我們在子元件中還是透過$emit向外層傳遞更改後的數值

提醒:為何update不能更改為其他JS原生事件:click?、input?
A:因為vue並未定義JS原生事件。所以必須要使用vue本身設定update
優點:
Vue實體不需要在使用methods接受子層傳遞出來資料!

小結論:

這就是props in , emit out的由來

Event Bus

用途:將原本元件之間資料傳遞時的樹狀結構更改為網狀結構!

寫法:
Step1. 新增一個Vue實體
var bus = new Vue()
Step2. 透過向bus發送事件,與訂閱事件來完成元件與元件的溝通

接著,我們直接來看範例:
備註:訂閱事件必須撰寫在created()階段

缺點:
1.當事件名稱重複時,會同時觸發不同子元件
2.因為事件是自己手動訂閱,所以銷毀時候,在beforeDestroy必須手動刪除

綜合練習:todoList


更新Component 資料方法整理

1.Event Bus
2.Props in Emit Out
3.$children 、$parent
4.Vuex

is動態切換元件

用途:最常使用在頁籤切換!
我們可以透過
1.<component>加上:is屬性來來決定目前元件是誰
2.或是<div>加上:is
寫法:

1
2
<component:is="currentView"></component>
<div :is="currentView"></div>

說明:上面介紹兩種寫法其實沒有差異,只是列出比較常使用方法而已。
注意:使用 is 要注意的只有像<li><tr><option>這類有特別限制上層 DOM 元素必須要是哪幾種的,像 <li> 的外層就只能是<ol><ul>官網連結
範例: codepen六角課堂範例
而當我們透過 加上 :is 屬性來來切換元件時,原本元件內的狀態不會保留,這時候就需要透過 <keep-alive> … </keep-alive>
來為元件保留內部狀態 codepen
延伸閱讀:[[Vue Instance/生命週期介紹]]

Slot 元件插槽

在介紹slot之前,我們必須先瞭解一個觀念:編譯作用域
我們先來看個範例

1
2
3
4
5
6
7
8
<div id="app">
<div class="parent">{{msg}}</div>

//生命週期中,在created階段,如果有template,vue則會去編譯template
//內容,也因此,child元件不會顯示I'mparent內容

<child>{{msg}}</child>
</div>

猜猜看,child元件中是否會印出父層的msg:I'mparent內容?
答案是不會的!原因是目前child所處的位置是父層作用域,當然不會印出子層內容~ codepen連結

小結論:
1.父層作用域在父層編譯
2.子層作用域在子層編譯
若要突破作用域編譯問題,就要使用到slot

Slot簡介

參考資料:Summer 夏天Vue.js: Slot
突破作用域,將父層內容傳遞進來!通常會應用在如果內容不一定要放入component中時,可以透過slot傳遞一整個html結構進來。
Slot 是一種用於內容分配(Content Distribution / Transclusion)的元件,適用於複雜或巢狀元件的實作上,可以想像成是空間預留的方法,在迭代過程中再把內容塞進去。

卡斯伯老師在課堂上將slot分為三種:

1.沒有slot的狀態

1
2
3
<no-slot-component>
<p>這是一段沒有插槽使用的狀態</p>
</no-slot-component>

說明:即使寫p段落文字在子元件中,在html上依舊不會顯示。因為元件中的內容都會被模板替換掉。這就是上方提到作用域編譯的問題~

2.單組slot

特點:在模板中添加

1
2
3
<single-slot-component>
<p>使用這段取代原本的 Slot。</p>
</single-slot-component>
1
2
3
4
5
6
7
8
<script type="text/x-template" id="singleSlotComponent">
<div class="alert alert-warning">
<h6>我是一個元件</h6>
<slot>
如果沒有內容,則會顯示此段落。
</slot>
</div>
</script>

畫面上,就可以將<p>使用這段取代原本的 Slot。</p>呈現在畫面上!如下

3.多組slot(具名插槽)

簡單來說,將內容放入指定位置
在html加入slot
在JS中插入 name

1
2
3
4
5
6
<named-slot-component>
<header slot="header">替換的 Header</header>
<template>替換的 Footer</template>
<template slot="btn">按鈕內容</template>
<p>其餘的內容</p>
</named-slot-component>
1
2
3
<div class="card-header">
<slot name="header">這段是預設的文字</slot>
</div>

補充:html結構中若不想顯示標籤如a、header,可以使用