Vue.js 2

MVVM 的概念

此小節節錄自 sunbu

在說 MVVM 之前先介紹一下 MVC,MVC 本身也是一個經典的 web 開發架構,現在很多的開發架構就是用 MVC ,MVC 包含了前後端。

這邊 MVC 分別為 view、controller 及 model,那 view 指的就是我們看到的畫面,controller 就是控制器,model 指的就是模型,這裡也是資料介接的地方。
MVC 的架構中 controller 是做轉接的,model 是取得資料用的。

在一開始的時候會發送一個需求給 controller,比方說我們要取得資料列表的畫面,這個資料列表是網址下面帶有一個 list 的目錄下,此時使用者會對 list 本身發送一個請求說我要看這個 data 的內容,那 view 就會對 controller 發送一個需求,那在目前還沒有實際取得 data,只是發送一個請求說『我要看資料表』,此時 controller 就會對 model 說,請把資料表吐給我,那這個時候他就會去要求 model,model 得知此事後就會把 DB 裡的相關資料表取出來,比如說我要某某人的名字,那它把取出來並且把 data 返回給 controller,這個時候conroller 才會把整個網頁的內容呈現在某頁的樣板,一併回給畫面本身,這就是很基本的 MVC 架構。

那 MVVM 的概念會有點不一樣:

view 是視圖、ViewModel 是資料繫結器、model 是資料狀態,那這個 ViewModel 它是與畫面做綁定,它是綁定的狀態,它是一個連接器的感覺,在寫 vue 的時候並不會去寫 ViewModel,而是對 Model 做撰寫就可以了,那在寫 Model 的時候它在資料變動的同時它就會去控制視圖的變化。

所以我們在對某個 input 做輸入的時候 ViewModel 會幫我們把 model 的 text 做變動,相反的如果對 Model 的 text 做變更的話也會影響到 input 裡的文字。

Vue 實體的生命週期

此小節節錄自 勇者鬥 Vue 龍

Vue 實體的生命週期

一個 Vue 實體有生老病死,而 Vue 實體會在各個生命階段提供鉤子事件( Hook Event )讓開發者可以在實體的不同階段做想要的處理,本文介紹各個 Hook 的叫用時機。

各階段的鉤子函式

  • beforeCreate : 實例初始化立即叫用,這時還未創建實例,所以任何 Vue 實體中的設定(例如: data )都還未配置。
  • created : 完成創建實例,這時 Vue 實體中的配置除了 $el 外已全部配置,而 $el 要在掛載模板後才會配置。
  • beforeMount : 在 Vue 實體中的定義被掛載到目標元素之前叫用,這時的 $el 會是還未被 Vue 實體中的定義渲染的初始設定模板。
  • mounted : Vue 實體上的設置已經安裝上模板,這時的 $el 是已經藉由實體中的定義渲染而成的真正的頁面。
  • beforeUpdate : 當實體中的 data 產生變化後或是執行 vm.$forceUpdate() (opens new window)叫用,這時的頁面還未被重渲染為改變後的畫面。
  • updated : 在重新渲染頁面後叫用,這時的頁面已經被重渲染成改變後的畫面。
  • beforeDestroy : 在此實體被銷毀前時叫用,這時實體還是擁有完整的功能。
  • destroyed : 此實體被銷毀後叫用,這時實體中的任何定義( datamethods等)都已被解除綁定,代表在此做的任何操作都會失效。

鉤子函式會因為引用了其他的工具(例如: vue-router (opens new window))或是 Vue 實體配置的不同(例如: keep-alive (opens new window))而有所增減。

實際演練

接著我們實際來操作各個鉤子函數,然後印出 data 及 $el 看看在各階段會如何變化。

以下是我們使用的例子:

<div id="app"> {{a}} </div>
var vm = new Vue({ el: '#app', data: { a: 1 } });

這個例子在之前的文章中也有使用,只有一個很單純 a 資料以及綁定 Vue 實體到

的範例。

下面會將鉤子函數拆成四組來分析,分別是:

  • beforeCreatecreated : 創建實體。
  • beforeMountmounted : 掛載目標元素。
  • beforeUpdate 及 updated : data 改變後的重渲染。
  • beforeDestroydestroyed : 銷毀實體。

快速結論

beforeCreatecreated
  • beforeCreate : 在 beforeCreate 時因實體還沒創建,所以 a$el 都是 undefined
  • created : 到了 created 時已經創建實例,所以 a 已變為 1 ,但是 $el 因為還未掛載至目標元素,所以依然是 undefined

所以在 beforeCreate 是不能操作實體中的物件的。

beforeMountmounted
  • beforeCreate : 在 beforeCreate 時因實體還沒創建,所以 a$el 都是 undefined
  • created : 到了 created 時已經創建實例,所以 a 已變為 1 ,但是 $el 因為還未掛載至目標元素,所以依然是 undefined

所以在 beforeCreate 是不能操作實體中的物件的。

beforeMountmounted
  • beforeMount : 流程圖上有提到,在叫用 beforeMount 前 Vue 已經決定模板的樣式,所以在 beforeMount 中的 $el 已經有值了,只是它還未依照 Vue 實體上的定義所渲染,只是個初始設定的模板,因此可以看到 {{a}}v-on 這些模板語法都還未被轉換。

  • mounted : 在 mounted 被叫用時已經把 Vue 實體上的定義綁定到元素上,所以這裡看到的是經由 Vue 渲染後的配置。

所以在 beforeMount 前不能操作 DOM 元素。

beforeUpdateupdated
  • beforeUpdate : a 改變後觸發 beforeUpdate ,可以看到 a 已經變為 2 了,可是頁面上還是 1 ,表示這時雖然 data 已經改變,可是還沒有重新渲染畫面。
  • updated : 完成重新渲染的作業後觸發,這時可以看到畫面已經將 1 改為 2了。

updated 時盡量避免修改 data ,這樣有可能再次觸發 update 造成無限循環,如果 data 要連動變化可以使用後面的章節會介紹的 computed (opens new window)屬性。

beforeDestroydestroyed
  • beforeDestroy : 叫用 beforeDestroy 表示即將執行銷毀動作,如果有些物件要釋放資源可以在這處理。
  • destroyed : 叫用 destroyed 時,實體已經銷毀。

由於 Vue 會將 Vue 實體綁定在 this 上,所以在 Vue 實例中只要有使用到 this 的函式都不能使用箭頭函數,因箭頭函數的 this 會綁定上層(父親)的內容,所以箭頭函數中的 this 不會是期望的 Vue 實體。

指令

v-text

  • 預期string

  • 詳細

    更新元素的 textContent。如果要更新部分的 textContent,需要使用 {{ Mustache }} 插值。

  • 示例

    ​​<span v-text="msg"></span>
    ​​<!-- 和下面的一样 -->
    ​​<span>{{msg}}</span>
    

v-html

  • 預期string

  • 詳細

    更新元素的 innerHTML注意:內容按普通HTML插入-不會作為Vue模板進行編譯。如果試圖使用v-html組合模板,可以重新考慮是否通過使用組件來替代。

    在網站上動態渲染任意HTML是非常危險的,因為容易導致 XSS 攻擊。只在可信內容上使用 v-html永不用在用戶提交的內容上。

    單文件組件裡,scoped 的樣式不會應用在 v-html 內部,因為那部分HTML沒有被Vue的模板編譯器處理。如果你希望針對 v-html的內容設置帶作用域的 CSS,你可以替換為 CSS Modules 或用一個額外的全局 <style> 元素手動設置類似 BEM 的作用域策略。

  • 示例

    ​​<div v-html="html"></div>
    

v-model

  • 預期:隨表單控件類型不同而不同。

  • 限制

    • <input>
    • <select>
    • <textarea>
    • components
  • 修飾符

    • .lazy:取代input監聽change事件
    • .number : 輸入字符串轉為有效的數字
    • .trim :輸入首尾空格過濾
  • 用法

    在表單控件或者組件上創建雙向綁定。

v-bind

  • 縮寫:

  • 參數attrOrProp (optional)

  • 修飾符

    • .prop:作為一個 DOM property 綁定而不是作為 attribute 綁定。(差別在哪裡? )
    • .camel:將 kebab-case attribute 名轉換為 camelCase。
    • .sync:語法糖,會擴展成一個更新父組件綁定值的 v-on 偵聽器。
  • 用法

    動態地綁定一個或多個 attribute,或一個組件 prop 到表達式。

<!-- 綁定一個 attribute -->
<img v-bind:src="imageSrc">

<!-- 動態 attribute 名 -->
<button v-bind:[key]="value"></button>

<!-- 縮寫 -->
<img :src="imageSrc">

<!-- 動態 attribute 名縮寫 -->
<button :[key]="value"></button>

v-for

  • 預期Array | Object | number | string | Iterable

  • 用法

    基於源數據多次渲染元素或模板塊。此指令之值,必須使用特定語法 alias in expression,為當前遍歷的元素提供別名:

    ​​<div v-for="item in items">
    ​​  {{ item.text }}
    ​​</div>
    

    另外也可以為數組索引指定別名(或者用於對象的鍵):

    ​​<div v-for="(item, index) in items"></div>
    ​​<div v-for="(val, key) in object"></div>
    

    v-for 的默認行為會嘗試原地修改元素而不是移動它們。要強制其重新排序元素,你需要用特殊 attribute key來提供一個排序提示:

    ​​<div v-for="item in items" :key="item.id">
    ​​  {{ item.text }}
    ​​</div>
    

v-if

  • 預期any

  • 用法

    根據表達式的值的truthiness來有條件地渲染元素。

當和 v-if 一起使用時,v-for 的優先級比 v-if 更高。

v-on

  • 縮寫@

  • 預期Function | Inline Statement | Object

  • 參數event

  • 修飾符

    • .stop:調用 event.stopPropagation()
    • .prevent:調用 event.preventDefault()
    • .capture :添加事件偵聽器時使用capture 模式。
    • .self :只當事件是從偵聽器綁定的元素本身觸發時才觸發回調。
    • .{keyCode | keyAlias} : 只當事件是從特定鍵觸發時才觸發回調。
    • .native :監聽組件根元素的原生事件。
    • .once :只觸發一次回調。
    • .left :只當點擊鼠標左鍵時觸發。
    • .right :只當點擊鼠標右鍵時觸發。
    • .middle :只當點擊鼠標中鍵時觸發。
    • .passive{ passive: true } 模式添加偵聽器
  • 用法

    綁定事件監聽器。事件類型由參數指定。表達式可以是一個方法的名字或一個內聯語句,如果沒有修飾符也可以省略。

<!-- 方法處理器 -->
<button v-on:click="doThis"></button>

Vue 出一個電商網站

基礎 Vue.js 概述

v-class 動態切換 className

一般寫法:

:class="computed 變數名"

物件寫法:

:class="{'class 名稱': 布林值 }"

  • v-on:事件="[這裡可直接放表達式]" => 對照下面範例 @click="isTransform = !isTransform"
<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 運算功能

computed 用法:

computed 的值是一個物件,裡面寫入方法,方法名稱可以直接放入 {{}} 渲染於畫面上:

<div id="app">
  <input type="text" class="form-control" v-model="text">
  <div class="mt-3">
    {{ reverseText }}
  </div>
</div>

<script>
var app = new Vue({
  el: '#app',
  data: {
    text: '',
    newText: ''
  },
  // 請在此撰寫 JavaScript
  computed: {
    reverseText: function () {
      return this.text.split('').reverse().join('')
    }
  },
});
</script>

Methods 與 Computed 的使用情境

  • computed 是在監控資料更動後,重新運算結果呈現於畫面上。一般來說不會修改資料,只會回傳用於畫面呈現的資料

  • methods 就是互動的函式,需要觸發才會運作,會用來修改資料內容。

效能:

如果資料量大,computed 自然會比較慢,只要資料變動就會觸發,無形之中執行次數也會增加。因此在大量資料時,會建議透過 methods 減少不必要的運算喔。

元件基礎概念

Vue.component() 第一個參數為自定義的標籤名稱,第二參數是一個物件,裡面放入 data 和 template。(這裡的 data 是一個方法且回傳一個物件)

每個 <counter-component> 裡面的 counter 都是獨立的不會互相影響。

<div id="app">
  <div>
    你已經點擊 <button class="btn btn-outline-secondary btn-sm" @click="counter += 1">{{ counter }}</button> 下。
  </div>
  <counter-component></counter-component>
  <counter-component></counter-component>
</div>

<script>
Vue.component('counter-component', {
  data: function () {
    return {
      counter: 0,
    }
  },
  template: `
  <div><button class="btn btn-outline-secondary btn-sm" @click="counter += 1">{{ counter }}</button> 下。</div>
  `
})

var app = new Vue({
  el: '#app',
  data: {
    counter: 0
  },
});
</script>

進階模板語法介紹

模板資料細節說明

  • v-once 用於單次綁定,作用一次之後就不會再隨著 v-model 變動。
  • {{ }} 雙大括號內的內容會被渲染成文字,如若想要插入 HTML 請使用 v-html
<h4 class="mt-3">原始 HTML</h4>
  {{ rawHtml }}  <!-- 這裡會呈現字串 -->
<p v-html="rawHtml">請在此加入原始 HTML 結構</p> <!-- v-html 會覆蓋原本的文字內容 -->
var app = new Vue({
  el: '#app',
  data: {
    text: '這是一段文字',
    rawHtml: `<span class="text-danger">紅色文字</span>`,
    number1: 100,
    number2: 300,
    htmlId: 'HTMLID',
    isDisabled: true
  },
});

**注意!**在網站上動態渲染任意HTML是非常危險的,因為容易導致 XSS 攻擊。只在可信內容上使用 v-html永不用在用戶提交的內容上。

  • input 標籤上動態綁定 disabled 屬性:
<input type="text" class="form-control" placeholder="請在此加上動態 disabled" :disabled="isDisabled">
  • {{ }} 雙大括號內可使用運算式:
  <p>練習不同的表達式</p>
  {{ text + rawHtml }} <br>
  {{ text.split('').reverse().join('') }} <br>
  {{ number1 + number2 }}
output to be:
這是一段文字<span class="text-danger">紅色文字</span>
字文段一是這
400

動態切換 ClassName 及 Style 多種方法

+ 物件寫法:
<!-- 可寫入多組物件 -->
<div class="box" :class="{ 'rotate': isTransform, 'bg-danger': boxColor }"></div>
<p>請為此元素加上動態 className</p>
<hr>
<!-- 按鈕加上 click 事件 -->
<button class="btn btn-outline-primary" v-on:click="isTransform = !isTransform">選轉物件</button>
<div class="form-check">
    <!-- input 綁定 v-model -->
    <input type="checkbox" class="form-check-input" id="classToggle1" v-model="boxColor">
    <label class="form-check-label" for="classToggle1">切換色彩</label>
</div>
+ 物件寫法 2:
<div class="box" :class="objectClass"></div>
<p>請將此範例改為 "物件" 寫法</p>
<hr>
<!-- 按鈕加上 click 事件 -->
<button class="btn btn-outline-primary" @click="objectClass.rotate = !objectClass.rotate">選轉物件</button>
<div class="form-check">
    <!-- input 綁定 v-model -->
  <input type="checkbox" class="form-check-input" id="classToggle2" v-model="objectClass['bg-danger']">
  <label class="form-check-label" for="classToggle2">切換色彩</label>
</div>
var app = new Vue({
    el: '#app',
    data: {
      isTransform: false,
      boxColor: false,
      objectClass: {
        'rotate': false,
        'bg-danger': false,
      },
    }
})

注意!v-model 要寫入物件加上「點」運算子,屬性名稱不可帶有 dash - ,例如:

<!-- 此為錯誤寫法 -->
<input type="checkbox" v-model="objectClass.bg-danger">

若屬性名稱帶有 dash - ,應使用中括號取物件值:

<!-- 此為正確寫法 -->
<input type="checkbox" v-model="objectClass['bg-danger']">
+ 陣列寫法:
<!-- 直接將 class 寫入,就沒有動態切換了 -->
<button class="btn" :class="['btn-outline-primary', 'active']">請操作本元件</button>
+ 陣列寫法 2:
<!-- :class 寫入一個在 data 定義的空陣列 -->
<button class="btn" :class="arrayClass">請操作本元件</button>
<p>請用陣列呈現此元件 className</p>
<div class="form-check">
    <!-- input 綁定 v-model,勾選就會把 :value 的值 push 到 arrayClass 裡面 -->
    <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 綁定 v-model,勾選就會把 :value 的值 push 到 arrayClass 裡面 -->
    <input type="checkbox" class="form-check-input" id="classToggle4" v-model="arrayClass" :value="'active'">
    <label class="form-check-label" for="classToggle4">啟用元素狀態</label>
</div>
var app = new Vue({
    el: '#app',
    data: {
      arrayClass: [],
    }
})
+ 綁定行內樣式 :style
<!-- in line style -->
<p>請用不同方式綁定以下行內樣式</p>
<!-- 放入物件 -->
<div class="box" :style="{backgroundColor: 'red'}"></div>
<div class="box" :style="styleObject"></div> <!-- 套用了兩項 -->

<!-- 陣列寫法裡面放入數組物件 -->
<div class="box" :style="[styleObject, styleObject2]"></div>
var app = new Vue({
    el: '#app',
    data: {
      styleObject: {
        backgroundColor: 'red',
        borderWidth: '5px'
      },
      styleObject2: {
        boxShadow: '3px 3px 5px rgba(0, 0, 0, 0.16)'
      }
    }
})

v-for 與其使用細節

+ 陣列與物件的迴圈

差別在於 key 的不同,陣列的 key 是索引值,物件的 key 是屬性名稱:

<ul>
  <li v-for="(item, key) in arrayData">
    {{ key }} - {{ item.name }} {{ item.age }} 歲
  </li>
</ul>
<!-- output ot be:
0 - 小明 16 歲
1 - 漂亮阿姨 24 歲
2 - 杰倫 20 歲
-->
<ul>
  <li v-for="(item, key) in objectData">
    {{ key }} - {{ item.name }} {{ item.age }} 歲
  </li>
</ul>
<!-- output ot be:
ming - 小明 16 歲
auntie - 漂亮阿姨 24 歲
jay - 杰倫 20 歲
-->
var app = new Vue({
  el: '#app',
  data: {
    arrayData: [
      {
        name: '小明',
        age: 16
      },
      {
        name: '漂亮阿姨',
        age: 24
      },
      {
        name: '杰倫',
        age: 20
      }
    ],
    objectData: {
      ming: {
        name: '小明',
        age: 16
      },
      auntie: {
        name: '漂亮阿姨',
        age: 24
      },
      jay: {
        name: '杰倫',
        age: 20
      }
    },
  }
})
+ :key

『 Vue 正在更新使用 v-for 渲染的元素列表時,它默認使用「就地更新」的策略。如果數據項的順序被改變,Vue 將不會移動 DOM 元素來匹配數據項的順序,而是就地更新每個元素,並且確保它們在每個索引位置正確渲染。』

『為了給 Vue 一個提示,以便它能跟踪每個節點的身份,從而重用和重新排序現有元素,你需要為每項提供一個唯一 key 屬性』

+ filter 過濾資料
<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>
var app = new Vue({
  el: '#app',
  data: {
    filterArray: [],
    filterText: ''
  },
  methods: {
    filterData: function () {
      var vm = this;
      vm.filterArray = vm.arrayData.filter(function (item) {
        console.log(vm.filterText, item.name, item.name.match(vm.filterText))
        return item.name.match(vm.filterText);
      });
    },
  }
})
  • filter() 方法會建立一個經指定之函式運算後,回傳由原陣列中通過該函式檢驗之元素所構成的新陣列

  • match() 方法會檢驗是否包含指定字串,若無回傳 null

+ 不能運行的狀況(陣列相關)
  • 修改陣列長度,length 的確會被你改掉,但是畫面渲染還是舊的不變。
  • 透過索引值將陣列元素指派新的值,值會被你改掉,但是畫面渲染還是舊的不變。

正確修改陣列中的元素方式為使用 Vue.set()

  • 使用時機:當你想操作的資料不在 data 裡面的時候
  • 語法:Vue.set(target, key, value)
var app = new Vue({
  el: '#app',
  data: {
    filterArray: [],
    filterText: ''
  },
  methods: {
    cantWork: function () {
      /* 情境一
       this.arrayData.length = 2;
        情境二
       this.arrayData[0] = {
         name: '阿強',
         age: 99
       }
       */
       //解法
       Vue.set(this.arrayData, 0, {
         name: '阿強',
         age: 99
       })
    }
  }
})

+ 可以使用純數字迴圈
<ul>
  <li v-for="item in 5">
    {{ item }}
  </li>
</ul>
<!-- output to be:
1
2
3
4
5
-->
+ Template 的運用

template 標籤不會被渲染到畫面上

<p>請將兩個 tr 一組使用 v-for</p>
<table class="table">
  <template v-for="item in arrayData">
    <tr>
      <td>{{ item.age }}</td>
    </tr>
    <tr>
      <td>{{ item.name }}</td>
    </tr>
  </template>
</table>
+ v-forv-if 一起使用

先執行 v-for 再執行 v-if

<ul>
  <li v-for="(item, key) in arrayData" v-if="item.age <= 20">
    {{ key }} - {{ item.name }} {{ item.age }} 歲
  </li>
</ul>
+ 元件使用 v-for 時,建議都加上 :key ,值盡量是不會重複的,例如 id

v-if 與其使用細節

+ v-if & v-else
<p>使用 v-if, v-else 切換物件呈現</p>
<div class="alert alert-success" v-if="isSuccess === true">成功!</div>
<div class="alert alert-danger" v-if="isSuccess === false">失敗!</div>
<!-- 精簡寫法 -->
<div class="alert alert-success" v-if="isSuccess">成功!</div>
<div class="alert alert-danger" v-if="!isSuccess">失敗!</div>
<!-- v-else 寫法 -->
<div class="alert alert-success" v-if="isSuccess === true">成功!</div>
<div class="alert alert-danger" v-else>失敗!</div>
+ v-if & v-show 的差異

v-show 會將資料都渲染出來再利用 CSS display: none 的方式隱藏起來,v-if 則是連 DOM 元素都不會出現在 HTML 裡面。

watch:

watch 監控一個變數,當這個變數產生變化就執行特定事件

<p>使用 trigger 來觸發旋轉 box、並在三秒後改變回來</p>
<div class="box" :class="{'rotate': trigger }"></div>
<button class="btn btn-outline-primary" @click="trigger = true">Counter</button>
var app = new Vue({
    el: '#app',
    data: {
      trigger: false,
    },
    watch: {
      trigger: function () {
        const vm = this
        setTimeout(() => {
          vm.trigger = false
        }, 3000);
      }
    }
})

表單操作細節

+ 一般寫法:
<select class="form-control" v-model="selected">
    <option disabled value="">請選擇</option>
    <option value="小美">小美</option>
    <option value="可愛小妞">可愛小妞</option>
    <option value="漂亮阿姨">漂亮阿姨</option>
</select>
<p>小明喜歡的女生是 {{ selected }}。</p>
+ 利用 v-for 遍歷 option:
<!-- 記得 value 是動態綁上去的,要加冒號 -->
<select class="form-control" v-model="selected2">  <!-- 加上 mutiple 就變多選 -->
    <option disabled value="">請選擇</option>
    <option :value="item" v-for="item in selectData">{{ item }}</option>
</select>
<p>小明喜歡的女生是 {{ selected2 }}。</p>
var app = new Vue({
  el: '#app',
  data: {
    selected: '',
    selectData: ['小美', '可愛小妞', '漂亮阿姨'],
    selected2: '',
    sex: '男生'
  },
});
+ input="checkbox" 加上 true-value & false-value
<!-- 本來 {{ sex }} 會顯示 true and false -->
<div class="form-check">
    <input type="checkbox" class="form-check-input" id="sex" v-model="sex" true-value="男生" false-value="女生">
    <label class="form-check-label" for="sex">{{ sex }}</label>
</div>
<!-- 調整後讓選取 checkbox 渲染為男生或女生 -->
+ v-model 的修飾符
  • v-model.lazy 不會立即將 input 框框中內容改變反應至 data 的變數中,而是等到離開框框的時候 (blur)。
  • v-model.number 確保 {{ }} 中渲染出來的內容為數值。
  • v-model.trim 減去 input 框框中內容的前後空白。

v-on 與頁面操作細節

+ 事件修飾符
  • .stop - 調用 event.stopPropagation()。
  • .prevent - 調用 event.preventDefault()。
  • .capture - 添加事件偵聽器時使用 capture 捕獲模式。
  • .self - 只當事件是從偵聽器綁定的元素本身觸發時才觸發回調。
  • .once - 只觸發一次回調。
+ 按鍵修飾符
  • .keyCode - 只當事件是從特定鍵觸發時才觸發回調。
  • 別名修飾 - .enter, .tab, .delete, .esc, .space, .up, .down, .left, .right
  • 修飾符來實現僅在按下相應按鍵時才觸發鼠標或鍵盤事件的監聽器 - .ctrl, .alt, .shift, .meta
+ 滑鼠修飾符
  • .left - (2.2.0) 只當點擊鼠標左鍵時觸發。
  • .right - (2.2.0) 只當點擊鼠標右鍵時觸發。
  • .middle - (2.2.0) 只當點擊鼠標中鍵時觸發。

Vue.js 元件

什麼是元件?

每個 Vue.js 的應用程式都是從 Vue 建構式 (vue constructor) 建立根實體 (root vue instance) 開始,再一個個元件 (Components) 搭建上去而來的,透過元件的方式能讓開發者將程式碼封裝而更好利用。此段落節錄自 Summer。桑莫。夏天

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

元件的使用方式

可使用全域或局部的方式註冊元件,但是不論全域註冊或是局部註冊,都必須在 Vue 實例 之前完成。

全域註冊

使用 Vue.component() 語法接收兩個參數,分別為「組件名稱」及「選項物件」:

// js
Vue.component('photo', {
  props: ['imgUrl'],
  template: `
  <div>
  <img :src="imgUrl" class="img-fluid" alt="" />
  <p>風景照</p>
  </div>`
})

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'
  }
})
<!-- html -->
<div id="app">
  <h2>動態傳遞</h2>
  <photo :img-url="url"></photo>
</div>

全域註冊可以用在任何新創建的 Vue 根實例 ( new Vue) 的模板中,因此全局註冊往往是不夠理想的。比如,如果你使用一個像 webpack 這樣的構建系統,全局註冊所有的組件意味著即便你已經不再使用一個組件了,它仍然會被包含在你最終的構建結果中。這造成了用戶下載的 JavaScript 的無謂的增加。

局部註冊

對於 components 物件中的每個 property 來說,其 property 名就是自定義元素的名字,其 property 值就是這個元件的選項物件。

<div id="app">
  <h2>動態傳遞</h2>
  <photo :img-url="url"></photo>
</div>
const template = {
  props: ['imgUrl'],
  template: `
  <div>
  <img :src="imgUrl" class="img-fluid" alt="" />
  <p>風景照</p>
  </div>`
}

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'
  },
  components: {
    'photo': template
  }
})

元件中 data 必須使用 function return

Vue.component('counter-component', {
  data: function () {
    return {
      counter: 0
    }
  },
  template: '#counter-component'
})

載入模板的方式

  • 字串模板

    上述範例不管是全域註冊還是局部註冊都是使用字串模板的方式 :

    ​​const template = {
    ​​  props: ['imgUrl'],
    ​​  template: `
    ​​  <div>
    ​​  <img :src="imgUrl" class="img-fluid" alt="" />
    ​​  <p>風景照</p>
    ​​  </div>`
    ​​}
    ​​// ...略
    
  • X-template

    然而,隨著專案規模的擴增,我們的 HTML 模板結構可能會變得越來越大,光是用 template 直接掛上 HTML 字串時,可能你的程式架構就會變得不是那麼好閱讀、管理,這時候,我們可以把整個 HTML 模板區塊透過 <script id="xxx" type="text/x-template"> </script> 這樣的 <script> 標籤來封裝我們的 HTML 模板,這種方式通常被稱為「X-Templates」。此段落節錄自 Kuro's Blog

    ​​<div id="app">
    ​​  <counter-component></counter-component>
    ​​  <counter-component></counter-component>
    ​​  <counter-component></counter-component>
    ​​</div>
    ​​<!-- x-template 需要指定一個 id -->
    ​​<script type="text/x-template" id="counter-component">
    ​​<div>
    ​​  你已經點擊 <button class="btn btn-outline-secondary btn-sm" @click="counter += 1">{{ counter }}</button> 下。
    ​​</div>
    ​​</script>
    
    ​​Vue.component('counter-component', {
    ​​  data: function () {
    ​​    return {
    ​​      counter: 0
    ​​    }
    ​​  },
    ​​    // template 的屬性值則用 CSS 選取器與 x-template 的 id 配對
    ​​  template: '#counter-component'
    ​​})
    ​​
    ​​var app = new Vue({
    ​​  el: '#app',
    ​​});
    

    官方文件的提醒:

    这些可以用于模板特别大的 demo 或极小型的应用,但是其它情况下请避免使用,因为这会将模板和该组件的其它定义分离开。

  • 載入外部的靜態檔案 (*.vue 檔案) 來做為你元件的內容

    範例同樣可參考自 Kuro's Blog

props 基本觀念

每個元件的資料狀態都是獨立的,透過 props 屬性可以將外部資料傳到內部

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

注意!props 大小寫命名方式,以下為官方文件說明:

HTML 中的 attribute 名是大小寫不敏感的,所以瀏覽器會把所有大寫字符解釋為小寫字符。這意味著當你使用 DOM 中的模板時,camelCase (駝峰命名法) 的 prop 名需要使用其等價的 kebab-case (短橫線分隔命名) 命名:

Vue.component('blog-post', {
// 在 JavaScript 中是 camelCase 的
props: ['postTitle'],
template: '<h3>{{ postTitle }}</h3>'
})
<!-- 在 HTML 中是 kebab-case 的 -->
<blog-post post-title="hello!"></blog-post>

重申一次,如果你使用字符串模板,那麼這個限制就不存在了。

props 使用上的注意事項

+ 單向數據流 (One-Way Data Flow)

Prop 是單向的,只會從父層傳至子層,並且 Prop 的值會隨父層更動設定而改變。若要在子層做處理,可使用「計算屬性」(Computed)自動處理或一個 Local Variable 儲存值以供使用。若 Prop 的值是陣列或物件,記得使用深拷貝(Deep Copy),避免誤觸(Call by Address)的陷阱而更改了父層 Prop 的值。此段落節錄自 Summer。桑莫。夏天

+ 物件傳參考特性 及 尚未宣告的變數

  • 如果資料的匯入有一些時間差,例如透過 AJAX 傳的資料,可使用 v-if 判別傳入的資料的某個屬性是否已經存在來正確渲染畫面:
<!-- 修改前,無法正確渲染畫面,因為 user 的資料是透過 AJAX 傳入,有些時間差導致無法正確渲染 -->
<div class="row">
  <div class="col-sm-4">
    <card :user-data="user"></card>
  </div>
</div>
// js
var app = new Vue({
  el: '#app',
  data: {
    user: {},
    url: '網址',
    isShow: true 
  },
  created: function() {
    var vm = this;
    $.ajax({
      url: 'https://randomuser.me/api/',
      dataType: 'json',
      success: function(data) {
        vm.user = data.results[0];
      }
    });
  }
});
<!-- 修改後,透過 v-if 判別由 AJAX 傳入的資料是否載入完畢-->
<div class="row">
  <div class="col-sm-4">
    <card :user-data="user" v-if="user.phone"></card>
  </div>
</div>
  • 物件傳參考特性 這個部分只是說明如 props 由外部傳到內部的資料是物件的話,當資料被更改時會同步更改。

+ props 型別及預設值

若要替 props 加上型別限制與預設值,則用物件寫入並加上 typedefault 屬性:

// js
Vue.component('prop-type', {
  // 解答:
  props: {
    'cash': {
      type: Number,
      default: 100
    }
  },
  template: '#propType',
  data: function() {
    return {
      newCash: this.cash
    }
  }
})
var app = new Vue({
  el: '#app',
  data: {
    cash: 300
  }
})
<div id="app">
  <h2>Props 的型別</h2>
  <prop-type :cash="cash"></prop-type>

  <h2 class="mt-3">靜態與動態傳入數值差異</h2>
  <prop-type :cash="123"></prop-type>
</div>

<script type="text/x-template" id="propType">
<div>
  <input class="form-control" v-model="newCash">
  {{ typeof(cash)}}
</div>
</script>

**提醒!**若使用 : 動態寫入 props 的值為數字,那它的值就會是數值,所以上例第二個 <prop-type> 標籤的 :cash="123",最後渲染在畫面上時 ({{ typeof(cash) }}) ,結果就為數值 Number。

+ emit 向外層傳遞事件

目的:在元件內本身的 method 透過 this.$emit() 連結外部 data (Vue 實體)的 method 進而改變外部資料。

  1. 在元件內新增元件自己的 method incrementCounter,接著使用 this.$emit('increment') 傳向外層 。(觸發名稱自定義)
  2. 在 HTML 元件標籤 <button-counter> 接收內部 emit 傳來的 @increment 事件,並將外部 method incrementTotal 與之綁定。
    • 內部的資料也可透過 emit 傳到外層,此範例是傳入內部的 counter,並由外部 method 接收參數。
<div id="app">
  <h2>透過 emit 向外傳遞資訊</h2>
  我透過元件儲值了 {{ cash }} 元
  <button-counter v-on:increment="incrementTotal"></button-counter>
  <hr>
  <button-counter></button-counter>
</div>
Vue.component('buttonCounter', {
  template: `<div>
    <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 (newNumber) {
      this.cash += newNumber
    }
  }
})

元件插槽 Slot

元件標籤裡面的內容完全會被模板覆蓋,所以可以使用插槽 Slot 來新增或替換元件標籤的內容。

小叮嚀 1:<slot> 是寫在模板裡面。

小叮嚀 2:<slot> 標籤裡面可以放其他 HTML 標籤喔!

+ 單一插槽 Single slot

範例:

<div id="app">
  <h2>Slot 基礎範例</h2>
  <single-slot-component>
    <p>Slot Slot Slot 在這邊不會選渲染,因為沒有加入 Slot 插槽</p>
  </single-slot-component>
</div>

<script type="text/x-template" id="singleSlotComponent">
<div class="alert alert-warning">
  <h6>我是一個元件</h6>
</div>
</script>
<!-- JS 部分略... -->

渲染結果:

我是一個元件
加上 Slot 插槽:
<div id="app">
  <h2>Slot 基礎範例</h2>
  <single-slot-component>
    <p>Slot Slot Slot</p>
  </single-slot-component>
</div>

<script type="text/x-template" id="singleSlotComponent">
<div class="alert alert-warning">
  <h6>我是一個元件</h6>
  <slot></slot>
</div>
</script>
<!-- JS 部分略... -->

渲染結果:

我是一個元件
Slot Slot Slot
在 Slot 插槽裡可放入預設內容:
<div id="app">
  <h2>Slot 基礎範例</h2>
  <single-slot-component>
    <!-- 元件標籤內沒有內容時就會渲染 slot 插槽裡預設的內容 -->
  </single-slot-component>
</div>

<script type="text/x-template" id="singleSlotComponent">
<div class="alert alert-warning">
  <h6>我是一個元件</h6>
  <slot>
    預設內容
  </slot>
</div>
</script>
<!-- JS 部分略... -->

渲染結果:

我是一個元件
預設內容

反之:

<div id="app">
  <h2>Slot 基礎範例</h2>
  <single-slot-component>
    <p>Slot Slot Slot</p>
  </single-slot-component>
</div>

<script type="text/x-template" id="singleSlotComponent">
<div class="alert alert-warning">
  <h6>我是一個元件</h6>
  <slot>
    預設內容,因為元件標籤內有其他內容所以不會渲染
  </slot>
</div>
</script>
<!-- JS 部分略... -->

渲染結果:

我是一個元件
Slot Slot Slot

+ 具名插槽

替元件標籤內的要替換的標籤新增 slot 屬性,模板內的 <slot> 新增 name 屬性進行配對。

注意!<slot><template> 標籤都不會被渲染出來。

範例:

<div id="app">
  <named-slot-component>
    <header slot="header">替換的 Header</header>
    <template slot="footer">替換的 Footer</template>
    <template slot="btn">按鈕內容</template>
    <p>其餘的內容</p>
  </named-slot-component>
</div>

<script type="text/x-template" id="namedSlotComponent">
  <div class="card my-3">
    <div class="card-header">
      <slot name="header">這段是預設的文字</slot name="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>
    </div>
    <div class="card-footer">
      <slot name="footer">這是預設的 Footer</slot>
    </div>
  </div>
</script>

使用 :is 動態切換元件

+ 靜態切換元件

<!-- 以下兩種寫法結果相同 -->
<div id="app">
  <primary-component :data="item"></primary-component> <!-- 第一種 -->
  <div is="primary-component" :data="item"></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>
Vue.component('primary-component', {
  props: ['data'],
  template: '#primaryComponent',
});
var app = new Vue({
  el: '#app',
  data: {
    item: {略...},
    current: 'primary-component'
  }
})

+ 動態切換元件

也可使用 v-if 寫法去判別(註解部分),但若要切換數量過多還是建議用 :is

<div id="app">
  <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">
  // 略...
</script>

<script type="text/x-template" id="dangerComponent">
  // 略...
</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: {略...},
    current: 'primary-component'
  }
})

Vue 常用 API

使用 Extend 避免重複造輪子

目的:若兩個元件內容很相近,差異非常小,就可將重複的內容放入 extends。

範例:

childOne 和 childTwo 只有 template 綁定的對象不同而已

var childOne = {
  props: ['item'],
  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 childTwo = {
  props: ['item'],
  data: function() {
    return {
      data: {},
      extendData: '這段文字是 extend 得到'
    }
  },
  template: '#row-component-two',
  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)
  }
}

使用 extends:

// 把重複的內容放入 Vue.extend()
var newExtend = Vue.extend({
  props: ['item'],
  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 = {
  extends: newExtend
}
// 注意 childTwo 會有繼承的 data 也會有自己 data,但若是屬性名稱相同,值將會被覆蓋
var childTwo = {
  data: function () {
  return {
    childTwo: '元件 2',
    extendData: '這段文字是 自己的'
   }
  },
  template: '#row-component-two',
  extends: newExtend,
  // 注意 childTwo 會執行繼承的 mounted 也會執行自己 mounted
  mounted () {
      // ...略
  }
}

Filter 自訂畫面資料呈現格式

**filters 的作用: 主要用來「處理格式化文字」,適合使用時機在「非資料處理,只用於畫面上的呈現」,並且可以全域使用。 **

用法:使用 | 符號代入 filter 變數。

範例:

<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 }}</td> // 左邊的結果將作為參數傳入 currency
<td>{{ item.icash | currency | dollarSign}}</td> // item.icash | currency 將作為參數傳入 dollarSign
</tr>
</script>
var child = {
  props: ['item'],
  template: '#row-component',
  data: function () {
    return {
      data: {}
    }
  },
  mounted: function () {
    console.log('Component:', this)
  },
  filters: {
    currency: function (n) {
        // 將數值加上千分號
      return n.toFixed(2).replace(/./g, function (c, i, a) {
        return i && c !== "." && ((a.length - i) % 3 === 0) ? ',' + c : c;
      });
    },
    dollarSign (n) {
      return `$ ${n}`
    }
  }
}

var app = new Vue({
  el: '#app',
  data: {
    data: [/*... 略*/]
  },
  components: {
    "row-component": child
  },
});

注意 {{ data[1].cash }} 這段程式碼會無法正確渲染,因為此範例的 filter 屬性是註冊在元件內無法適用於外部,若要解決此問題可將 filter 全域註冊:

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;
  });
});

+ 無法寫入的資料,用 set 搞定他

語法:vm.$set(target, key, value)

此範例點擊按鈕後,打開開發者工具觀看,資料確實已經寫入 data 裡的 item,但為什麼畫面無法正確渲染?其原因為一開始沒有被註冊到的物件不會響應式更新。要解決此問題可以一開始就將 item 屬性名稱寫入 data(註解一),或者使用 vm.$set() 語法寫入,更多詳情可參考:

為什麼畫面沒有隨資料更新?- Vue 響應式原理(Reactivity)

<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>
var child = {
  props: ['item'],
  template: '#row-component',
  data: function() {
    return {
      data: {
        // item: {} 註解一在這
      }
    }
  },
  methods: {
    addData: function() {
      this.data.item = {
        name: this.item.name
      }
      console.log(this.data, this);
      // 註解二
      // this.$set(this.data, 'item', {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)
  }
});

+ Mixin 混合其它的元件內容

mixins 與 extends 大同小異,用法也一樣,不過 mixins 是一個陣列可以放入多筆值:

var mixinFilter = {
  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 = {
  data: function() {
    return {
      dataMixins: 'Mixins Data'
    }
  },
  mounted () {
    console.log('這段是 Mixin 產生')
  }
}

Vue.component('row-component', {
  props: ['item'],
  data: function() {
    return {
      data: {},
    }
  },
  mixins: [mixinFilter, mixinMounted]
});

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,
      },
    ]
  },
  mounted: function() {
    console.log('Vue init:', this)
  }
})

+ 使用 Directive 開發自己的互動 UI

目的:建立自定義指令。

語法:

// 全域註冊寫法
Vue.directive('自定義指令', {
    // 以下三個較常用的鉤子函數(可選)
    bind () {}
    inserted () {}
	update () {}
})

// 局部註冊寫法
directives: {
  '自定義指令': {
   		// 鉤子函數
    }
  }
}

在 HTML 標籤上加入自定義指令即可作用:

<!-- v-focus 和 v-validator 為自定義指令-->
<div id="app">
  <input type="email" v-model="email" v-focus v-validator>
</div>

鉤子函數

一個指令定義對象可以提供如下幾個鉤子函數(均為可選):

  • bind:只調用一次,指令第一次綁定到元素時調用。在這裡可以進行一次性的初始化設置。
  • inserted:被綁定元素插入父節點時調用(僅保證父節點存在,但不一定已被插入文檔中)。
  • update:所在組件的VNode更新時調用,但是可能發生在其子 VNode 更新之前。指令的值可能發生了改變,也可能沒有。但是你可以通過比較更新前後的值來忽略不必要的模板更新。
  • componentUpdated:指令所在組件的 VNode**及其子 VNode **全部更新後調用。
  • unbind:只調用一次,指令與元素解綁時調用。

補充:bind 和 inserted 的概念類似於 created 和 mounted。

接下來我們來看一下鉤子函數的參數(即elbindingvnodeoldVnode)。

鉤子函數參數

  • el:指令所綁定的元素,可以用來直接操作 DOM。

  • binding:一個對象,包含以下 property:

    • name:指令名,不包括 v- 前綴。
    • value:指令的綁定值,例如:v-my-directive="1 + 1" 中,綁定值為 2
    • oldValue:指令綁定的前一個值,僅在 updatecomponentUpdated 鉤子中可用。無論值是否改變都可用。
    • 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 編譯生成的虛擬節點。移步 VNode API 來了解更多詳情。

  • oldVnode:上一個虛擬節點,僅在 updatecomponentUpdated 鉤子中可用。

+ Directive 細節說明

  • 自定義指令可以傳入值進入 binding 裡面:
<div id="app">
  <input type="email" v-model="email" v-focus v-validator="{ className: 'form-control' }">
</div>
// 傳入的值會出現在 binding.value 裡面
Vue.directive('validator', {
    bind (el, binding) {
        console.log('binding.value.className') // form-control
    }
})

Vue Cli 的建置與運作原理

+ 為什麼要學 Vue Cli

VUE CLI 是什麼?

  1. 基於 Webpack 所建置的開發工具
  2. 便於使用各種第三方套件 (BS4, Vue Router)
  3. 可運行 Sass, Babel 等編譯工具
  4. 便於開發 SPA 的網頁工具
  5. 簡單設定就能搭建開發時常用的 環境

VUE CLI 的限制

不便於開發非 SPA 的網頁(此需求可用 CDN 模式)

Q&A

Q: 是不是表示當我開發的不是SPA(沒用到vue-route),就是使用CDN的方式嗎?
A: 是的

+ 如何使用 Vue Cli (2.x 版本)

安裝:npm install -g vue-cli (注意這是 2.x 版本)

使用:vue init <template-name> <project-name>

  • 範例:vue init webpack my-project

+ Vue Cli 所產生的資料夾結構說明

  • static 資料夾放入不會被編譯的檔案
  • VueCli 的「.vue」檔可以視為一個元件,而這個元件可以讓別的 「.vue」檔案使用

+ 補充:安裝其他插件

  • vue-axios:npm install --save axios vue-axios
    • 注意若你是使用 Vue.js 2.x ,vue-axios 必須使用 2.x 舊版,請在 package.jsondependencies 移除 "vue-axios",並安裝 2.x 版本:npm install --save vue-axios@2

Vue Router

+ 使用 Vue Router 及配置路由文件

三個重點:

  1. 進入點 main.js
  2. Router 配置檔案(前端路由) router/index.js
  3. 分頁內容 Vue Components **.vue

步驟:

  1. 先安裝:npm install vue-router --save

  2. src 資料夾底下新增 router 資料夾,router 資料夾再新增 index.js 檔案

    • index.js 需配置:
    ​​​// 官方的元件
    ​​​import Vue from 'vue'
    ​​​import VueRouter from 'vue-router'
    ​​​
    ​​​// 自訂的分頁元件
    ​​​import Home from '@/components/HelloWorld' // @ 代表 src 這個路徑
    ​​​
    ​​​Vue.use(VueRouter)
    ​​​
    ​​​// 匯出給 entry 使用 (main.js)
    ​​​export default new VueRouter ({
    ​​​    routes: [
    ​​​        {
    ​​​            name: '首頁', // 元件呈現的名稱
    ​​​            path: '/index', // 對應的虛擬路徑
    ​​​            component: Home, // 對應的元件
    ​​​        }
    ​​​    ]
    ​​​})
    
  3. 回到 main.js,新增配置(註解部分):

    ​​​import Vue from 'vue'
    ​​​import App from './App'
    ​​​import axios from 'axios'
    ​​​import vueAxios from 'vue-axios'
    ​​​import router from './components/router' // import router
    ​​​Vue.use(vueAxios, axios)
    ​​​   
    ​​​Vue.config.productionTip = false
    ​​​   
    ​​​new Vue({
    ​​​  el: '#app',
    ​​​  components: { App },
    ​​​  template: '<App/>',
    ​​​  router // 新增 router
    ​​​})
    
  4. 回到 App.vue,將模板標籤 <HelloWorld/> 改為 <view-router>

    ​​​<template>
    ​​​  <div id="app">
    ​​​    <img src="./assets/logo.png">
    ​​​    <!-- <HelloWorld/> -->
    ​​​    <router-view></router-view>
    ​​​  </div>
    ​​​</template>
    ​​​<!-- 以下略... <script> & <style> -->
    

    補充

    <view-router>router/index.js 的以下內容

    ​​​export default new VueRouter ({
    ​​​    routes: [
    ​​​        {
    ​​​            name: '首頁', // 元件呈現的名稱
    ​​​            path: '/index', // 對應的虛擬路徑
    ​​​            component: Home, // 對應的元件
    ​​​        },
    ​​​        {
    ​​​            name: '分頁', // 元件呈現的名稱
    ​​​            path: '/page', // 對應的虛擬路徑
    ​​​            component: Page, // 對應的元件
    ​​​        }
    ​​​    ]
    ​​​})
    

    相對應。

+ 新增路由路徑及連結

  1. src/components 資料夾下新增 pages 資料夾,底下新增 page.vue (自定義內容)

  2. router/index.js,配置:

    ​​​// 官方的元件
    ​​​import Vue from 'vue'
    ​​​import VueRouter from 'vue-router'
    ​​​
    ​​​// 自訂的分頁元件
    ​​​import Home from '@/components/HelloWorld' // @ 代表 src 這個路徑
    ​​​import Page from '@/components/pages/page' // import page
    ​​​
    ​​​Vue.use(VueRouter)
    ​​​
    ​​​export default new VueRouter ({
    ​​​    routes: [
    ​​​        {
    ​​​            name: '首頁', // 元件呈現的名稱
    ​​​            path: '/index', // 對應的虛擬路徑
    ​​​            component: Home, // 對應的元件
    ​​​        },
    ​​​        { // 新增 page 路由
    ​​​            name: '分頁', 
    ​​​            path: '/page', 
    ​​​            component: Page, 
    ​​​        }
    ​​​    ]
    ​​​})
    
  3. 分頁切換製作,回 App.vue 將超連結 a 標籤替換為 <routerLink>href 屬性改為 to="路徑名稱" 或者 :to="{ name: '路由名稱' }"router/index.js 的配置)

    ​​​<template>
    ​​​  <div id="app">
    ​​​    <nav class="navbar navbar-expand-lg navbar-light bg-light">
    ​​​      <div class="container-fluid">
    ​​​        <a class="navbar-brand" href="#">Navbar</a>
    ​​​        <div class="collapse navbar-collapse" id="navbarSupportedContent">
    ​​​          <ul class="navbar-nav me-auto mb-2 mb-lg-0">
    ​​​            <li class="nav-item">
    ​​​              <routerLink class="nav-link" aria-current="page" to="/index">Home</routerLink>
    ​​​              <!-- 或替換為 :to="{name: '首頁'}" -->
    ​​​            </li>
    ​​​            <li class="nav-item">
    ​​​              <routerLink class="nav-link" aria-current="page" to="/page">Page</routerLink>
    ​​​            </li>
    ​​​          </ul>
    ​​​        </div>
    ​​​      </div>
    ​​​    </nav>
    ​​​    <img src="./assets/logo.png">
    ​​​    <!-- <HelloWorld/> -->
    ​​​    <router-view></router-view>
    ​​​  </div>
    ​​​</template>
    ​​​<!-- 以下略... -->
    

+ 製作巢狀路由頁面

承上,

  1. src/components/pages 底下新增三個 child 檔案,分別為 child.vuechild2.vuechild3

  2. page.vue ,加上 router-view 標籤:

    ​​​<template>
    ​​​  <div class="hello">
    ​​​    <div class="card" style="width: 18rem;">
    ​​​      <router-view></router-view>
    ​​​    </div>
    ​​​  </div>
    ​​​</template>
    
  3. router/index.jscompontent: Page 加上 children 屬性:

    ​​​export default new VueRouter ({
    ​​​    routes: [
    ​​​        {
    ​​​            name: '首頁', // 元件呈現的名稱
    ​​​            path: '/index', // 對應的虛擬路徑
    ​​​            component: Home, // 對應的元件
    ​​​        },
    ​​​        {
    ​​​            name: '分頁', 
    ​​​            path: '/page', 
    ​​​            component: Page,
    ​​​            children: [
    ​​​                {
    ​​​                    name: '卡片1', 
    ​​​                    path: '', 
    ​​​                    component: child, 
    ​​​                },
    ​​​                {
    ​​​                    name: '卡片2', 
    ​​​                    path: 'child2', 
    ​​​                    component: child2, 
    ​​​                },
    ​​​                {
    ​​​                    name: '卡片3', 
    ​​​                    path: 'child3', 
    ​​​                    component: child3, 
    ​​​                },
    ​​​            ]
    ​​​        }
    ​​​    ]
    ​​​
    ​​​/**注意事項**
    ​​​因為 Card1 沒有設定 path,也就是 path: '',代表說在 Pages 下,會預設載入卡片1 這個元件,所以使用 to="/page" 會顯示卡片1,而若是動態使用 :to="{ name: '分頁' }" 則直接顯示其內容,**Card1 這個元件不會被顯示出來**。/*
    
  4. page.vue 加上 routerLink 連結:

    ​​​<template>
    ​​​  <div class="hello">
    ​​​    <router-link to="/page">卡片 1</router-link>
    ​​​    <router-link to="/page/child2">卡片 2</router-link>
    ​​​    <router-link to="/page/child3">卡片 3</router-link>
    ​​​    <div class="card" style="width: 18rem;">
    ​​​      <router-view></router-view>
    ​​​    </div>
    ​​​  </div>
    ​​​</template>
    

+ 使用動態路由切換頁面 Ajax 結果

承上,以 child3 為例:

  1. router/index.js 將 child3 更改為動態路由:

    ​​​{
    ​​​    name: '卡片3', 
    ​​​    path: 'child/:id', 
    ​​​    component: child3, 
    ​​​},
    
  2. child3.vuethis.$route 可以找到當前路由相關資訊

    ​​​<script>
    ​​​export default {
    ​​​    // this.$route.path 可以找到當前路徑
    ​​​  created() {
    ​​​    console.log(this.$route.params.id); // 找到當前路徑最後的參數
    ​​​    const id = this.$route.params.id
    ​​​    this.$http.get(`https://randomuser.me/api/?seed=${id}`).then(res => console.log(res))
    ​​​  },
    ​​​}
    ​​​</script>
    

    注意 this.$route.params.自定義名稱 需與路由 index.js 中相配對:

    ​​​// this.$route.params.id 路由這邊要相同名稱
    ​​​{
    ​​​  path: '/customer_order/:id',
    ​​​  name: 'CustomerOrderID',
    ​​​  component: CustomerOrderID
    ​​​},
    ​​​-MT9zlzkmqpbBq6Vqbas
    

+ 命名路由,同一個路徑載入兩個頁面元件

  • 若欲在 /page 路徑下載入兩個元件頁面,回 router/index.js
// 官方的元件
import Vue from 'vue'
import VueRouter from 'vue-router'

// 自訂的分頁元件
import Home from '@/components/HelloWorld' // @ 代表 src 這個路徑
import Page from '@/components/pages/page'
import child from '@/components/pages/child'
import child2 from '@/components/pages/child2'
import child3 from '@/components/pages/child3'
import Menu from '@/components/pages/menu' // 記得要 import 進來

Vue.use(VueRouter)
export default new VueRouter ({
    routes: [
        {
            name: '首頁', // 元件呈現的名稱
            path: '/index', // 對應的虛擬路徑
            component: Home, // 對應的元件
        },
        {
            // name: '分頁',
            path: '/page',
            // component: Page,
            components: { // component 改為複數並且設定 default
                default: Page,
                menu: Menu
            },
            children: [/* 略 */]
        }
    ]
})
  • App.vue 中配置 <router-view> 並新增 name 屬性與 router/index.js 中的 meue: Menu 配對:
<template>
  <div id="app">
    <nav class="navbar navbar-expand-lg navbar-light bg-light">
      <div class="container-fluid">
        <a class="navbar-brand" href="#">Navbar</a>
        <div class="collapse navbar-collapse" id="navbarSupportedContent">
          <ul class="navbar-nav me-auto mb-2 mb-lg-0">
            <li class="nav-item">
              <routerLink class="nav-link" aria-current="page" :to="{name: '首頁'}">Home</routerLink>
            </li>
            <li class="nav-item">
              <routerLink class="nav-link" aria-current="page" to="/page">Page</routerLink>
            </li>
          </ul>
        </div>
      </div>
    </nav>
    <img src="./assets/logo.png">
    <router-view name="menu"></router-view>
    <div class="container">
      <router-view></router-view>
    </div>
  </div>
</template>

Vue Router 參數設定

+ 切換 mode 模式

export default new VueRouter ({
    mode: 'history',
    routes: [
        {
            name: '首頁', // 元件呈現的名稱
            path: '/index', // 對應的虛擬路徑
            component: Home, // 對應的元件
        },
    ]
})

原本路徑:http://localhost:8080/#/page/child3

切換為 'history' mode 之後路徑不再帶有 # 字號:http://localhost:8080/page/child3

注意

若使用 'history' 模式代表路由由後端提供,前後端必須搭配,建議不啟用此模式,使用預設模式即可。

+ base 設定

  • 默認值: "/"

    應用的基路徑。例如,如果整個單頁應用服務在/app/下,然後base就應該設為"/app/"

+ linkActiveClass

  • 類型: string

  • 默認值: "router-link-active"

    全局配置<router-link>默認的激活的class。參考 router-link

+ linkExactActiveClass

  • 類型: string

  • 默認值: "router-link-exact-active"

    全局配置 <router-link> 默認的精確激活的class。可同時翻閱 router-link

自定義切換路由方法

自定義方法:

<template>
  <div class="hello">
    <ul class="nav">
      <li class="nav-item">
        <router-link to="/page" class="nav-link">卡片 1</router-link>
      </li>
      <li class="nav-item">
        <router-link to="/page/child2" class="nav-link">卡片 2</router-link>
      </li>
      <li class="nav-item">
        <router-link to="/page/child3" class="nav-link">卡片 3</router-link>
      </li>
      <li class="nav-item">
        <a href="#" class="nav-link" @click.prevent="updatePath">切換到指定頁面</a>
      </li>
      <li class="nav-item">
        <a href="#" class="nav-link" @click.prevent="beforePath">回到上一頁</a>
      </li>
    </ul>
  </div>
</template>

<script>
  export default {
    data() {
      return {

      }
    },
    methods: {
      beforePath () {
        this.$router.back()
      },
      updatePath () {
        this.$router.push('/page/child2') // 指定回到第二張卡片位置
      },
    },
  }
</script>

更多自定義用法可參考 此篇