# 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) ![](https://hackmd.io/_uploads/HJt8zqBAn.png) 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!。 ![](https://hackmd.io/_uploads/SkAKYDOA2.png =200x) --- ### ▎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。 ![](https://hackmd.io/_uploads/B1wUYvOC2.png =400x) 可以理解為: ```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> ``` ::: > * 結果: > ![](https://hackmd.io/_uploads/r1TVBnL03.png =500x) > * `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> ``` ::: > * 結果 ![初始](https://hackmd.io/_uploads/rkLotsS0n.png =200x) ![點一下後](https://hackmd.io/_uploads/B1K6tsHRn.png =210x) ### ▎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>`元素 > ![](https://hackmd.io/_uploads/Hks1-2BAn.png =200x) 也可以用下面的寫法: ```javascript <li v-for="(item, index) in items" :key="item.id">{{index}}: {{item}}</li> ``` > * `index`可以自由命名 > * 結果會是 > ![](https://hackmd.io/_uploads/Sk7-zhB03.png =230x) 也可以用在物件上,使用慘數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> ``` ::: > * 結果 > ![](https://hackmd.io/_uploads/HkxdNhrC2.png =200x) ## ▊ 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實數的值,都會產生對應的變化 > * 結果 > ![](https://hackmd.io/_uploads/ByE1_2BRn.png =200x) #### ▏練習:利用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 > * 結果 > ![](https://hackmd.io/_uploads/SyCRu3BC2.png) ::: ## ▊ 自定義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 ![](https://hackmd.io/_uploads/rJoFfaUAh.png) 紅框為主要的階段 (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)的結果如下: ![](https://hackmd.io/_uploads/r1vFi1wR3.png =250x) --- ### ▎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> ``` 父層渲染後的結果會長這樣 ![](https://hackmd.io/_uploads/HJ3c9gwC3.png =180x) 渲染完的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> ``` 渲染結果如下: ![](https://hackmd.io/_uploads/Bkoz6xP03.png =150x) :::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**的概念。 ![](https://hackmd.io/_uploads/ByJUZzPAn.png) 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`等屬性 ![](https://hackmd.io/_uploads/HyP8g-O03.png) > 流程: > * 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),不希望每一次頁面變化時都向後端發送請求,只希望變化頁面,用模擬路由的方式就可以很好的達成。 ![](https://hackmd.io/_uploads/Sk91DEuAh.png) ### ▎在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`可以看到這段: ![](https://hackmd.io/_uploads/rksbTVO0h.png =400x) <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' ``` ![](https://hackmd.io/_uploads/BkBydLuRh.png =450x) * 在`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的效果。 ![](https://hackmd.io/_uploads/r1wgzI_03.png =600x) ### ▎動態路由 router也提供了類似後端路由的功能,透過URL的動態路徑來讓不同路徑都能指向同一個Vue元件實體。 ![](https://hackmd.io/_uploads/ryyxcIuA3.png =400x) #### ▏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`。 :::