Vue.js (9.1) - 元件(Component)

Vue.js

基本上,看完前面幾個章節的內容就已經瞭解 Vue.js 的基本功能了,但 Vue.js 還提供許多很棒的功能,其中之一就是元件。元件就像一個自訂的 HTML 元素,將相關的功能全都封裝在一起,它們就像一個個的積木,你可以用它們來建立整個網站,而且不但易於日後維護,同時也容易重覆使用。

建立元件

你可以透過兩種方式建立元件,在 Vue 實體內的稱為區域元件,只有該實體能使用;而利用 Vue.component() 方法直接建立的就是全域元件,可以在多個實體上共用。

全域元件

要建立全域元件,必須使用 Vue.component(id, [definition]) 方法:
  • id:字串,是元件被看到的名稱,不能和其他全域元件重覆。
  • definition:函式或物件,為選擇性參數。
使用範例:
HTML<div id="vm">
  <book></book>
</div>

<script>
Vue.component('book', {
  template: '<span>Vue.js 入門</span>'
})

var vm = new Vue({
  el: '#vm'
})
</script>
我們建立了一個元件,名字叫做 book,雖然它是不區分大小寫的,但為了讓它看起來像是 HTML 元素,建議遵循 W3C規則一律用小寫,可以包含一個橫線。這個名稱所建立的元件,就是在 HTML 中的元素 <book></book>

在元件中,我們用 template 樣板屬性寫了一個簡單的樣板內容,執行後,Vue.js 會將它置換到 <book></book>,最後瀏覽器看到的內容是這樣:
HTML<div id="vm">
  <span>Vue.js 入門</span>
</div>
注意!全域元件的建立必須寫在 Vue 實體使用它之前。

區域元件

你也可以建立區域元件,也就是只存在單一 Vue 實體中的元件:
JavaScript<script>
var vm = new Vue({
  el: '#vm',
  components: {
    'book': {
      template: '<span>Vue.js 入門</span>'
    }
  }
})
</script>
這次用到 components 屬性,建立一個名為 book 的元件,內容則和前範例一樣。
元件中的 data 必須是函數
在元件中使用 data 屬性時,記得必須改為使用函式,否則 Vue 會終止執行。使用函式的原因是為了每次回傳的資料是新建立的,而不是同一個資料,如果所有元件都指向同一個資料,那當其中一個元件更動了這個資料時,其他元件的資料也會跟著變動,因為它們實際上是指向同一個資料。

以下這個範例是模擬回傳同一筆資料會發生的情況,因為使用資料屬性會被 Vue.js 停止,所以這裡用全域變數去模擬,這個範例只是為了說明 Vue.js 將資料屬性設計成強制使用函式回傳值的原因:
HTML<div id="vm">
  <counter>計數器 A</counter>
  <counter>計數器 B</counter>
  <counter>計數器 C</counter>
</div>

<script>
// 要被所有元件取用的資料
var count = { count: 0 }

Vue.component('counter', {
  template: '<button @click="count++">{{ count }}</button>',
  data: function () {
    // 全部指向元件外的同一筆資料
    return count
  }
})

var vm = new Vue({
  el: '#vm'
})
</script>
計數器 A 被按一下時,它讓 count + 1 了,但同時 計數器 B計數器 C 也發生了改變,這是不對的,每個元件的資料應該是獨立的,這就是為什麼元件中的資料應該由元件內部自行建立並回傳,以下是正確使用的範例:
JavaScript// 刪除 count 全域變數
// ...略
data: function () {
  return { 
    count: 0 
  }
}
// ...略
這樣每次要取用 data 屬性時,都會得到一個新建立的資料。

元件組合

一個元件可以包含另一個或多個其他的元件,當一個元件包含另一個元件,它們就形成了父子關係,這時候就必須知道父子元件如何互相通訊。在 Vue.js 中,父子元件的溝通過程是 props down, events up,也就是父元件透過 props (特性) 將資料傳遞給子元件,子元件則透過 events (事件) 向父元件發送訊息。圖示如下: vue-props-events

(圖片來源:https://cn.vuejs.org/v2/guide/components.html)

父元件透過特性 (Prop) 傳遞資料給子元件

為了避免元件和元件間形成藕合,每個元件的作用域都(必須)是互相獨立的,也就是你不能(也不應該)在子元件中直接引用父元件的資料(不應該假設元件會屬於某個元件)。你應該透過 props 屬性將資料傳給子元件。
HTML<div id="vm">
  <my-profile name="Tony" age="5"></my-profile>
  <my-profile name="Jack"></my-profile>
  <my-profile></my-profile>
</div>

<script>
Vue.component('my-profile', {
  props: ['name', 'age'],
  template: '<p v-if="name">我是 {{ name }} 今年 {{ age || "?" }} 歲</p>'
})

new Vue({
  el: '#vm'
})
</script>
結果:
Output我是 Tony 今年 5 歲
我是 Jack 今年 ? 歲
我們建立了一個元件 my-profile,並建立了兩個自訂屬性 nameage;在樣板中,如果 name 有被設定才會顯示內容。在設計元件的時候,你不應該假設 props 指定的自訂屬性總是會被指定值,所以你應該做好沒有值的應對方法。

props 的值可以是陣列或物件,用於接收來自父元件的資料。陣列只能簡單的接受資料傳入,它將會成為元件中的自訂屬性;使用物件則可以額外設定資料類型的檢查、指定預設值等更複雜的配置。

駝峰式名稱的問題

因為 HTML 的屬性是不分大小寫的,所以元件中以駝峰式命名的 prop ,必須在 HTML中改用橫線連接的方式:
HTML<div id="vm">
  <!-- 顯示:我是 -->
  <my-profile myName="Tony"></my-profile>
  
  <!-- 顯示:我是 Tony -->
  <my-profile my-name="Tony"></my-profile>
</div>

<script>
Vue.component('my-profile', {
  props: ['myName'],
  template: '<p>我是 {{ myName }}</p>'
})

new Vue({
 el: '#vm'
})
</script>
如果是使用字串模板,就沒有這個限制。

動態 Prop

你可以用 v-bind 將父元件的資料綁定到子元件的 props ,形成動態 Prop,當父元件中的資料改變時,子元件會立刻收到這個改變後的資料:
HTML<div id="vm">
  <input type="text" v-model="name">
  <my-profile :my-name="name"></my-profile>
</div>

<script>
Vue.component('my-profile', {
  props: ['myName'],
  template: '<p>我是 {{ myName }}</p>'
})

new Vue({
  el: '#vm',
  data: {
    name: 'Tony'
  }
})
</script>
v-bind:my-name 可以縮寫成 :my-name,我們將這個元件自訂屬性綁定到 name 這個資料屬性上,當 name 發生變動,my-name 屬性,也就是元件的 myName 特性(prop)將會收到這個變動後的資料,於是樣板中的 {{ myName }} 就會顯示新的資料。

傳遞的是字串還是數值

在傳遞資料給 prop 特性時,有個細節必須注意,你傳入的資料都是字串。如果要傳入數值,必須使用 v-bind,為什麼要注意傳入的是字串還是數值呢?看看以下範例:
HTML<div id="vm">
  <my-profile :age="5"></my-profile>
  <my-profile age="5"></my-profile>
</div>

<script>
Vue.component('my-profile', {
  props: ['age'],
  template: '<p>我今年 {{ age + 1 }} 歲 {{ typeof age }}</p>'
})

new Vue({
  el: '#vm'
})
</script>
輸出結果:
Output我今年 6 歲 number
我今年 51 歲 string
當你在 {{ }} 中使用表達式運算,+ 運算子會因資料型態不同而有不同的行為,對數值做加法,對字串做串接,所以當你需要傳入數值時,記得要用 v-bind

父元件的資料只能單向流入子元件

記得前面說過,元件溝通的方是 props down, events up,意即 prop 只能單向綁定,當父元件的屬性發生變動,會立即將變動的資料傳給子元件,但這個方向不會反過來,這樣可以防止子元件無意間修改了父元件的資料。因此,當你在子元件內部修改 prop 的值時,Vue 會在主控台發出警告。
錯誤示範:
HTML<div id="vm">
  <my-profile my-name="Smith" gender="male"></my-profile>
  <my-profile my-name="Taylor" gender="female"></my-profile>
</div>

<script>
Vue.component('my-profile', {
  props: ['myName', 'gender'],
  
  data: function () {
    if (this.gender.toLowerCase() === 'male') {
      this.myName = "Mr. " + this.myName
    } else if (this.gender.toLowerCase() === 'female') {
      this.myName = "Ms. " + this.myName
    }
    
    return { 
      theName: this.myName 
    }
  },
  
  template: '<p>{{ theName }}</p>'
})

new Vue({
  el: '#vm'
})
</script>
這裡對 this.myName 重新指派新的值,就會引發 Vue 的警告。
正確示範:
JavaScript// ...略
data: function () {
  var theName = ''
  if (this.gender.toLowerCase() === 'male') {
    theName = "Mr. " + this.myName
  } else if (this.gender.toLowerCase() === 'female') {
    theName = "Ms. " + this.myName
  }
  
  return { 
    theName: theName 
  }
},
// ...略
藉由一個區域變數來儲存新的資料,然後回傳它,就可以避免修改到父元件的資料。

除了使用 data 屬性,你也可以使用計算屬性 computed
JavaScript// ...略
Vue.component('my-profile', {
  props: ['myName', 'gender'],
  
  computed: {
    theName: function () {
      if (this.gender.toLowerCase() === 'male') {
        return "Mr. " + this.myName
      } else if (this.gender.toLowerCase() === 'female') {
        return "Ms. " + this.myName
      }
      return this.myName
    }
  },
  
  template: '<p>{{ theName }}</p>'
})
// ...略

檢查輸入 Prop 的資料

有一天,你寫的元件可能會給別人用,使用你的元件的人可能會傳入不符合規定的資料,為了防止出錯,你可以指定 prop 的資料類型、是否為必要或是預設值等等條件限制。如此一來,當使用你的元件的人傳入不符合規定的資料,Vue 就會發出警告。

要指定檢查的規格只能使用物件,不能用前面範例所使用的陣列型式:
HTML<div id="vm">
  <my-profile :id="1" password="ab12" name="Tony" :age="6"></my-profile>
</div>

<script>
Vue.component('my-profile', {
  props: {
    // 指定資料型態為數值
    id: Number,
    
    // 指定多種資料型態
    password: [String, Number],
    
    // 指定為字串型態,且為必要屬性
    name: {
      type: String,
      required: true
    },
    
    // 指定為數值型態,且有預設值
    age: {
      type: Number,
      default: 0,
      validator: function (value) {
        return value >= 0
      }
    },
    
    // 指定為陣列型態,且有預設值,
    // 物件或陣列的資料型態其預設值由函數回傳值
    skills: {
      type: Array,
      default: function(){
       return ['nothing']
      }
    }
  },
  
  template: '<p>id: {{ id }} <br> password: {{ password }} <br> age: {{ age }} <br> skills: {{ skills }}</p>'
})

new Vue({
  el: '#vm'
})
</script>
你可以直接指定資料型態,如果有多個可以用陣列表示;除了資料型態以外的規格就要用物件來指定,type 指定資料型態、default 指定預設值、validaotr指定自訂的驗證函式、required指定該屬性是否為必要。

type 可以使用的資料型態:
  • String
  • Number
  • Boolean
  • Function
  • Object
  • Array
type 也可以是自訂的建構式函式,使用 instanceof 來檢測。

註:你必須使用開發版本的 Vue.js ,當檢查發生不符合規定時,才會在主控台看到 Vue 發出的警告。
本文網址:http://blog.tonycube.com/2017/05/vuejs-9-1-component.html
Tony Blog 撰寫,請勿全文複製,轉載時請註明出處及連結,謝謝 😀

我要留言

留言小提醒:
1.回覆時間通常在晚上,如果太忙可能要等幾天。
2.請先瀏覽一下其他人的留言,也許有人問過同樣的問題。
3.程式碼請先將它編碼後再貼上。(線上編碼:http://bit.ly/1DL6yog)
4.文字請加上標點符號及斷行,難以閱讀者恕難回覆。
5.感謝您的留言,您的問題也可能幫助到其他有相同問題的人。