--- title: 'Vue.js 筆記' disqus: Pai --- # 壹、目錄及代辦事項 ## Table of Contents [TOC] # 貳、基礎內容文件要點說明 ## 一、Vue 開發環境介紹 Vue 是由資料來驅動畫面的,因此資料是不斷在變動的。 這邊就要用 Vue Dev tools 來檢視資料。 ## 二、建立應用程式 - 使用el綁定ID - 直接使用 el:"#app" 掛載 - 在需要時再使用 $mount("#app") 掛載的功能 - Vue的應用程式不能巢狀排列 ```html= <div id="app"> {{ text }} </div> <script> var app = new Vue({ el: '#app', data: { text: '這是一段話' } }); </script> ``` - 呈現程式碼在頁面的方法 < pre >< /pre > ```htmlmixed= <div id="app"> <pre>{{ list }}</pre> </div> <script> var app = new Vue({ el: '#app', data: { list: [ { name: '小明', age: 16 }, { name: '媽媽', age: 38, }, { name: '漂亮阿姨', age: 24 } ] } }) </script> ``` ## 三、雙向綁定的資料 1. v-model : 透過花括號{{}}方式綁定文字,基本上 v-model 只會用在 input, textarea, select 這些傳入資訊的元素 2. v-text : 透過指令的方式增加文字 3. v-html : 透過指令的方式增加文字及Html標籤 ```htmlmixed= <div id="app"> {{message}} <div v-text="text1"></div> <div V-html="text2"></div> <input type="text" v-model="message"> </div> <script> var app = new Vue({ el: '#app', // 在此建立資料內容 data: { message: '哈囉', text1: 'v-text', text2: '<h1>html-text</h1>', } }) ``` ## 四、MVVM 的概念 VueJS是以資料狀態操作畫面 ![](https://i.imgur.com/0NF2bq7.png) ## 五、v-bind 動態屬性指令 ### 1.透過 v-bind 添加 html 屬性,類似 setAttribute。但是這樣說可能會有點差距,因為 vue 裡面是使用property 來綁定 HTML 屬性,而不是 attribute。 ```htmlmixed= <div id="app"> <img v-bind:src="imgSrc" v-bind:class="className" alt=""> </div> <script> var app = new Vue({ el: '#app', data: { //加上IMG連結 imgSrc: 'https://images.unsplash.com/photo-1479568933336-ea01829af8de?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=d9926ef56492b20aea8508ed32ec6030&auto=format&fit=crop&w=2250&q=80', className: 'img-fluid', //加上class } }) </script> ``` ### 2.v-bind 內可以放入表達式 表達式:()有值的 ```javascript= <input type="check" v-bind:check = "count%2===0"> // (count%2===0) 值為 true or false // count 為偶數時才打勾 ``` ## 六、v-for 、 v-if ### 1. v-for: 動態產生多筆資料於畫面上 當括號中有不同筆數的資料,變數也代表不同意思 ``` v-for="(itemValue,item,index) in array" v-for="(item,index) in array" v-for="item in array" ``` - itemValue : 回傳的是陣列中的屬性值。冒號:後 - item : 回傳的是陣列中的屬性名稱。冒號:前 - index : 回傳的是陣列中的索引值 ### 2. v-if: 過濾資料條件 ### 避免將 v-for 、 v-if 放在同一層內。 ```htmlmixed= <div id="app"> <ul class="mb-0"> //v-for 內宣告有什麼會用在 list 內, v-if 過濾顯示的條件 <li v-for="(item, index) in list" > <span v-if="item.age>25">{{ index+1 }} - {{ item.name }} 今年 {{ item.age }}歲....</span> </li> </ul> </div> <script> var app = new Vue({ el: '#app', data: { list: [ { name: '小明', age: 16 }, { name: '媽媽', age: 38, }, { name: '漂亮阿姨', age: 24 } ] } }) </script> // result : 2 - 媽媽今年38歲.... ``` ## 七、使用 v-on 來操作頁面行為 ### 1.按下按鈕後,觸發文字反轉的行為並顯示文字 - 使用this來選取這個app裡面的屬性 - v-on:click搭配methods內的函式來觸發動作 - 可以觸發多個函式 v-on:click="firstFunction(); secondFunction(); - v-on要使用的資料一定要在data中預先定義好 ```htmlmixed= <div id="app"> <input type="text" class="form-control" v-model="text"> <button class="btn btn-primary mt-1" v-on:click = "reverseText">反轉字串</button> <div class="mt-3"> {{ newText }} </div> </div> <script> var app = new Vue({ el: '#app', data: { text: '', newText: '' }, methods: { reverseText : function() { this.newText = this.text.split('').reverse().join('');//split('')先轉為陣列,reverse()反轉陣列,join('')重組回字串 } }, }); </script> ``` ### 2. 可以把函式內容寫在 v-on 裡面 ```htmlmixed= <p>請切換下方 box 的 className</p> <div class="box" :class="{'rotate': isRotate }"></div> <hr> //將 v-on 觸發後的動作寫在 @click 裡面 <button class="btn btn-outline-primary" @click="isRotate= !isRotate">切換 box 樣式</button> //呼叫函式執行動作 <button class="btn btn-outline-primary" @click="changeRotate">切換 box 樣式</button> data: { isRotate: false, }, methods: { changeRotate: function() { this.isRotate = !this.isRotate; }, } }); ``` ### 3. v-on 帶入參數 每按一次按鈕,item.cash 的錢就會加五百 ```javascript= <h4>帶入參數</h4> <ul> <li v-for="item in arrayData" class="my-2"> {{ item.name }} 有 {{ item.cash }} 元 <button class="btn btn-sm btn-outline-primary" @click="storeMoney(item)">儲值</button> </li> </ul> methods: { storeMoney: function(item) { item.cash = item.cash + 500; }, } ``` ## 八、透過修飾符,讓 v-on 操作更簡單 ### 1. v-on:click後面的修飾符 - .stop 停止 event.stopPropagation()。停止冒泡事件,由內而外 button -> box -> div - .prevent 停止預設動作 - .capture 添加事件偵聽器時使用 capture 模式。 由外而內 div -> box -> button - .self 只觸發本身事件 - .once 只觸發一次事件 - .passive ```javascript= <!-- 阻止单击事件继续传播 --> <a v-on:click.stop="doThis"></a> <!-- 提交事件不再重载页面 --> <form v-on:submit.prevent="onSubmit"></form> <!-- 修饰符可以串联 --> <a v-on:click.stop.prevent="doThat"></a> <!-- 只有修饰符 --> <form v-on:submit.prevent></form> <!-- 添加事件监听器时使用事件捕获模式 --> <!-- 即内部元素触发的事件先在此处理,然后才交由内部元素进行处理 --> <div v-on:click.capture="doThis">...</div> <!-- 只当在 event.target 是当前元素自身时触发处理函数 --> <!-- 即事件不是从内部元素触发的 --> <div v-on:click.self="doThat">...</div> ``` ### 2. 鍵盤修飾符 - .enter - .tab - .delete (捕获“删除”和“退格”键) - .esc - .space - .up - .down - .left - .right ```javascript= <h5>按鍵修飾符</h5> <ul> <li>.{keyCode | keyAlias} - 只當事件是從特定鍵觸發時才觸發回調。</li> <li>別名修飾 - .enter, .tab, .delete, .esc, .space, .up, .down, .left, .right</li> <li>修飾符來實現僅在按下相應按鍵時才觸發鼠標或鍵盤事件的監聽器 - .ctrl, .alt, .shift, .meta</li> </ul> <h6 class="mt-3">keyCode</h6> <input type="text" class="form-control" v-model="text" @keyup.13="trigger(13)"> <h6 class="mt-3">別名修飾</h6> <input type="text" class="form-control" v-model="text" @keyup.space="trigger('space')"> <h6 class="mt-3">相應按鍵時才觸發的監聽器</h6> <input type="text" class="form-control" v-model="text" @keyup.shift.enter="trigger('shift + Enter')"> ``` ### 3.滑鼠修飾符 ```javascript= <h5>滑鼠修飾符</h5> <ul> <li>.left - (2.2.0) 只當點擊鼠標左鍵時觸發。</li> <li>.right - (2.2.0) 只當點擊鼠標右鍵時觸發。</li> <li>.middle - (2.2.0) 只當點擊鼠標中鍵時觸發。</li> </ul> <h6 class="mt-3">滑鼠修飾符</h6> <div class="p-3 bg-primary"> <span class="box" @click.middle="trigger('Right button')"> </span> </div> ``` ### 2.縮寫 - v-on的縮寫 @ - v-bind的縮寫 : ```htmlmixed= <div id="app"> <input type="text" class="form-control" v-model="text" @keyup.enter="reverseText"> <a :href="src" class="btn btn-primary mt-1" @click.prevent="reverseText">反轉字串</a> <div class="mt-3"> {{ newText }} </div> </div> <script> var app = new Vue({ el: '#app', data: { text: '', newText: '', src:'https://www.google.com/' }, // 請在此撰寫 JavaScript methods: { reverseText(event) { this.newText = this.text.split('').reverse().join(''); } } }); </script> ``` # 九、 v-class 動態切換 className ## 動態增加class - @class="{ '要加入的className' : 判斷式true/false } ```htmlmixed= <div id="app"> <div class="box" :class="{ 'rotate' : isTransform }"></div> <hr> <button class="btn btn-outline-primary" @click = "isTransform = ! isTransform ">選轉物件</button> </div> <script> var app = new Vue({ el: '#app', data: { isTransform: false }, }); </script> <style> .box { transition: transform .5s; } .box.rotate { transform: rotate(45deg) } </style> ``` ## 十、computed 運算功能 ### 1. computed 內必須是 function ### 2. 一定要回傳值 return ### 3. 如果不是在 this 內,computed無法被觸發 ### 4. computed 不能傳參數 function( para ) ### 5. computed 變更條件是資料更動時 ```htmlmixed= <div id="app"> <input type="text" class="form-control" v-model="text"> <button class="btn btn-primary mt-1">反轉字串</button> <div class="mt-3"> {{ reverseText }} </div> </div> <script> var app = new Vue({ el: '#app', data: { text: '', newText: '' }, computed: { reverseText: function() { return this.text.split('').reverse().join(''); } }, }); </script> ``` ## 十一、Methods 與 Computed 的使用情境 ### 1. computed 是在監控資料更動後,重新運算結果呈現於畫面上 一般來說不會修改資料,只會回傳用於畫面呈現的資料 ### 2. methods 就是互動的函式,需要觸發才會運作 會用來修改資料內容,會建議把 Method 作為主動處發的方式,就比較不會搞混(如 click) ### 3. 效能 如果資料量大,computed 自然會比較慢 只要資料變動就會觸發,無形之中執行次數也會增加勒 因此在大量資料時,會建議透過 methods 減少不必要的運算喔 ## 十二、Vue 表單與資料的綁定 ### 1. checkbox、radio、selected 的 v-model 會去綁定 value 內的值 ### 2. 在input內type="checkbox"的v-model 由於開發者設定了 type="checkbox",所以自動的對應到 checked 屬性的監聽, 相對於 type="text" 來說,一個是監聽 value,另一個則是監聽 checked 屬性。 但假如v-mode的資料型態有不一樣,也會去做另外的相對應。 ``` v-mode:array data: { array: []; } ``` 資料型態是陣列的話,checkBox打勾後,他會去抓value的值。 ``` v-mode:completed data: { completed: false || ''; } ``` 資料型態是布林或字串的話,checkBox打勾後,他會顯示true或false。 ```htmlmixed= <div id="app"> <h4>字串</h4> {{ text }} <input type="text" class="form-control" v-model="text"> <hr> <pre>{{ textarea }}</pre> <textarea cols="30" rows="3" class="form-control" v-model="textarea"></textarea> <hr> <h4>Checkbox 與 Radio</h4> <div class="form-check"> <input type="checkbox" class="form-check-input" id="check1" v-model="checkbox"> <label class="form-check-label" for="check1"> ... </label> </div> <hr> <div class="form-check"> <input type="checkbox" class="form-check-input" id="check2" value="雞" v-model="checkboxArray"> <label class="form-check-label" for="check2">雞</label> </div> <div class="form-check"> <input type="checkbox" class="form-check-input" id="check3" value="豬" v-model="checkboxArray"> <label class="form-check-label" for="check3">豬</label> </div> <div class="form-check"> <input type="checkbox" class="form-check-input" id="check4" value="牛" v-model="checkboxArray"> <label class="form-check-label" for="check4">牛</label> </div> <p>晚餐火鍋裡有 <span v-for="item in checkboxArray"> {{ item }}</span></p> <hr> <div class="form-check"> <input type="radio" class="form-check-input" id="radio2" value="雞" v-model="singleRadio"> <label class="form-check-label" for="radio2">雞</label> </div> <div class="form-check"> <input type="radio" class="form-check-input" id="radio3" value="豬" v-model="singleRadio"> <label class="form-check-label" for="radio3">豬</label> </div> <div class="form-check"> <input type="radio" class="form-check-input" id="radio4" value="牛" v-model="singleRadio"> <label class="form-check-label" for="radio4">牛</label> </div> <p>晚餐火鍋裡有 {{singleRadio}}</p> <hr> <h4>Select</h4> <select name="" id="" class="form-control" v-model="selected"> <option value="" disabled>--請選擇--</option> <option v-for="item in list" :value="item.value">{{ item.name }}</option> </select> </div> <script> var app = new Vue({ el: '#app', data: { text: '', textarea: '', checkbox: false, checkboxArray: [], singleRadio: '', selected: '', list: [ { name: '小明', value: 'MING', }, { name: '小胖', value: 'PONG', }, ] }, }); </script> ``` ### 3. select 結合 v-for #### a. 記的 value 是動態加入的,要綁定 :value #### b. 如果要讓選項變成多選,可以在 select 標籤中加入 multiple ```htmlmixed= <select name="" id="" class="form-control" multiple v-model="selected"> <option disabled value="">請選擇</option> <option :value="item" v-for="item in selectData">{{ item }}</option> </select> <p>小明喜歡的女生是 {{ selected }}。</p> data: { selected: '', multiSelected: [], }, ``` ### 4. 複選框 #### 加入 true-value 和 false-value 可以讓 sex 值在因為 true or false 改變 ```javascript= <h4 class="mt-3">複選框</h4> <div class="form-check"> <input type="checkbox" class="form-check-input" id="sex" v-model="sex" true-value="male" false-value="female"> <label class="form-check-label" for="sex">{{ sex }}</label> </div> ``` ### 5. 修飾符介紹 #### a. v-model.lazy 類似 on-click 功能,在離開 focus 或是按下 enter 時,字串才輸出 #### b. v-model.number 確認輸出的數字型態是數值,而不是字串 #### c. v-model.trim 去除頭尾的空白 ```javascript= <h4 class="mt-3">修飾符</h4> {{ lazyMsg }} <input type="text" class="form-control" v-model.lazy="lazyMsg"> <br> <pre>{{ age }}</pre> <input type="number" class="form-control" v-model.number="age"> <br> {{ trimMsg }}緊黏的文字 <input type="text" class="form-control" v-model.trim="trimMsg"> ``` ## 十三、元件基礎概念 ### 1. vue 的 conponent 中,每個元件都可以獨立的儲存自己的狀態 ### 2. 元件必須先定義,後面的程式碼才讀取的到,所以要擺在前面。 ### 3. 試著定義一個元件 **Vue.component('元件名稱' , data{function} , 資料呈現的方式)** ```htmlmixed= <div id="app"> <div> <counter-component></counter-component> </div> </div> <script> // 請在此撰寫 JavaScript Vue.component('counter-component', { data: function() { return{ counter : 0 , //可以回傳多個item buffer : 1, } }, template: ` <button class="btn btn-outline-secondary btn-sm" @click="counter += 1">{{ counter }}</button> ` }, ) ``` # 貳、 製作一個 Todo List 來小試身手吧 ### 參考 Medium [文章](https://medium.com/%E5%89%8D%E7%AB%AF%E4%B9%8B%E7%9C%BC-%E4%B8%96%E7%95%8C%E9%80%99%E9%BA%BC%E5%A4%A7-%E6%88%91%E8%A6%81%E5%8E%BB%E7%9C%8B%E7%9C%8B%E5%89%8D%E7%AB%AF%E7%9A%84%E4%B8%96%E7%95%8C/vue%E5%87%BA%E4%B8%80%E5%80%8Btodolist-39cb8abb5f83) # 參、進階模板語法介紹 ## 一、模板資料細節說明 ### 1. v-model="text" - 為雙向綁定的。 - v-model 是綁定在表單元件或自訂元件上。表單元件像是<input>、<select>和<textarea> ### 2. v-text="text"、{{ text }} 與 v-once - 為雙向綁定。 - 綁定輸入文字的內容。如div、p、span... - 如果不想雙向綁定,則在<>內加入 v-once,就只會出現 data 初始的內容 ### 3. v-html - 使用 v-html 可以插入有作用的 Html 語法。但要注意使用的地方可能造成的XSS攻擊,像是讓客戶輸入的資料就不建議使用。 ### 4. 表達式 {{ }} - 串接兩段文字 {{ text+rawHtml }} - 直接實現js語法 {{ text.split('').reverse().join('') }} - 運算 {{ number1+number2 }} ### 5. 改變 input 的屬性 ```javascript= <input type="text" :disabled="isDisabled" class="form-control" placeholder="請在此加上動態 disabled"> </div> ``` - :disabled 的資料為 true 時,會使這個 input disable。 ## 二、 動態切換 ClassName 及 Style 多種方法 ### 1. 物件寫法 將物件寫在 :class 裡面 ```javascript= <h4>物件寫法 1</h4> <div class="box" :class="{'rotate':isTransform , 'bg-danger':boxColor}"></div> <p>請為此元素加上動態 className</p> <hr> <button class="btn btn-outline-primary" v-on:click="isTransform = !isTransform">選轉物件</button> <div class="form-check"> <input type="checkbox" class="form-check-input" id="classToggle1" v-model="boxColor"> <label class="form-check-label" for="classToggle1">切換色彩</label> </div> var app = new Vue({ el: '#app', data: { isTransform: false, boxColor: false, } } ) ``` ### 2. 物件寫法2 將物件寫在data裡面 ```javascript= <h5>物件寫法 2</h5> <div class="box" :class="objectClass"></div> <p>請將此範例改為 "物件" 寫法</p> <hr> <button class="btn btn-outline-primary" @click="objectClass.rotate = !objectClass.rotate">選轉物件</button> <div class="form-check"> <input type="checkbox" class="form-check-input" id="classToggle2" v-model="objectClass['bg-danger']"> <label class="form-check-label" for="classToggle2">切換色彩</label> </div> data: { objectClass: { 'rotate': false, 'bg-danger':false, } } ``` ### 3. 陣列寫法 先在 data 裡面宣告一個空陣列,在 input 勾選時,再將 value 一一加到陣列裡面 ```javascript= <h4>陣列寫法</h4> <button class="btn" :class="arrayClass">請操作本元件</button> <p>請用陣列呈現此元件 className</p> <div class="form-check"> <input type="checkbox" class="form-check-input" id="classToggle3" v-model="arrayClass" value="btn-outline-primary"> <label class="form-check-label" for="classToggle3">切換樣式</label> </div> <div class="form-check"> <input type="checkbox" class="form-check-input" id="classToggle4" v-model="arrayClass" value="active"> <label class="form-check-label" for="classToggle4">啟用元素狀態</label> </div> data: { arrayClass: [], }, ``` ### 4. 綁定行內樣式 :style = {樣式屬性:"樣式的值"} 樣式屬性採駝峰式寫法 backgroundColoe,或是用單引號刮起來 'background-color' ```javascript= <div class="box" :style="{backgroundColor:'red'}"></div> <div class="box" :style="styleObject"></div> <div class="box" :style="[{backgroundColor:'red'},{borderWidth:'5px'}]"></div> <div class="box" :style="[styleObject,styleObject2,styleObject3]"></div> data: { styleObject: { backgroundColor: 'red', borderWidth: '5px', }, styleObject2: { boxShadow: '3px 3px 5px rgba(0, 0, 0, 0.16)' }, styleObject3: { userSelect: 'none' } }, ``` ### 5. Vue 會為不同瀏覽器加上 Prefix ## 三、v-for 與其使用細節 ### 1. index (索引) 可以不只是數字,當物件有鍵值時就會把數字替換成鍵值 - 將 v-for 後面補上(item, key ,index)三個屬性,就能提取物件的屬性、鍵值和索引。 ```javascript= //這個index會是數字 <p>請使用 v-for 在陣列與物件上,並且加上數字索引</p> <ul> <li v-for="(item, index) in arrayData"> {{ index }} - {{ item.name }} {{ item.age }} 歲 //這邊的 key 就會是 0,1,2 </li> </ul> <ul> //讀取到的陣列前方有鍵值,就會變成 index 文字 <p>請使用 v-for 在陣列與物件上,並且加上文字索引</p> <ul> <li v-for="(item, index) in objectData"> {{ index }} - {{ item.name }} {{ item.age }} //這邊的 key 就會是 ming,auntie,jay </li> </ul> <ul> //如果 for 迴圈裡面 (item, key ,index) 都有,就能讀取到鍵值和索引值 <p>請使用 v-for 在陣列與物件上,並且加上數字索引和鍵值</p> <ul> <li v-for="(item, key ,index) in objectData"> {{ index }} - {{key}} - {{ item.name }} {{ item.age }} </li> </ul> <ul> //0 - ming - 小明 16 歲 //1 - auntie - 漂亮阿姨 24 歲 //2 - jay - 杰倫 20 歲 data: { arrayData: [ { name: '小明', age: 16 }, { name: '漂亮阿姨', age: 24 }, { name: '杰倫', age: 20 } ], objectData: { ming: { name: '小明', age: 16 }, auntie: { name: '漂亮阿姨', age: 24 }, jay: { name: '杰倫', age: 20 } }, }, ``` ### 2. 避免 DOM 元素快速置換的方法 當 Vue.js 用v-for正在更新已渲染過的元素列表時,它默認用“就地複用”策略。如果數據項的順序被改變 **(以這單元為例,是指 arrayData 資料順序被翻轉改變)** Vue 將不會移動 DOM 元素來匹配數據項的順序 **(以這單元為例,是指 input 元素順序不會跟著 arrayData 資料新的順序改變位置)** 而是簡單複用此處每個元素 **(繼續使用原本的 input 元素)**,並且確保它在特定索引下顯示已被渲染過的每個元素 **(也就是顯示、渲染原本順序的 input 元素)** 為了給 Vue 一個提示,以便它能跟蹤每個節點的身份 **(每個元素在 DOM 裡面就是一個節點,所以就是要給 Vue 一個提示,讓 Vue 能追蹤到 input 元素)** 從而重用和重新排序現有元素 **(重新排序 input 元素的順序,讓它跟著 arrayData 資料新的順序改變位置)** 你需要為每項提供一個唯一key屬性 **(提供一個唯一key屬性給 input 元素,讓 Vue 能依據唯一的key屬性追蹤 input 元素,進而讓它跟著 arrayData 資料新的順序改變位置 )** 理想的key值是每項都有的唯一 id 所以 :key 是避免 就地複用(快速替換),直接做強制替換喔 而就地複用就是不替換實體 DOM 來達到,因為在 JS 中,抽換 DOM 是相當耗費效能的 替 key 綁定上專屬的變數,這樣後面的 input 的資料也會跟著變換 ```javascript= <p>請在範例上補上 key,並觀察其差異</p> <ul> <li v-for="(item,index) in arrayData" :key="item.age"> {{index}} - {{ item.name }} {{ item.age }} 歲 <input type="text"> </li> </ul> <button class="btn btn-outline-primary" @click="reverseArray">反轉陣列</button> //反轉陣列 methods: { // 請在此練習 JavaScript reverseArray: function() { this.arrayData.reverse() } ``` ### 3. 使用 v-for 過濾陣列 - filter ( function(item, index, array ) {.... reutrn true;} #### a. 指向 data 要使用 this,但是在 v-for 裡面有使用函式 filter 會使 this 指向全域 window,不管是 filter、forEach、find 等處理陣列行為的方法裡面的 this 都是指向 window,因為他們是 callBack function,回呼函式的 this 都會只到 globe,因此要宣告 vm [參考文章](https://www.hexschool.com/2017/09/01/2017-09-01-javascript-for/) #### b. filter 的 return 會回傳 ture 的內容。如果 matach 的比對有吻合,在 return 裡面就會回傳 ```javascript= <p>請輸入姓名,程式會自動選取有在陣列內的名字</p> <input type="text" class="form-control" v-model="filterText" @keyup.enter="filterData"> <ul> <li v-for="(item, key) in filterArray" :key="item.age"> {{ key }} - {{ item.name }} {{ item.age }} 歲 <input type="text"> </li> </ul> data: { arrayData: [ { name: '小明', age: 16 }, { name: '漂亮阿姨', age: 24 }, { name: '杰倫', age: 20 } ], filterArray: [], filterText: '' }, methods: { filterData : function(){ let vm = this; //指向 data 要使用 this,但是在 v-for 裡面會指向全域 window,因此要宣告 vm vm.filterArray = vm.arrayData.filter(function(item){ console.log(vm.filterText, item.name , item.name.match(vm.filterText)); return item.name.match(vm.filterText); //return 會回傳 turn 的內容。如果 matach 的比對有吻合,在 return 裡面就會回傳 }) } } ``` ### 4. v-for 無法運行的狀況 #### a. 不能直接把陣列長度變成 0 來刪除陣列。 ```javascript= cantWork: function(){ this.arrayData.length = 0; console.log(this.arrayData.length); } ``` #### b. 無法直接對這個陣列的索引修改資料 ```javascript= cantWork: function(){ this.arrayData[0] = { name : 小強, age : 99, } } ``` 如果要修改要使用 Vue.set(target,key,value) ```javascript= Vue.set(this.arrayData[0]{ name : '小強', age : '99', }) ``` #### c. 使用純數字的迴圈 ```javascript= <h4>純數字的迴圈</h4> <ul> <li v-for="item in 20"> {{ item }} </li> </ul> ``` #### d. Template 在 v-for 中的運用 template 是在 JS 中渲染而已,不會出現在 HTML 裡面,考慮到語意,使用 template 會比較好, 如果將 v-for 放在 tbody 裡面,這樣 tbody 會一直重複出現,對語意不好。 ```javascript= <h4>Template 的運用</h4> <p>請將兩個 tr 一組使用 v-for</p> <table class="table"> <template v-for="item in arrayData"> <tr> <td>{{ item.name }}</td> </tr> <tr> <td>{{ item.age }}</td> </tr> </template> </table> ``` #### e. 使用 v-for 時,建議都要綁定 :key ,可以使用 id,不重複的值 [v-for連結參考](https://cn.vuejs.org/v2/guide/list.html#%E4%B8%80%E4%B8%AA%E7%BB%84%E4%BB%B6%E7%9A%84-v-for) ```javascript= <h4>v-for 與 元件</h4> <p>講師說明</p> <ul> <list-item :item="item" v-for="(item, key) in arrayData"></list-item> </ul> <p>注意:現在建議元件使用 v-for 都加上 key。</p> ``` ## 四、 v-if 、v-else 與 v-else-if ### 1. v-if 、v-else 的運用 前一兄弟元素必需有 v-if 或 v-else-if 才能使用 v-else ```html= <h4>v-if, v-else</h4> <p>使用 v-if, v-else 切換物件呈現</p> <div class="alert alert-success" v-if="isSuccess">成功!</div> <div class="alert alert-danger" v-else>失敗!</div> <div class="form-check"> <input type="checkbox" class="form-check-input" id="isSuccess" v-model="isSuccess"> <label class="form-check-label" for="isSuccess">啟用元素狀態</label> </div> ``` ### 2. template 標籤 #### a.我們需要使用 Vue 指令,但我們希望這個標籤不要被輸出,就可以使用 template。 template 標籤像是在人口販子,在背後操作 HTML 的消失與出現,而他自己則是從來不出現。 #### b. :key 要綁定在真實的節點上,而 template 是 Vue 的虛擬節點,不能綁定 :key #### a. 使用 template 配合 v-if 來控制 tr、td 是否出現 ```htmlmixed= <h4>template 標籤</h4> <p>使用 template 切換多數 DOM 呈現</p> <table class="table"> <thead> <th>編號</th> <th>姓名</th> </thead> <template v-if="showTemplate"> <tr> <td>1</td> <td>安妮</td> </tr> <tr> <td>2</td> <td>小明</td> </tr> </template> ``` ### 3. v-else-if 的運用 ```htmlmixed= <p>使用 v-else-if 做出分頁頁籤</p> <ul class="nav nav-tabs"> <li class="nav-item"> <a class="nav-link" href="#" :class="{'active':link == 'a'}" @click.prevent="link = 'a'">標題一</a> </li> <li class="nav-item"> <a class="nav-link" href="#" :class="{'active':link == 'b'}" @click.prevent="link = 'b'">標題二</a> </li> <li class="nav-item"> <a class="nav-link" href="#" :class="{'active':link == 'c'}" @click.prevent="link = 'c'">標題三</a> </li> </ul> <div class="content"> <div v-if = "link == 'a'">A</div> <div v-else-if = "link == 'b'">B</div> <div v-else-if = "link == 'c'">C</div> </div> ``` ### 4. v-if 與 v-show 的差異 v-if 會把不符合條件的 DOM 移除 v-show 則是把不符合條件的 DOM 加上 display : none ```htmlmixed= <h4>v-if 與 v-show</h4> <p>講師說明 v-if 與 v-show 的差異</p> <div class="alert alert-success" v-if="isSuccess">成功!</div> <div class="alert alert-danger" v-if="!isSuccess">失敗!</div> <div class="form-check"> <input type="checkbox" class="form-check-input" id="isSuccess2" v-model="isSuccess"> <label class="form-check-label" for="isSuccess2">啟用元素狀態</label> </div> ``` ### 5. key #### a. :key 必須綁定在真實的節點上 #### b. :key 綁定固定字串時,可以不使用: ```htmlmixed= <input class="form-control" placeholder="Enter your email address" key="2"> <input class="form-control" placeholder="Enter your username" key="1"> ``` #### c. :key 通常是綁定不同的 id ,或是不重複的變數 ```htmlmixed= v-for="(item, m) in array" :key="m" ``` ## 五、 Computed 與 Watch ### 1. computed #### a. computed 是不用經過主動的觸發就可以計算的功能,像 @click。 computed 有點像 RPG 裡面的被動技能。但記得 computed 的使用都得使用 return 回傳值。 #### b. computed 如果處理大量的資料會有效能上的問題 #### c. Computed 無法帶參數,methods 可以 #### d. 不確定每次都會更新,請用 Computed。確定每次都會更新,就用 methods。 #### e. computed 只會在相關的資料變動下才會更新 - 試著變動 input 內的資料,取得現在秒數 - 因為 computed 內的 now Fn 有寫到 var total = this.a+this.b,因此 a 或 b 的資料有變動,資料都會更新 <iframe height="265" style="width: 100%;" scrolling="no" title="computed 只會在有相關的資料變動下才會改變" src="https://codepen.io/PaiP/embed/BaNWxyd?height=265&theme-id=light&default-tab=html,result" frameborder="no" allowtransparency="true" allowfullscreen="true"> See the Pen <a href='https://codepen.io/PaiP/pen/BaNWxyd'>computed 只會在有相關的資料變動下才會改變</a> by 白家齊 (<a href='https://codepen.io/PaiP'>@PaiP</a>) on <a href='https://codepen.io'>CodePen</a>. </iframe> #### f. 使用 Computed 來過濾資料 ```htmlmixed= <p>使用 Computed 來過濾資料。</p> <input type="text" class="form-control" v-model="filterText"> <ul> <li v-for="(item, key) in filterArray" :key="item.age"> {{ key }} - {{ item.name }} {{ item.age }} 歲 <input type="text"> </li> </ul> data: { arrayData: [ { name: '小明', age: 16 }, { name: '漂亮阿姨', age: 24 }, { name: '杰倫', age: 20 } ], filterText: '', }, computed: { filterArray : function(){ let that = this; return that.arrayData.filter(function(item){ return item.name.match(that.filterText); }) }, }, }) ``` #### g. match 只能用在字串上面,如果要搜尋年紀要改成 ```javascript= return that.arrayData.filter(function(item){ return item.age == that.filterText; }) ``` ### 2.使用 Computed 來呈現時間格式 #### a. timeStamp 轉換文章[連結](https://medium.com/@nourished_brown_fox_845/timestamp-%E7%9A%84%E6%99%82%E9%96%93%E8%BD%89%E6%8F%9B-c5aeef6b1480) ```javascript= <p>使用 Computed 來呈現時間格式。</p> <p>{{ formatTime }}</p> //Vue el: '#app', data: { newDate: 0 }, computed: { formatTime: function () { console.log(this.newDate) var dates = new Date(this.newDate * 1000); var year = dates.getFullYear(); var month = dates.getMonth() + 1; var date = dates.getDate(); var hours = dates.getHours(); var minutes = dates.getMinutes(); var seconds = dates.getSeconds(); return `${year}/${month}/${date} ${hours}:${minutes}:${seconds}` }, }, mounted: function () { this.newDate = Math.floor(Date.now() / 1000); }, ``` ### 3. Watch Watch 可讓我們監視某個值,當這個值變動的時候,就去做某些事情。 ```htmlmixed= <h4>Watch</h4> <p>使用 trigger 來觸發旋轉 box、並在三秒後改變回來</p> <div class="box" :class="{'rotate': trigger }"></div> <hr> <button class="btn btn-outline-primary" @click="trigger = true">Counter</button> data: { trigger: false, }, watch:{ trigger: function(){ let that = this; setTimeout(() => { that.trigger = false; }, 3000); } }, ``` # 肆、Vue的生命週期 # 伍、Vue.js 元件 ## 一、使用 x-template 建立元件 ### 1. 建立全域的元件 #### a. 使用 Vue.component #### b. 宣告一段 type="text/x-template",並設置 id 指向 Vue.component 內的 template。 #### c. 因為 tbody 內必須放 tr,因此要寫為 tr is ="row-component" ,將 row-component 動態替換掉 tr。 #### d. Vue.component 和 #app 的資料是分開的,因此要藉由 props: ['person'] 宣告一個物件來傳遞資料。因此,原本的 item 會把資料傳到 person 裡面,person 將資料存入 props,讓元件獲得資料。 ![](https://i.imgur.com/AimIpQp.png) ```htmlmixed= <div id="app"> <table class="table"> <thead> </thead> <tbody> <tr is ="row-component" v-for="(item, key) in data" :person="item" :key="key"></tr> </tbody> </table> </div> <script type="text/x-template" id="rowComponentTemplate"> <tr> <td>{{ person.name }}</td> <td>{{ person.cash }}</td> <td>{{ person.icash }}</td> </tr> </script> <script> Vue.component('row-component',{ props: ['person'], template: '#rowComponentTemplate' }) var app = new Vue({ el: '#app', data: { data: [ { name: '小明', cash: 100, icash: 500, }, { name: '杰倫', cash: 10000, icash: 5000, }, { name: '漂亮阿姨', cash: 500, icash: 500, }, { name: '老媽', cash: 10000, icash: 100, }, ] } }); </script> ``` ### 2、建立區域的元件 #### a. 將原本全域的 Vue.component 改為另宣告一個應用,將原本 props 和 template 值放在裡面 #### b. 在 app 中在宣告 components: 對應到 child。 ```htmlmixed= <script> var child = { props: ['person'], template: '#rowComponentTemplate' } var app = new Vue({ el: '#app', data: { data: [ { name: '小明', cash: 100, icash: 500, }, { name: '杰倫', cash: 10000, icash: 5000, }, { name: '漂亮阿姨', cash: 500, icash: 500, }, { name: '老媽', cash: 10000, icash: 100, }, ] }, components: { "row-component": child } }); </script> ``` ### 3.使用 function return 建構資料格式 Vue.component 內必須有 function return ```htmlmixed= <script> Vue.component('counter-component', { data: function(){ return { counter: 0 } }, template: '#counter-component' }) ``` ### 4. props 基本觀念 #### a. 由外而內動態傳入資料 #### b. 如果 props 使用的是小駝峰寫法 imgUrl,那在 html 內要改成 img-url ```htmlmixed= <div id="app"> <h2>靜態傳遞</h2> <photo img-url="https://images.unsplash.com/photo-1522204538344-922f76ecc041?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=50e38600a12d623a878983fc5524423f&auto=format&fit=crop&w=1351&q=80"></photo> //這邊是靜態傳入,因為圖片連結是寫死的。 <h2>動態傳遞</h2> <photo :img-url="url"></photo> //這邊 :img-url 是動態傳入,網址可依 app data 內的內容改變變動 </div> <script type="text/x-template" id="photo"> <div> <img :src="imgUrl" class="img-fluid" alt="" /> //這邊因為傳入的網址會改變,是動態的,不論靜態傳入或動態傳入,每次放的 photo 網址連結可能都不同,所以要使用 v-bind <p>風景照</p> </div> </script> <script> Vue.component('photo', { props: ['imgUrl'], template: '#photo', }) var app = new Vue({ el: '#app', data: { url: 'https://images.unsplash.com/photo-1522204538344-922f76ecc041?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=50e38600a12d623a878983fc5524423f&auto=format&fit=crop&w=1351&q=80' } }); </script> ``` ### 5. props 使用上的注意事項 #### a. 修正單向數據流所造成的錯誤 ```htmlmixed= <h2>單向數據流</h2> <photo :img-url="url"></photo> <p>修正單向數據流所造成的錯誤</p> <script type="text/x-template" id="photo"> <div> <img :src="imgUrl" class="img-fluid" alt="" /> <input type="text" class="form-control" v-model="newUrl"> </div> </script> <script> Vue.component('photo', { props: ['imgUrl'], template: '#photo', ////多新增 data 內容 newUrl 去承接 input 回傳回來的數據//// data: function(){ return{ newUrl: this.imgUrl, } } }) </script> ``` ![](https://i.imgur.com/x8eg7be.png) 在 Vue 上看就會呈現兩筆資料 #### b. 物件傳參考特性 及 尚未宣告的變數 **尚未宣告的變數** - 透過 傳入的資料會因為會有傳入時間差,導致 props 找不到資料而跳錯誤。 - 解決方式可以透過在 HTML 內加入 v-if="一定會出現在 ajax 傳進來的資料",讓元件產生的時間往後移,當資料傳入完成後,在產生元件 **物件傳參考特性** - 因為物件傳參考特性,當修改 card 內的物件屬性,在原本 root 層的物件屬性也會改變。 ```htmlmixed= <h2 class="mt-3">物件傳參考特性 及 尚未宣告的變數</h2> <div class="row"> <div class="col-sm-4"> <card :user-data="user" v-if="user.phone"></card> </div> </div> <script type="text/x-template" id="card"> <div class="card"> <img class="card-img-top" :src="user.picture.large" alt="Card image cap"> <div class="card-body"> <h5 class="card-title">{{ user.name.first }} {{ user.name.last }}</h5> <p class="card-text">{{ user.email }}</p> </div> <div class="card-footer"> <input type="email" class="form-control" v-model="user.email"> </div> </div> </script> <script> Vue.component('card', { props: ['userData'], template: '#card', data: function () { return { user: this.userData } } }); var app = new Vue({ el: '#app', created: function() { var vm = this; $.ajax({ url: 'https://randomuser.me/api/', dataType: 'json', success: function(data) { vm.user = data.results[0]; } }); } }); </script> ``` - **比較 a 與 b,為什麼 a 會跳錯,b 不會 ?** 第一個單向數據流的 url 因為是「字串」的關係,所以在做更動的時候會跳錯誤;而第二個card的部分因為 data 是一個物件,所以有傳參考的特性 #### c. 維持狀態與生命週期 - 使用 keep-card 維持狀態 - keepalive 只能使用在元件上,如果使用在 HTML 標籤上 keepalive 會被強制移除 - 每個應用程式和元件都有獨自的生命週期 ```htmlmixed= <h2 class="mt-3">維持狀態與生命週期</h2> <div class="form-check"> <input type="checkbox" class="form-check-input" id="isShow" v-model="isShow"> <label class="form-check-label" for="isShow">Check me out</label> </div> <div class="row"> <div class="col-sm-4"> ////如果 v-if="isShow" 設定在外層把 <keep-card> 整個移除,就無法擁有保存生命週期的功能//// <keep-alive> <keep-card v-if="isShow"> </keep-card> </keep-alive> </div> </div> // 在三層以上(user.picture.large)會出現 not define 的錯誤 <script type="text/x-template" id="card"> <div class="card"> <img class="card-img-top" :src="user.picture.large" v-if="user.name" alt="Card image cap"> <div class="card-body"> <h5 class="card-title" v-if="user.name">{{ user.name.first }} {{ user.name.last }}</h5> <p class="card-text">{{ user.email }}</p> </div> <div class="card-footer"> <input type="email" class="form-control" v-model="user.email"> </div> </div> </script> <script> Vue.component('keepCard', { template: '#card', data: function() { return { user: {} } }, created: function() { var vm = this; $.ajax({ url: 'https://randomuser.me/api/', dataType: 'json', success: function(data) { vm.user = data.results[0]; } }); } }); </script> ``` ### 6. props 型別及預設值 #### a. props 型別 可以在 props 裡面設定型別,如果型別不對會跳錯 ```htmlmixed= <h2>Props 的型別</h2> <prop-type :cash="cash"></prop-type> <script> Vue.component('prop-type', { props: { cash: { type: Number, } }, template: '#propType', data: function() { return { newCash: this.cash } } }); var app = new Vue({ el: '#app', data: { cash: 300, } }); </script> ``` #### b. props 預設值 如果 template 沒有傳入任何值,可以在 props 裡面寫入預設值。 ```htmlmixed= <h2>Props 的型別</h2> <prop-type></prop-type> ////這邊沒有傳入任何值//// <script> Vue.component('prop-type', { props: { cash: { type: Number, default: 300, ////可以在這邊先預設預設值//// } }, template: '#propType', data: function() { return { newCash: this.cash } } }); </script> ``` #### c.靜態與動態傳入數值差異 - 在"靜態"傳入 cash="300" 的時候,cash 的內容會被判斷為字串 - 在"動態"傳入 :cash="300" 時,cash 的內容則會被判斷為數值, - 在"動態"傳入 :cash="true" 時,cash 的內容則會被判斷為布林 - 如果要寫入字串得加入'',改成 :cash="'300'",cash 的內容就會被判斷為字串 ```htmlmixed= <h2 class="mt-3">靜態與動態傳入數值差異</h2> <prop-type cash="300"></prop-type> ``` ### 7. emit 向外層傳遞事件 ![](https://i.imgur.com/mmBXFTR.png) 1. 先在父元件中定義一個名為 incrementTotal 的 method,incrementTotal 所執行的動作就是 this.cash 做累加。 2. 在 HTML 使用子元件標籤時,自訂一個觸發事件,並將在父元件的 method,也就是先前定義 incrementTotal 傳入作為監聽器,兩者間用 **v-on** 進行綁定。例如:``v-on:increment="incrementTotal"``,其中:increment 是自訂事件名稱,incrementTotal 則是父元件中的方法。 3. 為子元件 template 的 button 加上 click 事件 incrementCounter。而在該方法的實作中,我們會再使用 **emit** 去觸發自訂事件 **increment**,如:``this.$emit("increment")``。若有需要做參數傳遞,則將參數寫在逗號後面,不需要加“”,this.$emit("increment", Number(this.counter))。 ```htmlmixed= <div id="app"> <h2>透過 emit 向外傳遞資訊</h2> 我透過元件儲值了 {{ cash }} 元 <button class="btn btn-primary" @:click="incrementTotal">按一下</button> ////透過 @:increment="incrementTotal" 觸發外部的按鈕事件 <button-counter @:increment="incrementTotal"></button-counter> <hr> <button-counter></button-counter> </div> <script> Vue.component('buttonCounter', { template: `<div> ////透過 incrementCounter 觸發 template 的按鈕事件 <button @click="incrementCounter" class="btn btn-outline-primary">增加 {{ counter }} 元</button> <input type="number" class="form-control mt-2" v-model="counter"> </div>`, data: function() { return { counter: 1 } }, methods: { incrementCounter: function(){ this.$emit('increment',Number(this.counter)) } } }); var app = new Vue({ el: '#app', data: { cash: 300 }, methods: { incrementTotal: function(newNum){ this.cash = this.cash + newNum; } } }); </script> ``` ### 8. 元件插槽 - 藉由插槽讓元件內的模板可以替換 #### a. 單一插槽 如果只有一個標籤需要替換,就可以使用 slot 標籤 ```htmlmixed= <h2>Slot 基礎範例</h2> <single-slot-component></single-slot-component> ////出現預設文字: 如果沒有內容,則會顯示此段落。//// <single-slot-component> <p>使用這段取代原本的 Slot。</p> </single-slot-component> ////出現 slot 取代文字: 使用這段取代原本的 Slot。//// <script type="text/x-template" id="singleSlotComponent"> <div class="alert alert-warning"> <h6>我是一個元件</h6> <slot> 如果沒有內容,則會顯示此段落。 </slot> </div> </script> <script> Vue.component('single-slot-component', { template: '#singleSlotComponent', }); </script> ``` #### b. 具名插槽 當有大量的內容需要被取代,而且分散在元件的各處,就可以使用 "具名插槽" 。 - 在需要替換的標籤加入 slot="名字",如果是不要被顯示的標籤可以使用 template 代替 - 在 x-template 中,則可以使用<slot name="名字"></slot>對應 ```htmlmixed= <h2>具名插槽</h2> <named-slot-component> <header slot="header">替換的 Header</header> <template slot="footer">替換的 Footer</template> <template slot="btn">按鈕內容</template> <p>其餘的內容</p> </named-slot-component> <script type="text/x-template" id="namedSlotComponent"> <div class="card my-3"> <div class="card-header"> <slot name="header">這段是預設的文字</slot> </div> <div class="card-body"> <slot> <h5 class="card-title">Special title treatment</h5> <p class="card-text">With supporting text below as a natural lead-in to additional content.</p> </slot> <a href="#" class="btn btn-primary"> <slot name="btn">spanGo somewhere</slot> </a> </div> <div class="card-footer"> <slot name="footer">這是預設的 Footer</slot> </div> </div> </script> ``` ### 9.使用 is 動態切換元件 - 使用 <di v :is ="current " :data ="item"></div> 來動態切換兩個不同的 x-template。 - 當'藍綠色元件'按鈕按下時,current = 'primary-component', :is = "primary-component" - 當'紅色元件'按鈕按下時,current = 'primary-component', :is = "danger-component" ```htmlmixed= <h2 class="mt-3">使用 is 動態切換組件</h2> <ul class="nav nav-pills"> <li class="nav-item"> <a class="nav-link" :class="{'active': current == 'primary-component'}" href="#" @click.prevent="current = 'primary-component'">藍綠色元件</a> </li> <li class="nav-item"> <a class="nav-link" :class="{'active': current == 'danger-component'}" href="#" @click.prevent="current = 'danger-component'">紅色元件</a> </li> </ul> <div class="mt-3"> <!-- <primary-component :data="item" v-if="current === 'primary-component'"></primary-component> <danger-component :data="item" v-if="current === 'danger-component'"></danger-component> --> <div :is ="current" :data="item"></div> </div> </div> <script type="text/x-template" id="primaryComponent"> <div class="card text-white bg-primary mb-3" style="max-width: 18rem;"> <div class="card-header">{{ data.header }}</div> <div class="card-body"> <h5 class="card-title">{{ data.title }}</h5> <p class="card-text">{{ data.text }}</p> </div> </div> </script> <script type="text/x-template" id="dangerComponent"> <div class="card text-white bg-danger mb-3" style="max-width: 18rem;"> <div class="card-header">{{ data.header }}</div> <div class="card-body"> <h5 class="card-title">{{ data.title }}</h5> <p class="card-text">{{ data.text }}</p> </div> </div> </script> <script> Vue.component('primary-component', { props: ['data'], template: '#primaryComponent', }); Vue.component('danger-component', { props: ['data'], template: '#dangerComponent', }); var app = new Vue({ el: '#app', data: { item: { header: '這裡是 header', title: '這裡是 title', text: 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Enim perferendis illo reprehenderit ex natus earum explicabo modi voluptas cupiditate aperiam, quasi quisquam mollitia velit ut odio vitae atque incidunt minus?' }, current: 'primary-component' } }); </script> ``` # 陸、Vue 常用 API ## 一、使用 Extend 避免重複造輪子 下方的 childOne 和 childTwo 的內容大部分是一樣的,只差在 template: ,這時就可以建立一個 Vue.extend,把重複的內容放在裡面。child1 和 child2 再去取用就好。 #### 1. data 裡面原本就有的值,如果再寫一次就會覆蓋掉 #### 2. data 裡面原本沒有的值,寫上就會新增 ```javascript= var newExtend = Vue.extend({ data: function() { return { data: {}, extendData: '這段文字是 extend 得到' } }, template: '#row-component', filters: { dollarSign: function (n) { return `$ ${n}` }, currency: function(n) { return n.toFixed(2).replace(/./g, function(c, i, a) { return i && c !== "." && ((a.length - i) % 3 === 0) ? ',' + c : c; }); } }, mounted: function() { console.log('Extend:', this) } }) var childOne = { props: ['item'], extends: newExtend, } var childTwo = { props: ['item'], template: '#row-component-two', extends: newExtend, data: function() { return { extendData: '這段文字是 childTwo 得到', //覆蓋掉原本的 extendData apple: '蘋果'//新增的資料 } }, } ``` ## 二、Filter 自訂畫面資料呈現格式 ### 1. 區域使用方法 - 在要改變的變數上加上 **| (filter的變數名稱)** - 在 component 加上 **filters: (filter的變數名稱){方法}** ```javascript= <div id="app"> <table class="table"> <tbody> <tr is="row-component" v-for="(item, key) in data" :item="item" :key="key"></tr> </tbody> </table> {{ data[1].cash }} </div> <script type="text/x-template" id="row-component"> <tr> <td>{{ item.name }}</td> <td>{{ item.cash | currency | dollarSign}}</td> <td>{{ item.icash | currency | dollarSign }}</td> </tr> </script> <script> var child = { props: ['item'], template: '#row-component', data: function() { return { data: {} } }, filters: { dollarSign(n){ //加上錢字號,dollarSign: function(n) 縮寫 return `$ ${n}`; }, currency(n){ //加上千分位,currency: function(n) 縮寫 return n.toFixed(2).replace(/./g, function(c, i, a) { return i && c !== "." && ((a.length - i) % 3 === 0) ? ',' + c : c; }); } }, mounted: function() { console.log('Component:', this) } } var app = new Vue({ el: '#app', data: { data: [ { name: '小明', cash: 100, icash: 500, }, { name: '杰倫', cash: 10000, icash: 5000, }, { name: '漂亮阿姨', cash: 500, icash: 500, }, { name: '老媽', cash: 10000, icash: 100, }, ] }, components: { "row-component": child }, mounted: function() { console.log('Vue init:', this) } }); ``` ### 2. 全域使用方法 - 在 component 同樣外層的加上 **Vue.filter('filter 變數名稱',function(){filter 變數方法})** ```javascript= <div id="app"> <table class="table"> <tbody> <tr is="row-component" v-for="(item, key) in data" :item="item" :key="key"></tr> </tbody> </table> {{ data[1].cash | currency | dollarSign }} </div> <script type="text/x-template" id="row-component"> <tr> <td>{{ item.name }}</td> <td>{{ item.cash | currency | dollarSign}}</td> <td>{{ item.icash | currency | dollarSign }}</td> </tr> </script> <script> Vue.filter('dollarSign',function(n){ return `$ ${n}`; }) Vue.filter('currency',function(n){ return n.toFixed(2).replace(/./g, function(c, i, a) { return i && c !== "." && ((a.length - i) % 3 === 0) ? ',' + c : c; }); }) ``` ### 3. filter 使用範例 - [顯示資料](https://ithelp.ithome.com.tw/articles/10208812) - [建立分頁](https://kuro.tw/posts/2016/05/30/vuejs-in-v-for-through-the-filter-in-the-list-complete-search-and-page-functions/) ## 三. 無法寫入的資料,用 set 搞定他 ### 1. 有時使用 AJAX 時,無法先定義 data 內的資料內容,可以用 set 來強制寫入 ### 2. Vue.set (target, propertyName/index, value) - target : 目標 - index: 索引 - value: 值 ```javascript= <div id="app"> <table class="table"> <tbody> <tr is="row-component" v-for="(item, key) in data" :item="item" :key="key"></tr> </tbody> </table> </div> <script type="text/x-template" id="row-component"> <tr> <td>{{ item.name }}</td> <td>{{ item.cash }}</td> <td>{{ item.icash }}</td> <td> <span v-if="data.item">{{ data.item.name }}</span> <button class="btn btn-sm btn-primary" @click="addData()">寫入資料</button> </td> </tr> </script> <script> var child = { props: ['item'], template: '#row-component', data: function() { return { data: {} //這邊沒有資料內容,用 set 寫入後會變成 //,data: {item:{name:小明}} } }, methods: { addData: function() { // this.data.item = { // name: this.item.name // } // console.log(this.data, this); this.$set(this.data, 'item', { //使用 set 來寫入為定義的資料 name: this.item.name }); console.log(this.data, this); } }, mounted: function() { console.log('Component:', this) } } var app = new Vue({ el: '#app', data: { data: [ { name: '小明', cash: 100, icash: 500, }, { name: '杰倫', cash: 10000, icash: 5000, }, { name: '漂亮阿姨', cash: 500, icash: 500, }, { name: '老媽', cash: 10000, icash: 100, }, ] }, components: { "row-component": child }, mounted: function() { console.log('Vue init:', this) } }); </script> ``` ### 3. [補充資料1](https://pjchender.blogspot.com/2017/05/vue-vue-reactivity.html)、[補充資料2](https://ithelp.ithome.com.tw/articles/10206422) ## 四、 Mixin 混合其它的元件內容 ### 1. 使用 mixin 做出一個可以讓其他元件複製的內容。 - 宣告 var mixinName = {要給大家複製的內容} - 在 component 裡面寫上 mixins=[mixinName1,mixinName2 ....],可以寫多個。 ```javascript= <div id="app"> <table class="table"> <tbody> <tr is="row-component" v-for="(item, key) in data" :item="item" :key="key"></tr> </tbody> </table> </div> <script type="text/x-template" id="row-component"> <tr> <td>{{ item.name }}</td> <td>{{ item.cash | currency | dollarSign }}</td> <td>{{ item.icash | currency | dollarSign }}</td> </tr> </script> <script> // mixin 是多個混合的概念 var mixinFilter = { // 第一個 mixin data: function() { return { data: {}, } }, template: '#row-component', filters: { dollarSign: function (n) { return `$ ${n}` }, currency: function(n) { return n.toFixed(2).replace(/./g, function(c, i, a) { return i && c !== "." && ((a.length - i) % 3 === 0) ? ',' + c : c; }); } }, } var mixinMounted = {// 第二個 mixin mounted () { console.log('這段是 Mixin 產生') } } Vue.component('row-component', { props: ['item'], mixins: [mixinFilter,mixinMounted], //將兩個 mixin 注入 }); </script> ``` ## 五、使用 Directive 開發自己的互動 UI ### 1. 官方文件的例子: 每次重新整理後,都可以讓 input 呈現 focus 的狀態 ```javascript= <div id="app"> <input type="email" v-model="email" v-focus> </div> <script> // 官方文件 https://cn.vuejs.org/v2/guide/custom-directive.html Vue.directive('focus', { inserted: function(el) { el.focus() } }) </script> ``` ### 2. Directive 的生命週期 - bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的**初始化设置**。 - inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。 - update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新 (详细的钩子函数参数见下)。 **簡單來說,就是你每次更新狀態會觸發,例如在 input 裡面輸入文字,每輸入一個字元就會觸發一次 update** - componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。 - unbind:只调用一次,指令与元素解绑时调用。 ### 3. 使用 directive 做表單驗證 - el:指令所绑定的元素,可以用来直接操作 DOM 。 - binding: 包含一些參數可以做物件操作 - name:指令名,不包括 v- 前缀。 - value:指令的绑定值,例如:v-my-directive="1 + 1" 中,绑定值为 2。 - oldValue:指令绑定的前一个值,仅在 update 和 componentUpdated 钩子中可用。无论值是否改变都可用。 - expression:字符串形式的指令表达式。例如 v-my-directive="1 + 1" 中,表达式为 "1 + 1"。 - arg:传给指令的参数,可选。例如 v-my-directive:foo 中,参数为 "foo"。 - modifiers:一个包含修饰符的对象。例如:v-my-directive.foo.bar 中,修饰符对象为 { foo: true, bar: true }。 - vnode:Vue 编译生成的虚拟节点 ```javascript= <div id="app"> <input type="email" v-model="email" V-validation> </div> <script> // 官方文件 https://cn.vuejs.org/v2/guide/custom-directive.html Vue.directive('validation',{ update: function(el ,binding ,vnode ) { var value = el.value; var re = re = /^(([^<>()\[\]\.,;:\s@\"]+(\.[^<>()\[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i; console.log(value, re.test(value)); if(!re.test(value)){ el.className = "form-control is-invalid" }else if(re.test(value)){ el.className = "form-control is-valid" } }, bind: function (el ,binding ,vnode ) { el.className = 'form-control'; } }) ``` ### 4. directive 詳細用法 - 找到 V-validation 裡面的值 ![](https://i.imgur.com/PfkcZcY.png) ```javascript= <div id="app"> <input type="email" v-model="email" V-validation="{ className: 'form-control'}"> </div> <script> Vue.directive('validation',{ bind: function (el ,binding ,vnode ) { el.className = binding.value.className; //找到 validation 內帶的值 console.log('binding',el ,binding ,vnode ); } }) var app = new Vue({ el: '#app', data: { email: 'csc@fff.com', }, mounted: function() { console.log('Vue init:', this) } }); </script> ``` - 找到 v-model 內的值 ![](https://i.imgur.com/IiOsl9U.png) ```javascript= <div id="app"> <input type="email" v-model="email" V-validation="{ className: 'form-control'}"> </div> <script> Vue.directive('validation',{ bind: function (el ,binding ,vnode ) { //找到 v-model 內的值 var vModel = vnode.data.directives.find(function(item){ return item.name === "model"; }).expression; console.log('vModel',vModel); // vModel email } }) var app = new Vue({ el: '#app', data: { email: 'csc@fff.com', }, mounted: function() { console.log('Vue init:', this) } }); </script> ``` - 找到 email 裡面的值 ![](https://i.imgur.com/fRxPhyq.png) ```javascript= <div id="app"> <input type="email" v-model="email" V-validation="{ className: 'form-control'}"> </div> <script> Vue.directive('validation',{ bind: function (el ,binding ,vnode ) { //找到 v-model 內的值 var vModel = vnode.data.directives.find(function(item){ return item.name === "model"; }).expression; console.log('vModel',vModel); // vModel email // 找到 email 裡面的值 let value = vnode.context[vModel];//因為 v-model 的值不會固定,所以用 [] 表示 console.log('vModel',vModel,value); // vModel email csc@fff.com } }) var app = new Vue({ el: '#app', data: { email: 'csc@fff.com', }, mounted: function() { console.log('Vue init:', this) } }); </script> ``` ## 六、插入外部套件 [Bootstrap+Vue](https://bootstrap-vue.js.org/docs) # 柒、Vue Cli 的建置與運作原理 接下來的操作都在 cmd 命令操作提示字元上 ## 一、安裝 Vue cli ### 1. 將 Vue-cli 安裝在全域的環境下 ``` npm install -g vue-cli ``` ### 2. 查看 Vue 指令 ``` vue ``` Options: **-h, --help:** output usage information Commands: **init:** generate a new project from a template **list:** list available official templates **build:** prototype a new project **create:** (for v3 warning only) **help [cmd]:** display help for [cmd] ### 3. list 列出官方可以用的樣板 這邊使用 webpack ``` vue list ``` ★ browserify - A full-featured Browserify + vueify setup with hot-reload, linting & unit testing. ★ browserify-simple - A simple Browserify + vueify setup for quick prototyping. ★ pwa - PWA template for vue-cli based on the webpack template ★ simple - The simplest possible Vue setup in a single HTML file ★ webpack - A full-featured Webpack + vue-loader setup with hot reload, linting, testing & css extraction. ★ webpack-simple - A simple Webpack + vue-loader setup for quick prototyping. ### 4. 開啟一個 webpack 樣板 ``` vue init <template-name> <project-name> ``` template-name: 設為 webpack project-name: 專案名稱 ### 5. 安裝相關套件 到剛創立的資料夾到 ``` npm install ``` ### 6. 執行專案 ``` npm run dev ``` ## 二、新增自定義環境變數 ![](https://i.imgur.com/rkt76iO.png) - 在 config 的資料夾裡面有 - dev.env.js : 開發環境下的環境變數 ![](https://i.imgur.com/UvnSC5D.png) - prod.env.js : 生產環境下的環境變數 ![](https://i.imgur.com/8blkGLk.png) - 我們可以透過修改 src > component 內的 .vue 檔案路徑來讀取環境變數 ![](https://i.imgur.com/UoX2Jap.png) ## 三、 安裝套件在 Vue Webpack 中 ### 1. 安裝 bootstrap 及 sass ``` npm install bootstrap node-sass sass-loader -save ``` - sass-loader 的版本會過高無法安裝,改由安裝 7.1.0 版 ``` npm install --save-d sass-loader@7.1.0 ``` - 在 app.vue 下加入這段程式碼 ```javascript= <style lang="scss"> @import "~bootstrap/scss/bootstrap"; //~告訴 webpack 不是相對路徑 </style> ``` - component 的元件 css 是另外獨立出來的,在預設 helloWorld.vue 裡面 ```htmlmixed= <!-- Add "scoped" attribute to limit CSS to this component only --> <style scoped> : : </style> ``` ### 7. vue-cli 資料夾說明 - dist 這資料夾下的檔案都要放在server下才能正常運作,主要是透過npm run bulid生成的 - static 放入不會被編譯的檔案 - src 放入會被編譯的檔案,開發都在這邊 - 底下有main.js 就是所有vuejs的進入點 - 其中assets會針對特定尺寸的圖片來做編譯成base 64 - .postcssrc.js > 替CSS編譯加入前啜詞的設定 - .babelrc > 替ES6編譯的設定檔 - 在vue-cli中都是使用元件來載入。 ## 2.使用 vue-axios 套件串接 AJAX - 安裝 vue-axios ``` npm install --save axios vue-axios ``` - 在 entry 的資料夾(預設 main.js) import 下面的程式碼 ``` import Vue from 'vue' import axios from 'axios' import VueAxios from 'vue-axios' Vue.use(VueAxios, axios) ``` - 使用下列取法取得遠端資料 ``` this.$http.get("apiAddress").then((response) => { console.log(response.data) }) ``` # 捌、Vue Router ## 一、使用 Vue Router 及配置路由文件 - 在 src 下建立 router 的資料夾,資料夾內建立 index.js,並輸入下面程式碼 ```javascript= import Vue from 'vue' import Router from 'vue-router' import HelloWorld from '@/components/HelloWorld' Vue.use(Router) export default new Router({ routes: [ { path: '/index', //對應的虛擬路徑, #後面的名稱 name: 'HelloWorld', //元件呈現的名稱 component: HelloWorld // 對應的元件 } ] }) ``` - 在 app.vue 中,加入 router-view ```javascript= <template> <div id="app"> <img src="./assets/logo.png"> <router-view/> </div> </template> ``` ## 二、新增路由路徑及連結 在 Vue.app 加入 Navbar 新增導覽列的功能 - 切換連結的方法有兩種 - 動態綁定名字: <router-link :to="{name:'綁定的name'}"></router-link> - 綁定路徑: <router-link to="/綁定的路徑"></router-link> ```htmlmixed= <nav class="navbar navbar-expand-lg navbar-light bg-light"> <div class="collapse navbar-collapse" id="navbarSupportedContent"> <ul class="navbar-nav mr-auto"> <li class="nav-item"> <router-link class="nav-link" :to="{name:'HelloWorld'}">Home</router-link> </li> <li class="nav-item"> <router-link class="nav-link" to="/page">page</router-link> </li> </ul> </div> </nav> ``` ## 三、 製作巢狀路由頁面 變更元件中的子元件 children - router 中新增子元件路徑 ```javascript= { import child from '@/components/pages/child' import child2 from '@/components/pages/child2' import child3 from '@/components/pages/child3' path: '/page', //對應的虛擬路徑 name: 'page', //元件呈現的名稱 component: page, // 對應的元件 children:[ { path: '', name: 'card', component: child, }, { path: 'child2', name: 'card2', component: child2, }, { path: 'child3', name: 'card3', component: child3, }, ] } ``` - page 中的 template 修改 ```javascript= <template> <div class="card" style="width: 18rem;"> <router-link to="/page/">page1</router-link> //新增子元件選項 <router-link to="/page/child2">page2</router-link> <router-link to="/page/child3">page3</router-link> <router-view></router-view> //子元件替換的部分 </div> </template> ``` - 子元件的 template ```javascript= <template> <div class="card-body"> <h5 class="card-title">Card 1</h5> <p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p> <a href="#" class="btn btn-primary">Go somewhere</a> </div> </template> ``` ## 四、 使用動態路由切換頁面 Ajax 結果 - 把 child3 的路徑改為動態 id ```javascript= { path: 'child/:id', name: 'card3', component: child3, }, ``` - 在 child3 裡面讀取 ajax 資料 ```javascript= <script> export default { data () { return {} } }, created(){ console.log(this.$route.params.id);//取得動態路由 id const id = this.$route.params.id; this.$http.get(`https://randomuser.me/api/?seed=${id}`).then((response) => { //這邊的 id 會是固定的,因為它會變成讀取我們預設的 id //因為 id 沒改變,每次出現的資料的人也都會是一樣的 console.log(response.data); }) } } </script> ``` ## 五、 命名路由,同一個路徑載入兩個頁面元件 - 原本 Vue.app 只有一個 router-view,現在要另外加上一個,將新增的加上 name="", 沒有 name 的則為預設。 ```javascript= <router-view name="menu"></router-view> <div class="container"> <router-view></router-view> </div> ``` - 將原本對應一個的 component 改成對應多個的 components ```javascript= { path: '/page', //對應的虛擬路徑 name: 'page', //元件呈現的名稱 // component: page, // 對應的元件 components:{ default: page,// 對應 page.vue 頁籤 menu: Menu,//對應 menu 頁籤 }, } ``` ## 六、 Vue Router 參數設定 ## 七、 自定義切換路由方法 [官網連結](https://router.vuejs.org/zh/api/#to) - template 標籤改為 a 連結,觸發動作為 click ```javascript= <template> <div class="card" style="width: 18rem;"> <ul class="nav"> <li class="nav-item"> <a href="#" class="nav-link" @click.prevent="update">測試</a> </li> </ul> </div> </template> <script> export default { methods:{ // update() { //切換到路徑指定頁面 // this.$router.push('/page/child2'); // }, update() { //切換到路徑指定頁面,但不會向,back 有紀錄 this.$router.replace('/page/child2'); }, // beforeUpdate() {//回上一頁 // this.$router.back(); // }, // nextUpdate() { //到下一頁 // this.$router.forward(); // }, // nextUpdate() { //指定跳幾頁 // this.$router.go(-1); // }, nextUpdate() { //指定跳幾頁 this.$router.go(-1); }, }, } </script> ``` # 玖、 Vue 出一個電商網站 ## 一、登入介面 - api 路徑包含 [api伺服器路徑] [api path] - 伺服器的選取方式可能會改變,因此將 path 寫在 config/dev(開發用) 或 config/prod(實際產品) 裡面。 - 寫入方法 APIPATH: '"https://vue-course-api.hexschool.io"', CUSTOMPATH: '"csc98104"', **記得單引號裡面再包雙引號** - 實際擷取 process.env 為環境變量 ``` const api = `${process.env.APIPATH}/admin/signin`; ``` ```javascript= <template> <div> <form class="form-signin" @submit.prevent="signin">//登入綁定 <h1 class="h3 mb-3 font-weight-normal">Please sign in</h1> <label for="inputEmail" class="sr-only">Email address</label> <input type="email" id="inputEmail" v-model="user.username" class="form-control" placeholder="Email address" required autofocus> <label for="inputPassword" class="sr-only">Password</label> <input type="password" id="inputPassword" v-model="user.password" class="form-control" placeholder="Password" required> <div class="checkbox mb-3"> <label> <input type="checkbox" value="remember-me"> Remember me </label> </div> <button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button> <p class="mt-5 mb-3 text-muted">&copy; 2017-2018</p> </form> </div> </template> <script> export default { data () { return { user:{ //設定 user data 資料與 v-model 對應 username:'', password:'', } } }, methods:{ signin(){ const api = `${process.env.APIPATH}/admin/signin`; //process.env.APIPATH 綁定 config 內的 env 參數 const vm = this this.$http.post(api, vm.user).then((response) => { //post 裡面(ajax網址, data) console.log(response.data); if(response.data.success){ //登入成功後 vm.$router.push('/'); //跳轉至首頁 } }); } } } </script> ``` ## 二、驗證登入及 Vue Router 的配置 ### 1. 在 main.js 下使用 router.beforeEach 來設定導航守衛 ```javascript= router.beforeEach((to, from, next) => { //設定導航守衛,避免使用者在沒登入的狀態下,直接進入後台 if(to.meta.requiresAuth){ //在 to(指定要到的路徑下),如果指定到的路徑下有設定 meta.requiresAuth,進入後端驗證 const api = `${process.env.APIPATH}/api/user/check`; axios.post(api).then((response) => { //因為不是在 component 的資料夾下,不能使用 this.$http,改用 axios console.log(response.data); if(response.data.success){ //登入成功後,跳轉到原來要去的路徑 next(); }else{ next({ path: '/login', //登入沒成功,回到原來的登入頁面 }); } }); }else{ //如果沒有 meta.requiresAuth,直接到指定路徑 next(); } }) ``` ## 三、 套用 Bootstrap Dashboard 版型 ### 1. 建立 Dashboard 版型 - 切割版型 NavBar 和 SideBar 由另外兩個 component 匯入 ```javascript= <template> <div> <NavBar></NavBar> // 由其他 component 匯入 <div class="container-fluid"> <div class="row"> <nav class="col-md-2 d-none d-md-block bg-light sidebar"> <SideBar/> //由其他 component 匯入 </nav> <main role="main" class="col-md-9 ml-sm-auto col-lg-10 px-4"> <router-view></router-view> // 另一個 router 替換 </main> </div> </div> </div> </template> <script> import SideBar from './SideBar'; //匯入另外兩個 components import NavBar from './Navbar'; export default { components: { SideBar, NavBar, }, }; </script> ``` - Product 的頁面由 admin 的子 routes 進入 ```javascript= { path: '/admin', name: 'Dashboard', component: Dashboard, children:[ //巢狀 Router-link { path: 'products', name: 'Products', component: Products, meta: { requiresAuth: true }, //在進入 products 前要驗證 }, ], }, ``` ## 四、製作產品列表 讀取後端資料後,將資料放進 template 中 ```javascript= <template> <div> <div class="text-right"> <button class="btn btn-primary mt-4">建立新的產品</button> </div> <table class="table mt-4"> <thead> <tr> <th width="120">分類</th> <th>產品名稱</th> <th width="120">原價</th> <th width="120">售價</th> <th width="150">是否啟用</th> <th width="120">編輯</th> </tr> </thead> <tbody> <tr v-for="(item) in products" :key="item.id"> <td>{{ item.category }}</td> <td>{{ item.title }}</td> <td class="text-right">{{ item.origin_price }}</td> <td class="text-right">{{ item.price }}</td> <td> <span v-if="item.is_enable" class="text-success">啟用</span> <span v-else>未啟用</span> </td> <td> <button class="btn btn-outline-primary btn-sm">編輯</button> </td> </tr> </tbody> </table> </div> </template> <script> export default { data() { return { products: [] }; }, methods: { getProducts() { const api = `${process.env.APIPATH}/api/${process.env.CUSTOMPATH}/products`; const vm = this; this.$http.get(api).then(response => { vm.products = response.data.products; //將遠端產品的資料放進 products 中 }); } }, created() { this.getProducts(); //在 created 的 hook 生成後呼叫 getProducts } }; </script> ``` ## 五、 Vue 中運用 Bootstrap 及 jQuery - 這個部分要使用到 model,有使用到 JQuery,因此要 import 'bootstrap' - 還有另外安裝 npm install --save jquery popper.js - 使用 JS 的方法叫出 Model,確保 model 裡面的值已經背後端撈出來 - 在使用 $('#productModal').modal('show') 叫出 Model 時,會出現 $ 未定一的錯誤 ```javascript= <script> import $ from 'jquery'; //加入 $ 字號辨識 export default { data() { return { products: [] }; }, methods: { getProducts() { const api = `${process.env.APIPATH}/api/${process.env.CUSTOMPATH}/products`; const vm = this; this.$http.get(api).then(response => { vm.products = response.data.products; }); }, openModel() { $('#productModal').modal('show')//使用 JQuery 的方法叫出 Model } }, created() { this.getProducts(); } }; </script> ``` ## 六、 產品的新增修改 ### 1. methods 內新增更新資料的函數 ```javascript= data() { return { products: [], tempProduct: {}, //新增的資料存在這裡 }; }, updataProduct() { const api = `${process.env.APIPATH}/api/${process.env.CUSTOMPATH}/admin/product`; const vm = this; this.$http.post(api, { data:vm.tempProduct }).then((response) => { //因為後端的資料型態是 {data:{ }},所以傳出去的資料也要符合這個格式 if(response.data.success){//新增成功時 $("#productModal").modal("hide"); //將 model 的視窗關掉 vm.getProducts(); //更新產品列表 }else{// 新增失敗時 $("#productModal").modal("hide");//將 model 的視窗關掉 vm.getProducts();//更新產品列表 console.log('新增失敗');//顯示失敗訊息 } }); } ``` ### 2. 舊資料中編輯資料與新建立產品共用函式 - 透過傳進來的(isNew , item)參數,來判斷打開的 model 是新增產品還是編輯產品。 ```javascript= //html <button class="btn btn-primary mt-4" @click="openModel(true)">建立新的產品</button> <button class="btn btn-outline-primary btn-sm" @click="openModel(false,item)">編輯</button> //data data() { return { products: [], tempProduct: {}, //新增的資料存在這裡 editIsNew: false, }; }, //methods openModel(isNew , item) { $("#productModal").modal("show"); if(isNew){ //如果 isNew 是 true this.tempProduct = {}; // 將 model 內容清空 this.editIsNew = true; // 將編輯狀態是新的改成 true,代表是新增的產品 }else { //如果 isNew 是 false this.tempProduct = Object.assign({},item); //將 item 將 item 的內容放回 tempProduct this.editIsNew = false; // 將編輯狀態是新的改成 false,代表是需要編輯的產品 } }, updataProduct() { let api = `${process.env.APIPATH}/api/${process.env.CUSTOMPATH}/admin/product`; const vm = this; let httpStatus = 'post'; if(!vm.editIsNew){ api = `${process.env.APIPATH}/api/${process.env.CUSTOMPATH}/admin/product/${this.tempProduct.id}`; httpStatus = 'put'; } this.$http[httpStatus](api, { data:vm.tempProduct }).then((response) => { //[httpStatus]是把字串變成變數的方法,傳入的是'post'、'put',透過 [] 轉成 post、put //因為後端的資料型態是 {data:{ }},所以傳出去的資料也要符合這個格式 if(response.data.success){//新增成功時 $("#productModal").modal("hide"); //將 model 的視窗關掉 vm.getProducts(); //更新產品列表 }else{// 新增失敗時 $("#productModal").modal("hide");//將 model 的視窗關掉 vm.getProducts();//更新產品列表 console.log('新增失敗');//顯示失敗訊息 } }); } ``` ### 3. 刪除產品 ```javascript= //html <button class="btn btn-outline-primary btn-sm" @click="deleteModel(key)">刪除</button> // 將 key 傳入資料,確認要刪除的資料是哪一筆 //modelButton <button type="button" class="btn btn-primary" @click="deleteProduct">確認刪除</button> //methods deleteModel(key){ //將 v-for 中的 key 傳進資料中 $("#deleteModal").modal("show"); const vm = this; vm.deleteKey = key; }, deleteProduct(){ //讀取 key 資料,去刪除產品 const vm = this; console.log(vm.deleteKey); const api = `${process.env.APIPATH}/api/${process.env.CUSTOMPATH}/admin/product/${vm.products[vm.deleteKey].id}`; this.$http.delete(api).then(response => { vm.getProducts();//更新產品列表 $("#deleteModal").modal("hide"); }); } ``` ## 七、串接上傳檔案 API [上傳格式](https://github.com/hexschool/vue-course-api-wiki/wiki/%E7%AE%A1%E7%90%86%E6%8E%A7%E5%88%B6%E5%8F%B0-%5B%E9%9C%80%E9%A9%97%E8%AD%89%5D#%E4%B8%8A%E5%82%B3%E5%9C%96%E7%89%87) - 上傳表單 (前端測試使用) ```htmlmixed= <form action="/api/thisismycourse2/admin/upload" enctype="multipart/form-data" method="post"> <input type="file" name="file-to-upload"> <input type="submit" value="Upload"> </form> ``` - 上傳的行為需要使用 form-data 操作 ```javascript= html <input type="file" id="customFile" class="form-control" ref="upload" @change="uploadFile"/> //ref="upload" 必掛, 上傳的檔案 file,會放在 upload 裡面,Ajax 中需要透過 DOM 來取得 input file 內的上傳檔案 //會透過 ref 來指向該目標,在使用 js 來取得檔案資訊並上傳 //methods uploadFile(){ const uploadedFile = this.$refs.upload.files[0]; const vm = this; const formData = new FormData(); formData.append('file-to-upload',uploadedFile); const url = `${process.env.APIPATH}/api/${process.env.CUSTOMPATH}/admin/upload`; this.$http.post(url, formData, { headers: { //居然要加 s 'Content-Type' : 'multipart/form-data' //傳送「檔案」必須使用表單的形式,所以在此把傳送的類型改為 form-data 喔 } }).then(response => { if(response.data.success){ // vm.tempProduct.imageUrl = response.data.imageUrl; // console.log(vm.tempProduct); vm.$set(vm.tempProduct, 'imageUrl', response.data.imageUrl ); //因為 imageUrl 未事先定義,所以必須使用 $set 強制寫進去 console.log(vm.tempProduct); } }) }, ``` ## 八、增加使用者體驗 - 讀取中效果製作 - 使用套件 [vue-overlay](https://github.com/ankurk91/vue-loading-overlay) - import 元件 ``` import Loading from 'vue-loading-overlay'; import 'vue-loading-overlay/dist/vue-loading.css'; Vue.component('Loading',Loading); //因為 Loading 是元件,必須被啟用,這邊用的是全域啟用 //將元件載入,可以在 vue template 中運用該套件但資料、方法仍然與執行中的元件分離 ``` - 在開始讀取前將 vm.isLoading = true; 讓動畫效果開始,讀取結束後 vm.isLoading = false; 結束動畫效果 ```javascript= getProducts() { const api = `${process.env.APIPATH}/api/${process.env.CUSTOMPATH}/products`; const vm = this; vm.isLoading = true;//開始讀取動畫效果 this.$http.get(api).then(response => { vm.isLoading = false;//結束動畫效果 vm.products = response.data.products; }); }, ``` - 使用 font使用 font-awesome,CDN ``` <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.1.0/css/all.css" integrity="sha384-lKuwvrZot6UHsBSfcMvOkWwlCMgc0TaWr+30HWe3a4ltaBwTZhyTEggF5tJv8tbt" crossorigin="anonymous"> ``` - 使用 v-if 讓 fileUploading 為 true 時才會呈現動畫效果 ```javascript= <i class="fas fa-spinner fa-spin" v-if="fileUploading"></i> uploadFile(){ const uploadedFile = this.$refs.upload.files[0]; const vm = this; vm.fileUploading = true;//開始讀取動畫效果 const formData = new FormData(); formData.append('file-to-upload',uploadedFile); const url = `${process.env.APIPATH}/api/${process.env.CUSTOMPATH}/admin/upload`; this.$http.post(url, formData, { headers: { 'Content-Type' : 'multipart/form-data' } }).then(response => { if(response.data.success){ // vm.tempProduct.imageUrl = response.data.imageUrl; // console.log(vm.tempProduct); vm.$set(vm.tempProduct, 'imageUrl', response.data.imageUrl ); vm.fileUploading = false; //結束動畫效果 } }) }, ``` ## 九、 產品列表的分頁邏輯 ### 1. 使用後端的傳來的資料製作分頁功能 ```htmlmixed= <!-- html --> <nav aria-label="Page navigation example"> <ul class="pagination"> <li class="page-item" :class="{'disabled' : !pagination.has_pre}"> <a class="page-link" href="#" aria-label="Previous" @click.prevent="getProducts(pagination.current_page - 1)"> // 前往前一頁,使用 current_page - 1 <span aria-hidden="true">&laquo;</span> </a> </li> <li class="page-item" v-for="page in pagination.total_pages" :key="page" :class="{'active' : pagination.current_page == page}"> <!-- 當 current_page 頁數等於 page 時,就將 active 加上去 --> <!-- v-for 也可以接受整数。在这种情况下,它会把模板重复对应次数。所以 pagination.total_pages 可以是數字 --> <a class="page-link" href="#" @click.prevent="getProducts(page)">{{ page }}</a> </li> <li class="page-item" :class="{'disabled' : !pagination.has_next}"> <a class="page-link" href="#" aria-label="Next" @click.prevent="getProducts(pagination.current_page + 1)"> // 前往下一頁,使用 current_page + 1 <span aria-hidden="true">&raquo;</span> </a> </li> </ul> </nav> <script> //vue data() { return { products: [], tempProduct: {}, editIsNew: false, deleteKey: 0, isLoading: false, fileUploading: false, pagination: {},//分頁的資料存在這裡 }; }, methods: { getProducts(page = 1) { //預設頁數為 1 const api = `${process.env.APIPATH}/api/${process.env.CUSTOMPATH}/products?page=${page}`; const vm = this; vm.isLoading = true; this.$http.get(api).then(response => { vm.isLoading = false; console.log(response); vm.products = response.data.products; vm.pagination = response.data.pagination; //將後端的值放到 pagination }); }, </script> ``` ## 十、套用價格的 Filter 技巧 ### 1. 在 src 的資料下再建立一個 filter 的資料夾 ### 2. 將千分位的公式放在這的資料夾下 ![](https://i.imgur.com/SfZ1c2Z.png) ### 3. 在 main.js entry 下,import fliter 的資料進來 ```javascript= //使用 Vue.filter 宣告全域的 fliter Vue.filter('currency',currencyFilter); ``` ![](https://i.imgur.com/qnGunyZ.png) ## 十一、Dashboard 新增模擬購物頁面 - 新增卡片式產品列表 ```javascript= <template> <div> <loading :active.sync="isLoading"></loading> <div class="row mt-4"> <div class="col-md-4 mb-4" v-for="item in products" :key="item.id"> <div class="card border-0 shadow-sm"> <div style="height: 150px; background-size: cover; background-position: center" :style="{ backgroundImage : `url(${item.imageUrl})` }"></div> <div class="card-body"> <span class="badge badge-secondary float-right ml-2">{{item.category}}</span> <h5 class="card-title"> <a href="#" class="text-dark">{{item.title}}</a> </h5> <p class="card-text">{{item.content}}</p> <div class="d-flex justify-content-between align-items-baseline"> <div class="h5" v-if="!item.price">{{item.origin_price}} 元</div> <del class="h6" v-if="item.price">原價 {{item.origin_price}} 元</del> <div class="h5" v-if="item.price">現在只要 {{item.price}} 元</div> </div> </div> <div class="card-footer d-flex"> <button type="button" class="btn btn-outline-secondary btn-sm"> <i class="fas fa-spinner fa-spin"></i> 查看更多 </button> <button type="button" class="btn btn-outline-danger btn-sm ml-auto"> <i class="fas fa-spinner fa-spin"></i> 加到購物車 </button> </div> </div> </div> </div> </div> </template> <script> export default { data() { return { products: [], isLoading: false, }; }, methods: { getProducts() { const api = `${process.env.APIPATH}/api/${process.env.CUSTOMPATH}/products/all`; const vm = this; vm.isLoading = true; //讀取資料效果開 this.$http.get(api).then(response => { //讀取全部資料 vm.isLoading = false;//讀取資料效果關 vm.products = response.data.products; console.log(response); }); }, }, created() { this.getProducts(); }, }; </script> ``` ## 十二、取得單一產品 - 使用 status.loadingItem == item.id 來判斷動畫的顯示 ```javascript= <div class="card-footer d-flex"> <button type="button" class="btn btn-outline-secondary btn-sm" @click="getProduct(item.id)"> <i class="fas fa-spinner fa-spin" v-if="status.loadingItem == item.id"></i> // 判斷 status.loadingItem 等於 id 就顯示讀取動畫 查看更多 </button> <button type="button" class="btn btn-outline-danger btn-sm ml-auto"> <i class="fas fa-spinner fa-spin" v-if="status.loadingItem == item.id"></i> 加到購物車 </button> </div> <!-- model --> <div class="modal fade" id="productModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true"> <div class="modal-dialog" role="document"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title" id="exampleModalLabel">{{ product.title }}</h5> <button type="button" class="close" data-dismiss="modal" aria-label="Close"> <span aria-hidden="true">&times;</span> </button> </div> <div class="modal-body"> <img :src="product.imageUrl" class="img-fluid" alt=""> <blockquote class="blockquote mt-3"> <p class="mb-0">{{ product.content }}</p> <footer class="blockquote-footer text-right">{{ product.description }}</footer> </blockquote> <div class="d-flex justify-content-between align-items-baseline"> <div class="h4" v-if="!product.price">{{ product.origin_price }} 元</div> <del class="h6" v-if="product.price">原價 {{ product.origin_price }} 元</del> <div class="h4" v-if="product.price">現在只要 {{ product.price }} 元</div> </div> <select name="" class="form-control mt-3" v-model="product.num"> <option :value="num" v-for="num in 10" :key="num"> 選購 {{num}} {{product.unit}} </option> </select> </div> <div class="modal-footer"> <div class="text-muted text-nowrap mr-3"> 小計 <strong>{{ product.num * (product.price || product.origin_price)}}</strong> 元 </div> <button type="button" class="btn btn-primary"> <i class="fas fa-spinner fa-spin"></i> 加到購物車 </button> </div> </div> </div> </div> export default { data() { return { products: [], product: {}, status:{ loadingItem:'', }, isLoading: false, }; }, methods:{ getProduct(id) { const api = `${process.env.APIPATH}/api/${process.env.CUSTOMPATH}/product/${id}`; const vm = this; vm.status.loadingItem = id; //透過辨識 id 來顯示讀取頁面 this.$http.get(api).then(response => { vm.product = response.data.product; //取得單一產品資訊 vm.product.num = "1"; //取得產品資訊後,將 num 改為 1,這樣在下拉選單時,出現的才會是 1 $('#productModal').modal('show'); vm.status.loadingItem = ''; console.log(response.data.product); }); } } ``` ## 十三、 ## 十四、刪除購物車品項及新增優惠碼 ```javascript= removeCart(id) { const api = `${process.env.APIPATH}/api/${process.env.CUSTOMPATH}/cart/${id}`; const vm = this; vm.isLoading = true; this.$http.delete(api).then(response => { console.log(response); vm.getCart(); vm.isLoading = false; }); }, addCouponCode() { const api = `${process.env.APIPATH}/api/${process.env.CUSTOMPATH}/coupon`; const vm = this; const coupon ={ code: vm.coupon_code, //送出 api 的資料型態 } vm.isLoading = true; this.$http.post(api, { data: coupon }).then(response => { console.log(response); vm.getCart(); vm.isLoading = false; }); }, ``` ## 十四、建立訂單及表單驗證技巧 ### 1. 安裝 VeeValidate 套件 [參考同學 Blog](https://hao1229.github.io/2019/08/09/EcommercePractice8/) - 使用 2.x 版本,安裝 npm ``` npm install vee-validate@2.2.15 --save ``` - 在 main.js import VeeValidate 並使用 ``` import VeeValidate from 'vee-validate'; Vue.use(VeeValidate);/使用 ``` - 中文化提示文字,在 main.js 匯入 ``` import VeeValidate, { Validator } from 'vee-validate' //匯入檔案 import TW from 'vee-validate/dist/locale/zh_TW' //匯入語言包 Vue.use(VeeValidate) //啟用API Validator.localize('zh-TW', TW) //啟用語言包 ``` ### 2. 使用 VeeValidate **required 驗證** - 在 input 加入 v-validate="'required'" 代表這是需要驗證的資料 - 可以透過 errors.has('name') 來檢驗使用者是否有正確填入資料,errors.has('name') 預設為 false,如果他打完,又把資料清空, errors.has('name') 就會顯示為 true - errors.has('name') 的 name 對應的是 input 中 name=" "," " 中的內容 ```javascript= <div class="form-group"> <label for="username">收件人姓名</label> <input type="text" class="form-control" name="name" id="username" :class="{'is-invalid':errors.has('name')}" v-model="form.user.name" v-validate="'required'" placeholder="輸入姓名"> <span class="text-danger" v-if="errors.has('name')"> 姓名必須輸入 </span> </div> ``` **email 驗證** - v-validate="'required|email'" ,驗證 email 格式,required|email 不能有空格,不然會錯誤。 - 使用 errors.first('email') 顯示錯誤提示 ```javascript= <div class="form-group"> <label for="useremail">Email</label> <input type="email" class="form-control" name="email" id="useremail" v-model="form.user.email" v-validate="'required|email'" placeholder="請輸入 Email"> <span class="text-danger" v-if="errors.has('email')"> {{ errors.first('email') }} </span> </div> ``` **送出表單驗證** ```javascript= this.$validator.validate().then(valid => { if (valid) { console.log('驗證成功') } else { console.log("欄位不完整"); } }); ``` ## 十五、 Vue-CLI 3 ### 1. Vue CLI 2 和 3 的差別 #### [Vue-CLI 3 官網](https://cli.vuejs.org/zh/) - 提供完整的 GUI 套件 - 只提供一個核心,提供其他插件 - 設定檔提供 GUI 介面 ### 2. 安裝 - 如果有安裝 CLI2 要先透過 npm uninstall vue-cli -g 刪除 - 輸入 npm install -g @vue/cli 安裝 CLI3 ### 3. 環境變數的設定 新增 .env 的檔案 ``` .env # 在所有的环境中被载入 .env.local # 在所有的环境中被载入,但会被 git 忽略 .env.[mode] # 只在指定的模式中被载入 .env.[mode].local # 只在指定的模式中被载入,但会被 git 忽略 ``` - .env.local 會被 .gitignore 加入,因此不會加入版控,不會被上傳到 Github - .env.[mode] 可以指定在特定模式下載入這個環境變數 可以到 package.json 下。找到 ``` "scripts": { "serve": "vue-cli-service serve --mode test", "build": "vue-cli-service build", "lint": "vue-cli-service lint" }, ``` 然後命名 .env.test,這樣就能指定特定的環境變數了 - .dev.development 為 ``` "serve": "vue-cli-service serve", ``` 的預設,且權重比單純的 .dev 重 - .dev.production 為 ``` "build": "vue-cli-service build", ``` 的預設 ### 4. 使用 Vue GUI ``` vue ui ``` 使用 vue ui 指令叫出 GUI 工具 ## 十六、Vuex 管理大型網站資料狀態 ### 1. Vuex 的組成 - state ===> 資料型態 - action ===> 如同 method,但他只進行非同步的行為及取得資料 - getter ===> 如同 computed,為資料呈現的方式 - mutation ===> 改變資料內容的方法 ![](https://i.imgur.com/N0QHisU.png) ### 2. 新增一個 Store #### a. 安裝 Vuex - 在 main.js 新增 ``` import Vuex from 'vuex' vue.use(Vuex) ``` - 在 src 根目錄下創建 store 的資料夾,裡面存放 index.js index.js 裡面 ``` import Vue from 'vue'; import Vuex from 'vuex'; Vue.use(Vuex); export default new Vuex.Store({ state: { ..... }, }); ``` #### b. 使用 computed 將 store 的資料撈出來 ```javascript= computed: { isLoading() { return this.$store.state.變數名稱; }, }, ``` #### c. 使用的變數改從 store 取出 ```javascript= vm.$store.state.變數 = ...; ``` ### 3. 正確的使用 action 和 mutation 來取得變數 #### a. 如果有使用到非同步行為,要寫在 action 裡面,再去呼叫 mutation #### b. 改變 state 狀態的行為一定要寫在 mutation 裡面 #### c. 使用嚴謹模式 strict = true,在 vuex 裡面有不符合語法的操作就會跳錯 - dispatch 呼叫 action 中的 updateloading,並把 status = true 傳入 ```javascript= vm.$store.dispatch('updateloading', true); ``` - action 被呼叫後,去呼叫 mutation 執行 Loading - mutation 裡的 LOADING 會以大寫的方式呈現,以代表常數,這邊會呼叫 state 裡面的 isLoading,去改變它的 stutus 狀態為 true ```javascript= export default new Vuex.Store({ strict = true; //嚴謹模式 state: { isLoading: false, }, actions: { updateLoading(context, status) { context.commit('LOADING', status); }, }, mutations: { LOADING(state, status) { state.isLoading = status; }, }, }); ``` ### 4. 使用 Actions 取得遠端資料 將需要用 AJAX 取值得資料放到 action 裡面, 需要更該 state 值的部分則放到 mutation ```javascript= actions: { getProducts(context) { const url = `${process.env.APIPATH}/api/${process.env.CUSTOMPATH}/products/all`; context.commit('LOADING', true); axios.get(url).then((response) => { context.commit('PRODUCTS', response.data.products); console.log('取得產品列表:', response); context.commit('CATEGORY', response.data.products); context.commit('LOADING', false); }); }, }, PRODUCTS(state, payload) { state.products = payload; }, CATEGORY(state, payload) { const categories = new Set(); payload.forEach((item) => { categories.add(item.category); }); state.categories = Array.from(categories); }, ``` ### 5. payload 傳遞物件參數 在 action 中的 payload 參數只能帶一個值,因此有多於一個的值的參數,可以把它包成物件傳遞,傳到 action 時會使用解構的方式帶入參數。 ``` addtoCart(context, { id, qty }) {....} ``` ### 6. Vuex 中的 Getters 及 mapGetters, mapActions #### a. Getter 類似於 computed - 可以統一將 computed 的內容寫在 getters 裡面,再使用 mapgetters 取出 ```javascript= //將 computed 的內容寫再一起 getters: { isLoading(state) { return state.isLoading; }, cart(state) { return state.cart; }, categories(state) { return state.categories; }, products(state) { return state.products; }, }, ``` ```javascript= //將需要的 getters 取出 import { mapGetters, mapActions } from 'vuex'; computed: { ...mapGetters(['cart', 'isLoading']), }, ``` #### b. mapActions 有一樣的功能,可以把沒帶參數的 action 統一寫在一起 ```javascript= import { mapGetters, mapActions } from 'vuex'; methods: { ...mapActions(['getCart']), //沒參數可以使用 mapActions removeCart(id) { //有參數使用 dispatch const vm = this; vm.$store.dispatch('removeCart', id); }, }, ``` ### 7.模組化資料運用 簡化程式碼,將 index.js 拆分為各個模塊 - 新增一個新的檔案,將要拆出來的 modules 元件放入 - import moduldes ```javascript= import moduleProduct from './product'; : : : modules:{ moduleProduct, } ``` - state 為模塊變數,而 action, mutation, getters 屬於全域變數 因此如果要讀取 module 裡的 state 要加上模組名稱 ``` console.log(this.$store.state.**moduleProduct**.products); ``` - 如果要讓 action, mutation, getters 變成區域變數可以加上 namespaced: true, ```javascript= export default { namespaced: true, : : } ``` 這時的 mapAction 、mapGetters 要加上模組名稱 ```javascript= computed: { ...mapGetters('moduleProduct', ['categories', 'products', 'isLoading']), }, methods: { ...mapActions('moduleProduct', ['getProducts']), }, ``` 如果要維持原來全域的特性,可以加上 { root: true } ```javascript= context.commit('LOADING', false, { root: true }); ```