# Vue.js 學習筆記
:::warning
參考 [**30天手把手的vue.js教學!**](https://ithelp.ithome.com.tw/users/20129072/ironman/3052)、[**重新認識 Vue.js**]([https://](https://book.vue.tw/menu.html)) 做的學習筆記
:::
## 目錄
[TOC]
## ▊ 簡介
Vue基於標準的HTML、CSS和Javascript,提供了一套宣告式、元件化的模型。
**Vue的特色:**
1. **宣告式渲染**
2. **MVVM架構**
3. **漸進式框架**
### ▎宣告式渲染 vs 指令式渲染
**指令式渲染:**
1. 一步一步的告訴程序怎麼做
2. code之間會相互依賴,缺了一部份程式碼可能就不能動了
**宣告式渲染:**
1. 只專注想完成的部分,其他交給JS物件管理
2. code之間不會相互依賴
:writing_hand: **範例**
例如,當在input欄輸入文字後,下面的`<p>`段落要立即顯示輸入的文字。
:::warning
如果使用Vue(宣告式)的作法,只需要:
* 使用`v-model`綁定input欄位
* 在`<p>`裡使用`{{v-model綁定的資料}}`的寫法
:::
:::info
如果是指令式的做法:
* 用`.addEventListener()`監聽input事件
* 當input事件觸發後,用`document.querySelector`等語法抓取`<p>`的DOM,並將input內容抓出來
* 將抓到的文字寫進`<p>`
:::
---
### ▎MVVM
M(Model)、V(View)、VM(ViewModel)

Vue 包辦了監聽和資料綁定,畫面 (View) 觸發事件或是狀態 (Model)變更了,都會由 Vue (ViewModel)來處理並且同步更新畫面 (View)。
## ▊ Vue實體
每創建一個vue檔案,其實是在創建一個**vue實體**,原本的code如下:
```javascript=
new Vue({
el: '#app',
data: {
greeting: 'Hello World!',
user: 'Hassan Djirdeh',
city: 'Toronto',
},
});
```
vue實體中有眾多屬性,例如:data、methods、computed...。
### ▎data屬性
當一個vue實體被建立時,會將在data物件中的所有屬性加入vue的響應系統中(eactivity system),**當data物件中的任何屬性被改變值,我們的畫面(view)會即時重新渲染**,讓畫面的內容符合新變動的值。
在vue的`<template>`中可以利用 `{{ }}` (mustache syntax)來使用data物件中的屬性,範例如下:
:::spoiler **打開查看程式碼**
```javascript=
<template>
<div id="app">
<h1>{{message}}</h1>
</div>
</template>
<script>
export default {
data() {
return {
message: 'Welcome to Vue!'
};
}
};
</script>
```
:::
>* **`export default`**:導出vue.js。
>* **`data()`**:定義data物件中的數據,data物件中的message的值會取代template中的{{message}},如下圖,畫面會顯示Welcome to Vue!。

---
### ▎methods屬性
vue實體中methods屬性能定義綁在實體上的**函數**,就像一般的js函數,下面是一個簡單的[範例](https://codepen.io/nojdeluj-the-looper/pen/VwqmVpx):
:::spoiler **打開查看程式碼**
```javascript=
<template>
<div id="app">
<h1>{{message}}</h1>
<button v-on:click="greet">Click Me</button>
</div>
</template>
<script>
export default {
data() {
return {
message: 'Welcome to Vue!'
};
},
methods:{
greet(){
alert('Hello!')
}
}
};
</script>
```
:::
>* **`greet()`**:methods中定義了一個greet函數,此函數會跳出一個alert。
>* **`v-on:click="great"`**:在button監聽點擊事件,當點擊按鈕時會觸發greet()函數,所以會如下面的畫面跳出alert。

可以理解為:
```javascript
btn.addEventListener('click', greet)
```
`v-on`還可以縮寫為`@`:
```html
<button @click="greet">Click Me</button>
```
---
### ▎computed屬性
一般是用來處理運算後才會顯示在`<template>`上的資料,可以參考下面的[範例](https://codepen.io/nojdeluj-the-looper/pen/WNLRbgE):
:::spoiler **打開查看程式碼**
```javascript=
<template>
<div>
<label for="firstname">First name</label>
<input type="text" name="firstname" v-model="firstName">
<label for="lastname">Last name</label>
<input type="text" name="lastname" v-model="lastName">
<h2>{{fullName}}</h2>
</div>
</template>
<script>
export default {
data() {
return {
firstName: '',
lastName: ''
};
},
methods: {
},
computed: {
fullName() {
return this.firstName + ' ' + this.lastName
}
}
};
</script>
```
:::
> * 結果:
> 
> * `computed`一定要`return`某個值
#### ▏methods vs computed
methods其實也能辦到相同的功能
```javascript
methods: {
getFullName() {
return `${this.firstName} ${this.lastName}`
}
},
computed: {
fullName() {
return this.firstName + ' ' + this.lastName
}
}
```
:::info
兩者的差別在於:
* method是每次呼叫就會重新計算
* computed只有在資料變化時才會重新計算
:::
:::success
兩者使用的原則:
* method用在事件觸發的handler 或 vue實體中的函數上
* computed用在data屬性中,作為資料的延伸操作、處理 (例如排序)
:::
---
### ▎watch屬性
在vue實體中用來檢查某個屬性的值是否有發生任何變動,如果發生變動則做出對應的操作。
```javascript
watch: {
dataName: function(val, oldVal) {
// 要執行的callback
}
}
```
> * `dataName`:可以是data屬性的變數 或 computed屬性計算後的值
> * `val`:第一個參數會是變動後的值
> * `oldval`:第二個參數會是變動前的值
<br>
若要監聽的是某個物件的值,則需要監聽它的key值,並且以 **字串** 的方式傳入。
```javascript
data() {
return {
dataName: {
key1: 'abc',
key2: 123
}
};
},
```
```javascript
watch: {
"dataName.key1": function(val, oldVal) {
// 要執行的callback
}
}
```
例如:陣列的長度
```javascript
"arr.length" : function(val) {
console.log(`the new length is ${val}`)
}
```
<br>
若要傳入options,則需要以物件的方式表示,其中包含:
* handler function
* options (包含immediate & deep,預設為false)
```
watch: {
dataName: {
handler(val, oldVal) {
},
immediate: true,
deep: true
}
}
```
> * **`immediate`**:若設為true,則handler在初始化時會先執行一次。
> * **`deep`**:若設為true,則會一同監測這個值的深層有沒有變化,也就是監聽整個`dataName`物件,物件內的任何值發生變化都會觸發handler。
## ▊ vue directives 指令
常見的指令有:`v-on`, `v-model`, `v-bind`, `v-if`, `v-show`, `v-for`,透過這些指令可以輕鬆完成監控事件、同步資料或渲染資料。
## ▊ v-on | event handler
```htmlmixed
<h1 v-on:click="method">Click me!</h1>
<h1 v-on:dblclick="method">Double Click me!</h1>
<form v-on:submit="method">...</form>
<input v-on:keydown="method"
placeholder="Press down on keys" />
<input v-on:keyup="method"
placeholder="Release keys" />
```
### 縮寫和inline寫法
v-on也提供**縮寫**和**行內**(inline)寫法
```htmlembedded
// 原始寫法
<h1 v-on:click="method">Click me!</h1>
// 縮寫
<h1 @click="method">Click me!</h1>
// 縮寫 & inline-javascript
<h1 @click="isAdmin = true">Click me!</h1>
```
### ▎event modifiers
vue也提供各種**修飾符**(event modifiers)做更精確的事件監聽處理。
例如寫原生js使用到的`event.preventDefault()`或`event.stopPropagation()`在vue中可以使用修飾符達到一樣的效果。
* `event.preventDefault()`和`event.stopPropagation()` -> [參考教學](https://ithelp.ithome.com.tw/articles/10198999)
* `event.preventDefault()`:停止 事件的默認動作,例如`<a>`觸發的事件中加入,就不會觸發原本跳轉連結的動作。
* `event.stopPropagation()`:可以停止事件,避免一個事件觸發多個事件。
```htmlembedded
<!-- 下方的點擊事件將不會繼續傳播 -->
<a v-on:click.stop="doThis"></a>
<!-- 下方的submit事件將不會刷新頁面 -->
<form v-on:submit.prevent="onSubmit"></form>
<!-- 修飾符是可以彼此串接的 -->
<a v-on:click.stop.prevent="doThat"></a>
```
### ▎key modifiers
vue還提供了特殊按鍵的監聽修飾符(Key Modifiers),讓我們可以簡單的根據使用者按下的key去決定觸發的事件。
```htmlmixed
<!-- 下方的點擊事件將會由enter按鍵觸發 -->
<input v-on:keyup.enter="submit">
<!-- 方的點擊事件將會由Alt + C 觸發-->
<input v-on:keyup.alt.67="clear">
```
## ▊ v-bind | 屬性綁定
1. **透過v-bind可以改變html的屬性**,例如:
```javascript=
<template>
<div id="app">
<img v-bind:src="imgSrc"> //透過v-bind改變img的src屬性
</div>
</template>
<script>
export default {
data() {
return {
imgSrc: './XXX.jpg'
};
}
};
</script>
```
2. **也可以綁定style(以物件形式接收)**,例如:
```html
<div v-bind:style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
```
```javascript
data: {
activeColor: 'red',
fontSize: 30
}
```
3. **針對clss做動態綁定(以陣列形式接收)**,例如:
```htmlembedded
<div v-bind:class="[activeClass, errorClass]"></div>
```
```javascript
data: {
activeClass: 'active',
errorClass: 'text-danger'
}
```
以上的程式碼最終會render出以下的元素
```htmlembedded
<div class="active text-danger"></div>
```
還可以配合條件式決定綁哪個class,例如:
```htmlembedded
<div v-bind:class="[isActive ? activeClass : '', errorClass]"></div>
//根據isActive決定要不要綁activeClass,errorClass則必會綁定。
```
### ▎縮寫
同樣的可以縮寫
```htmlembedded
<img :src="imgSrc">
```
## ▊ v-if & v-show | 條件渲染
這是vue兩種常用的條件渲染方式,可以使用data內的屬性來控制`<template>`中的元素會不會出現,下面是[範例](https://codepen.io/nojdeluj-the-looper/pen/ExGNOvX):
:::spoiler **打開查看程式碼**
```javascript=
<template>
<div id="app">
<p v-if="isLogin">我有登入!</p>
<p v-show="isAdmin">我是管理員</p>
<button @click="isAdmin = !isAdmin">點我切換管理員身分</button>
<!-- v-on的縮寫和inline寫法 -->
</div>
</template>
<script>
export default {
data() {
return {
isAdmin: false,
isLogin: true
};
}
}
</script>
```
:::
> * 結果
 
### ▎v-show vs. v-if
* `v-show`是單純控制css display屬性,效能會比較好。
* `v-if`可以用在`<template>`(子元件)的顯示上;`v-show`不能。
* `v-if`可以搭配`v-else-if`、`v-else`使用。
```htmlembedded
<template>
<div id="app">
<p v-if="isLogin">我有登入!</p>
<p v-else>我沒登入喔喔喔喔喔!</p>
</div>
</template>
```
:::warning
**注意**:`v-else`要緊接在`v-if`後面,不然會顯示錯誤
:::
## ▊ v-for | 渲染複數元件
v-for指令可以利用你存放在data屬性內的值做元件渲染,下面是[示範](https://codepen.io/nojdeluj-the-looper/pen/WNLoYZN):
:::spoiler **打開查看程式碼**
```javascript=
<template>
<div id="app">
<h1>V-for demo</h1>
<ul>
<li v-for="item in items" :key="item.id">{{item}}</li>
</ul>
</div>
</template>
<script>
export default {
data() {
return {
items: ["Learn vue","Buy diner", "Make a todo list"]
};
}
};
```
:::
> * `v-for="item in items"`的item可以自由命名,後方是data屬性中的陣列/物件。
> * 使用`v-for`時,務必利用`v-bind`來綁定key值 (`:key="item.id"`),key值的目的是清楚分辨每一個`v-for`render出的元素。
> * 結果會render出三個`<li>`元素
> 
也可以用下面的寫法:
```javascript
<li v-for="(item, index) in items" :key="item.id">{{index}}: {{item}}</li>
```
> * `index`可以自由命名
> * 結果會是
> 
也可以用在物件上,使用慘數value、key、index,示範如下:
:::spoiler **打開查看程式碼**
```javascript=
<template>
<div id="app">
<h1>V-for demo</h1>
<ul>
<ul>
<li v-for="(value,key,index) in person">
{{key}} : {{value}}
</li>
</ul>
</ul>
</div>
</template>
<script>
export default {
data() {
return {
person: {
name:'Danny',
age: 29,
gender: 'male'
}
};
}
};
</script>
```
:::
> * 結果
> 
## ▊ v-model | 雙向綁定
### ▎單向綁定 vs. 雙向綁定
#### ▏單向綁定
`v-bind`就是單向綁定,改變vue實體的值會反應在view,但無法反過來。
#### 雙向綁定
當畫面或資料有更新,對方也會隨之更新,例如`v-model`,下面是[範例](https://codepen.io/nojdeluj-the-looper/pen/dywOQVK):
:::spoiler **打開查看程式碼**
```javascript=
<template>
<div id="app">
<h1>{{message}}</h1>
<input type="text" v-model="message">
</div>
</template>
<script>
export default {
data() {
return {
message: '你好'
};
}
};
</script>
```
:::
> * 使用`v-model`對message進行雙向綁定,不管是修改input欄位的值或是修改vue實數的值,都會產生對應的變化
> * 結果
> 
#### ▏練習:利用v-model模擬表單送出
:::spoiler **打開查看程式碼**
```javascript=
<template>
<div id="app">
<div class="form-container">
<form>
<h2>模擬送出表單</h2>
<div class="input-group">
<label for="username">請輸入帳號: </label>
<input type="text" name="username" v-model="username" v-focus>
</div>
<div class="input-group">
<label for="username">請輸入密碼: </label>
<input type="text" name="username" v-model="password">
</div>
<button @click.prevent="handleSubmit">送出</button>
</form>
</div>
</div>
</template>
<script>
export default {
data() {
return {
username: '',
password: ''
};
},
methods: {
handleSubmit() {
alert(`你所輸入的帳號是: ${this.username} \n而你所輸入的密碼是${this.password}`);
}
},
directives: {
focus: {
inserted: function (el) {
el.focus()
}
}
}
};
</script>
```
> * `v-focus`為vue實體中自定義的directives
> * 結果
> 
:::
## ▊ 自定義directives
寫法如下:
```javascript
directives: {
// 你的客製化指令名稱
focus: {
// 你想在哪個時間點執行什麼操作
inserted: function (el) {
el.focus()
}
}
}
```
> directives屬性中提供了各種hook,可以設定什麼時候操作指令:
> * **`bind`**:只在指令被綁到指定元素時,執行一次傳入的callback
> * **`inserted`**:當綁定元素被插入DOM中就執行傳入的callback
> * **`update`**:當包含綁定元素的VNode被更新時,執行傳入的callback
> 傳入的callback的參數:
> * **`el`**:綁定的元素
> * **`binding`**:包含指令名稱、值和傳入參數的物件
> * **`vnode`**:vue產出的虛擬節點
## 中間練習
學習完Vue的屬性和directives後,來做一個練習題:[實作一個計算機](https://hackmd.io/@broodfish/HJx-h3HAh)!
## ▊ Vue Lifecircle Hooks

紅框為主要的階段 (Hooks),其中包括:
* beforeCreate
* created
* beforeMount
* mounted
* beforeUpdate
* updated
* beforeDestroy
* destroyed
另外還有用於keep-alive標籤的兩個階段:
* activated
* deActivated
### ▎不同hook該做的事
我們需要在特定的階段執行特定的操作,避免出錯,例如載入前運行loading動畫、載入後執行某個函數...等。
* **beforeCreate**
* 初始化Vue實體、元件之後,數據觀測和event/watcher事件前。
* 例如:加入loading動畫
* **created**
* 實體、元件建立完成,建立的屬性也綁定到該實體,可以讀取data內的值。
* DOM元素尚未完成,且`$el`元素還不存在。
* 例如:結束loading,做初始化來實現函數執行
* **beforeMount**
* Vue實體被掛載之前的階段,`$el`尚未存在。
* **mounted**
* 建立`$el`,並掛載至創建的Vue實體,產生出頁面。
* DOM元素已產生,可以操作元素。
* 例如:通常在mounted 或 created階段向後端發出API請求。
* **beforeUpdate**
* 在數據被更新前的階段,數據已經更新,但DOM還沒渲染。
* **updated**
* 數據更新導致DOM重新渲染,並且View為重新渲染的結果。
* **beforeDestroy**
* Vue實體要被銷毀前的階段,此階段實體還可以使用。
* 例如:做最後的詢問
* **destroyed**
* 銷毀Vue實體,實體也會解除所有綁定,子實體也會被銷毀。
* 例如:清空相關的內容
* **activated**
* 若HTML標籤內有設定keep-alive,就會觸發這階段的函數,並跳過destroy階段。
* **deActivated**
* 停用keep-alive時會觸發的階段。
### ▎呼叫hook
寫法和vue屬性一樣
```javascript
<script>
export default {
data() {
return {
};
},
methods: {
}
},
beforeCreate(){
},
created() {
}
};
</script>
```
## ▊ Vue components
Vue提供了元件化的概念,讓網頁有良好的結構,以此方便管理,每一個Vue元件其實就是一個Vue實體。
data, methods, computed, watch等屬性在元件中也能使用
使用下列方式註冊Vue元件:
```javascript
Vue.component('元件名稱', {
傳入的選項
});
```
<br>
例如重複的內容可以把它包裝成一個元件,下面的範例是將重複出現的todo-list包裝成一個global的元件:
:::spoiler **打開查看程式碼**
```javascript=
<template>
<div id="app">
<h1>My Todo List</h1>
<todo-list v-for="todo in todos" :key="todo.id">
</todo-list>
</div>
</template>
<script>
const todos = [
{
title: "Get dressed",
isComplete: false
},
{
title: "Buy food",
isComplete: false
},
{
title: "Eat lunch",
isComplete: true
},
{
title: "Write Article",
isComplete: true
}
];
Vue.component('todo-list',{
template:
`
<div class="todo-wrapper">
<div class="todo-title">
test
</div>
<div class="todo-icons">
<i class = "fa fa-check" aria-hidden="true"></i>
<i class = "fa fa-trash-alt" aria-hidden="true"></i>
</div>
</div>
`
});
export default {
data() {
return {
todos: todos
};
},
methods: {}
};
</script>
```
:::
> 可以減少重複出現的todo-list格式,接下來需要將資料傳進todo-list這個子元件
> -> **使用props屬性來接收父層的資料傳遞進子層**
---
### ▎props屬性
設定props屬性可以讓這個元件接受來自父層傳入的資料,完整的props分為幾個部分:
1. 傳入的props名稱
2. 傳入的props資料型態
3. 傳入的選項
寫法如下:
```javascript
props: ['paramA', 'paramB']
```
<br>
也可以使用物件形式,給傳入的資料增加一些選項:
```javascript
props: {
paramA : {
type: Object, // 指定傳進來的參數必須是物件,否則報錯
required: true, // 此欄位必填
default: {} // 預設值為空物件
}
}
```
> * **`type`**:指定此參數的type
> * **`required`**:此參數是否必須傳入
> * **`default`**:此參數的預設值
<br>
所以上面的範例加上props後會如下:
:::spoiler **打開查看程式碼**
```javascript=
Vue.component('todo-list',{
template:
`
<div :class="[todo.isComplete? 'success':'error','todo-wrapper']">
<div class="todo-title">
{{todo.title}}
</div>
<div class="todo-icons">
<i class = "fa fa-check" aria-hidden="true"></i>
<i class = "fa fa-trash-alt" aria-hidden="true"></i>
</div>
</div>
`,
props: {
todo: {
type: Object,
required: true
}
}
});
```
:::
<br>
寫好props接收資料後,再使用 **`v-bind`** 將父層資料傳遞到子元件,寫法如下:
```javascript
v-bind: paramA="item"
```
> * `paramA`:props裡設定好接收資料的參數
> * `item`:要傳遞的資料
<br>
所以範例中`<template>`的todo-list要加上`v-bind`,寫法如下:
:::spoiler **打開查看程式碼**
```javascript=
<todo-list v-for="todo in todos" :key="todo.id" :todo="todo"></todo-list>
```
:::
<br>
最後[範例](https://codepen.io/nojdeluj-the-looper/pen/xxmgwPX)的結果如下:

---
### ▎emit | 客製事件
#### ▏emit & on
:::danger
Vue 3 實體不提供`emit`和`$on`函數了,在Vue 3使用請[參考此文章](https://juejin.cn/post/6890781300648017934)。
:::
* emit可以建立一個全新的事件,例如click, load, change等事件。
* on則是監聽到該事件時,該做什麼事。
子層觸發事件的寫法:
```javascript
// 子層元件
this.$emit('eventName') // 向上層傳一個叫做eventName的事件
```
父層監聽到事件的處理:
```javascript
// 父層
<componentName v-on:eventName="負責處理這事件的函數"></componentName>
...
methods: {
負責處理這事件的函數() {
// 略
}
}
```
<br>
若需要觸發事件並傳送資料到父層,可以將資料寫在第二個參數,寫法如下:
```javascript
this.$emit('eventName', {
paramA: 'abc'
})
```
<br>
父層 & 子層之間的資料流動如下:
```
props傳遞資料
------------>
父層 子層
<------------
emit傳達客製事件
```
> 子層是沒辦法直接影響父層的資料的
<br>
接續上面的todo-list範例,在子層加一個delete-todo事件,父層間聽到後刪除toods裡的資料,並重新render:
:::spoiler **打開查看程式碼**
```javascript=
Vue.component('todo-list',{
template:
`
<div :class="[todo.isComplete? 'success':'error','todo-wrapper']">
<div class="todo-title">
{{todo.title}}
</div>
<div class="todo-icons">
<i class = "fa fa-check" aria-hidden="true"></i>
<i class = "fa fa-trash-alt" aria-hidden="true" @click="deleteTodo"></i>
// 在子層綁一個v-on,當點擊時觸發deleteTodo
</div>
</div>
`,
props: {
todo: {
type: Object,
required: true
}
},
methods: {
deleteTodo() {
this.$emit('delete-todo', { //emite一個叫delete-todo的事件
title: this.todo.title //並回傳todo.title
})
}
}
});
```
:::
<br>
接著在父層監聽子層的事件:
:::spoiler **打開查看程式碼**
```javascript=
<todo-list v-for="todo in todos" :key="todo.id" :todo="todo" @delete-todo="handleDeleteTodo">
</todo-list>
...
methods: {
handleDeleteTodo(payload) {
console.log(payload.title);
this.todos = this.todos.filter(item => item.title !== payload.title)
}
}
```
:small_red_triangle_down: handleDeleteTodo可以再精簡
```javascript=
methods: {
handleDeleteTodo({ title }) {
console.log(title);
this.todos = this.todos.filter(item => item.title !== title)
}
}
```
:::
[範例的demo](https://codepen.io/nojdeluj-the-looper/pen/GRProMJ)
---
### ▎slot
slot可以讓元件內有一個插槽,可以插入內容,讓元件更泛用。
#### ▏用法
宣告一個子元件結構,內含slot:
```javascript
Vue.component("compName", {
template: `
<div>
<slot></slot>
</div>
`,
});
```
父層使用此元件:
```
<template>
<div>
<compName>
<h1>TEST1234</h1>
</compName>
<compName>
test5678
</compName>
</div>
</template>
```
父層渲染後的結果會長這樣

渲染完的DOM結構長這樣:
```javascript
<div>
<div>
<h1>TEST1234</h1>
</div>
<div>
test5678
</div>
</div>
```
#### ▏named slot 命名插槽
一個元件可以有多個slots,透過`name`屬性對每個slot命名,父層就可以在指定的槽插入內容。
子層寫多個slots:
```javascript
Vue.component("compName", {
template: `
<div>
<slot name="name1"></slot>
<slot name="name2"></slot>
<slot></slot>
</div>
`,
});
```
父層使用下面的寫法指定元件的slot槽:
```javascript
<template>
<div>
<compName>
<template #name1>
<h1>test1</h1>
</template>
<p>test2</p> //沒指定會插入slot預設位置
<template #name2>
<p>test3</p>
</template>
</compName>
</div>
</template>
```
渲染結果如下:

:::warning
* 若父層指定了特定的slot name,但子層沒有對應的slot name的話,則會無法插入。
* 若父層沒有指定slot name,但子層沒有預設的(沒命名的)slot,不管還有沒有slot沒插入,都會無法插入。
:::
## ▊ Single File Components (SFC)
SFC是一個完整的vue檔案,每個vue檔案就是一個完整的元件,包含三部分:
1. HTML (template)
2. Javasript (script)
3. CSS (style)
```javascript
<template>
<div>
<!--HTML-->
</div>
</template>
<script>
// JAvaScript
</script>
<style>
/*CSS*/
</style>
```
### ▎WHY use SFC?
用global component的方式會有幾個問題:
* 全域命名,所以元件名稱不能重複,當專案規模很大時會很麻煩。
* 不支援CSS
* template使用字串方式撰寫,閱讀和編寫不易。
=> 使用SFC可以解決這些問題,SFC還支援webpack打包建置的服務
### ▎`<template>` 特性
#### ▏只能用一個根元素 (root element)
必須長這樣,所有內容要用一個容器包起來:
```javascript
<template>
<div>
<!--略-->
</div>
</template>
```
#### ▏template預設的markup為HTML
若要使用其他樣板,可以使用`lang`屬性改寫:
```javascript
<template lang="pug">
div
Hello world
</template>
```
### ▎`<script>` 特性
#### ▏一個SFC只能有一個`<script>`
#### ▏最終要輸出成一個Vue實體
#### ▏使用`require`/`import`來引入外部的js檔案
```javascript
import dayjs from 'dayjs' // 引入npm套件
import {helper} from '../utils.js' // 以相對路徑引入自己專案內的js檔案
export default {
data() {
return {
};
},
methods: {
},
computed: {
},
mounted() {
}
};
</script>
```
### ▎`<style>` 特性
#### ▏style預設是全域管理
若只在此元件中套用style,可以加入`scoped`屬性
``` javascript
<style scoped>
/*CSS*/
</style>
```
#### ▏使用`lang`套用預處理器
像是`scss`或`stylus` (需安裝對應的預處理器和loader)
```javascript
<style lang="stylus" scoped>
/*CSS*/
</style>
```
### ▎在SFC中套用其他元件
在`<script>`內使用`import`載入指定的元件:
```javascript
import ComponentA from '../src/components/A.vue'
```
```javascript
//A.vue
export default {
components: {
ComponentA
}
};
```
這樣在`<template>`就可以使用該元件:
```
<template>
<div>
<component-a></component-a>
</div>
</template>
```
:warning: :warning: :warning: 不管原本是怎麼命名的,要在`<template>`使用須轉為kebab-case、小寫,並以`-`相連。
:::warning
MyApp 轉為-> my-app
componentA 轉為-> component-a
:::
## ▊ vue-cli
接下來使用vue-cli練習,[vue-cli環境建立方法](https://hackmd.io/@broodfish/HybyuZPA2)。
## ▊ EventBus
`props`和`emit`只能做父層與子層之間的資料傳遞,做不到子層與子層之間的溝通,所以需要使用**EventBus**的概念。

EventBus會做為所有元件的客製事件的監聽者,以此達到子層之間的溝通。
### ▎Vue 3
因為Vue 3不提供`emit`, `$on`, `$off`等函數,所以改用`mitt`套件。------------[教學1](https://www.casper.tw/development/2020/12/15/vue-3-mitt/) [教學2](https://juejin.cn/post/6957965225471508493)
## ▊ store
store的概念是所有資料的存放與變動都需要在管理資料的倉庫中完成,不能在其他元件變動倉庫內的資料。
## ▊ Vuex
Vuex分為`state`、`mutations`、`actions`等屬性

> 流程:
> * Components 透過State內的資料render出畫面
> * 資料需要變動時,透過Actions commit一個Mutation
> * Mutation 變更 State中的資料
> * Components 重新render變化後的結果
### ▎在vue-cli專案中加入vuex套件
在終端機輸入:`vue add vuex`,安裝vuex套件。
> `vue add 套件名稱` 是在vue-cli中安裝套件的其中一個方法
安裝好後,
* `main.js`會新增這行:`import store from './store'`
* 新增`store/index.js`檔案
### ▎State
儲存資料的物件
```javascript
state: {
numbers: [1,3,5,7,9]
}
```
### ▎Mutations
包含函數的物件,負責接收actions並更改state資料。
mutations有幾個特點:
* 必定是同步函數
* 是vuex中唯一可以改動state的方法
要改變state需要有以下的流程:
1. dispatch an action|發出一個action
2. commit a mutation|接收到action後,執行對應的mutaion
3. 透過mutation更改state
這樣的做法可以確保不同的函數要操作同一個state資料的順序,會先在action中處理可能的非同步請求,當取得對應資料後再透過mutation以同步處理的方式變更state的資料。
每個mutation包含兩個參數:
1. `state`:可以自由取用或變動state的數值
2. `payload`:從actions傳來的參數
> 若action不預期傳參數則可以省略不寫
```javascript
mutations: {
ADD_NUMBER(state, payload) {
state.numbers.push(payload)
}
}
```
### ▎Actions
用來呼叫mutations,actions有幾個特性:
* 可以是非同步函數
* 一個action可以觸發多個mutations
actions包含兩個參數:
1. `context`:是一個物件,裡面可以使用store中的`commit`, `getter` 或 state屬性
2. `number`:想傳入mutation的參數,沒有可以省略
```javascript
actions: {
addNumber(context,number) {
context.commit("ADD_NUMBER", number)
}
}
```
只需要使用某個屬性的話也可以寫成下面的方式:
```javascript
actions: {
addNumber({commit},number) {
commit("ADD_NUMBER", number)
}
}
```
### ▎getters
用來計算state (類似vue實體中的computed)
```javascript
getters: {
sortedNumbers(state) {
return state.numbers.sort((a, b) => a - b)
},
}
```
> 使用`state`作為第一個參數
也可以寫成箭頭函數
```javascript
getters: {
sortedNumbers: (state) => state.numbers.sort((a, b) => a - b)
}
```
:::spoiler **複習箭頭函式的寫法**
基本寫法:
```javascript
(參數1, 參數2, …, 參數N) => { 陳述式; }
(參數1, 參數2, …, 參數N) => 表示式;
// 等相同(參數1, 參數2, …, 參數N) => { return 表示式; }
```
<br>
只有一個參數時,括號才能不加:
```javascript
(單一參數) => { 陳述式; }
單一參數 => { 陳述式; }
```
<br>
若無參數,就一定要加括號:
```javascript
() => { statements }
```
<br>
用大括號將內容括起來,返回一個物件字面值表示法:
```javascript
params => ({foo: bar})
```
<br>
支援其餘參數與預設參數:
```javascript
(param1, param2, ...rest) => { statements }
(param1 = defaultValue1, param2, …, paramN = defaultValueN) => {
statements }
```
<br>
也支援 parameter list 的解構:
```javascript
var f = ([a, b] = [1, 2], {x: c} = {x: a + b}) => a + b + c; f(); // 6
```
:::
### ▎在組件中使用`state`, `actions`, `getters`
```javascript
this.$store.state.numbers
this.$store.dispatch('某action')
this.$store.commit('某mutation')
this.$store.getters.sortedNumbers
```
### ▎`mapState`, `mapActions`, `mapGetters`
可以讓`state`, `actions`, `getters`更簡化
從vuex引入:
```javascript
import { mapState,mapActions,mapGetters } from 'vuex'
export default {
data() {
}
}
```
#### ▏用`mapState`, `mapGetters`取出`state`, `computed`屬性
```javascript
computed: {
...mapState(['numbers', 'user', 'isLoading']),
...mapGetters(['sortedNumbers']),
自定義的computed屬性() {
}
}
```
> **`...`**:**擴展運算子**,可以展開陣列,轉化成多個逗點相隔的獨立參數
> :::spoiler **複習擴展運算子**
> * 寫法
> ```javascript
> ...arrayName
> ```
> * 範例
> ```javascript
> function foo(a, b, c) {
> console.log(a + b + c);
> }
> let arr = [10, 20, 30];
>
> foo(...arr); // 60
> // 等同於
> foo.apply(null, arr); // 60
> ```
> * 特性
> * 可以嵌在陣列裡:
> ```javascript
> let a1 = ['x', 'y'];
> let a2 = ['w', ...a1, 'z'];
> console.log(a2); // ['w', 'x', 'y', 'z']
> ```
> * 可以用來複製陣列:
> ```javascript
> let a1 = [1, 2];
> let a2 = [...a1];
> console.log(a2); // [1, 2]
> ```
> * 將字串展開為各單一字元:
> ```javascript
> let text = [...'Hello'];
> console.log(text); // ['H', 'e', 'l', 'l', 'o']
> ```
> * [參考教學](https://ithelp.ithome.com.tw/articles/10195477)
> :::
#### ▏用`mapActions`取出`methods`屬性
```javascript
methods: {
...mapActions(['addNumber']),
你的某些自定義methods屬性() {
}
}
```
## ▊ Vue Router
前端模擬路由的套件,達成切換網址時也會切換元件。
因為vue框架是SPA (Single Page Application),不希望每一次頁面變化時都向後端發送請求,只希望變化頁面,用模擬路由的方式就可以很好的達成。

### ▎在vue-cli專案中加入router
在終端機輸入:`vue add router`,安裝router。
vue-cli會問是否使用history mode,選`y`代表採用HTML5的history API管理前端路由
> 可以透過 `pushState()` , `replaceState()` 的方式更新 URL, 以及原本就有的 `history.go()` , `history.back()` 來指定頁面的路徑切換,同時也提供了 state 物件來讓開發者暫存與讀取每一頁的狀態。
安裝後會新增:
* `view`, `router`兩個資料夾
* `App.vue`也有被修正
### ▎Vue Router 路由設定
`router/index.js`可以看到這段:

<br>
**`createRouter()`** 有兩個部分:
* `history` 路由模式
* `routes` 路由比對
#### ▏`history` 路由模式
分為兩種:
* Hash mode
* HTML mode
Vue router預設是HTML mode
#### ▏`routes` 路由比對
`routes`是陣列型態,用來處理路徑和Vue實體元鑑比對的設定。
```javascript
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes: [
{ path: '/', component: Home},
{ path: '/about', component: About},
],
})
```
> * `path: '/', component: Home`:代表`path: '/'`對應到`Home.vue`,也就是在專案`http://localhost:8080/`的URL下,`<router-view>`顯示的是`Home.vue`的內容。
> * `path: '/about', component: About`:在專案`http://localhost:8080/about`的URL下,`<router-view>`顯示的是`About.vue`的內容。
<br>
* `path`:字串格式,用來表示對應的url,`'/'`表示根路由。
* `name`:除了`path`外,也可以用`name`來匹配對應的元件。
* 若想配合`<router-link>`來匹配顯示的元件,必須以`v-bind`傳入物件的方式。
```javascript
<router-link :to="{name: 'Home'}">Home</router-link>
```
* `name`屬性具有唯一性,不能重名。
* 雖然是用`name`去匹配,但實際上還是由`name`找到對應的`path`,所以url仍會顯示`/path`
* `component`:可以用兩種載入方式
* 利用`import`將元件先行載入
```javascript
import Home from '../views/Home.vue'
```

* 在`routes`內載入
```javascript
component: () => import('../views/About.vue')
```
### ▎`router-link` & `router-view`
```javascript=
// App.vue
<template>
<nav>
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link>
</nav>
<router-view/>
</template>
```
在`App.vue`我們可以看到`<router-link>` 和 `<router-view>`
#### ▏`router-view`
`router-view`是要顯示的元件,當路由變化時,會去更新`router-view`這個元件,藉由路由或`name`屬性來控制要顯示哪個view。
```javascript
// 基本使用,自動預設name屬性為default
<router-view />
// 命名使用,此範例會強制渲染名為home的組件
<router-view name="home"/>
```
#### ▏`<router-link>`
用來切換連結的元件,實際上會渲染出一個`<a>`tag,做到hyperlink的效果。

### ▎動態路由
router也提供了類似後端路由的功能,透過URL的動態路徑來讓不同路徑都能指向同一個Vue元件實體。

#### ▏Params
假設要有` localhost:8080/users/1`~` localhost:8080/users/5`5個url,可以寫成:
```javascript
routes: [{ path: '/users/:userId', component: User}]
```
* `:userId`可以自由命名,表示`/users/`後面的數值會傳入`userId`。
* 對應的元件是`User.vue`
* `User`元件可以透過`this.$route.params.userId`取得url的`userId`
<br>
`<router-link>`要生成5個連結,就可以寫成:
```javascript
<template>
<ul>
<li v-for="i in 5" :key="i">
<router-link :to="`/users/${i}`"> /users/{{i}} </router-link>
</li>
</ul>
<router-view/>
</template>
```
#### ▏query
`query.string`是指url問號後面的部分,可以利用`$route.query`來取得`query.string`。
例如:
* url為`localhost:8080/users?genger=male&age=25`
* `this.$route.query`會得到`{ gender:'male', age: '25'}`。
#### ▏`route`提供的屬性
* `$route.params`:存取自定義的變數
* `$route.query`:存取`location.search`
* `$route.hash`:存取`location.hash`
* `$route.path`:存取`location.pathname`
#### ▏`alias` 別名
以陣列的型態傳入,若是有匹配到任一別名,也能顯示對應的頁面。
```javascript
{ path: '/', name: 'Home', alias: ['home', 'homepage'], component: Home }
```
> `localhost:8080/`、`localhost:8080/home`、`localhost:8080/homepage` 都會顯示`Home`元件的內容。
#### ▏`redirect` 重新導向
```javascript
{
path: '/',
name: 'Home',
alias: ['home', 'homepage'],
component: Home,
redirect: '/about'
}
```
可以用命名 (`name`) 、物件或函式的寫法:
```javascript
// url
redirect: '/about'
// 命名
redirect: About
// 物件
redirect: { About }
// 函式
redirect: from => return '/'
```
#### ▏自訂路由參數格式
可以在`path`內,透過**正規表達式 (regexp)** 指定`param`裡的格式。
```javascript
const routes = [
{path: '/:orderId(\\d+)'}
]
```
> `/:orderId`只能是數字
#### ▏非強制的路由參數
若是`userId`這個參數可有可無,可以使用`?`:
```javascript
routes: [{ path: '/users/:userId?', component: User}]
```
>`localhost:8080/users`就能順利匹配到`User`元件
#### ▏路由比對失敗 (找不到網頁)
當使用者嘗試用不存在的URL進行讀取時,可以透過`/:pathMatch(.*)*`來指定所有的路由都會連到此元件。
```javascript
const routes = [
{ path: '/:pathMatch(.*)*', name: 'NotFound', component: NotFound}
]
```
:::warning
含`*`的路由應該放在所有的路由最後面,避免原本正確的url還沒匹配就轉到`NotFound`。
:::