現今算是 Web Component 當道,如果撇除 HTML5 原生的元件,那麼其實各家不管是 Vue, React 還是 Angular,大多都是圍繞的這個議題。

我們這裡開始會針對 Component 做一系列的操作,你可以想像成整個 Vue 生態中,大部分的時間是圍繞在 Component 上面的。所以我們這裡會慢慢的從基本使用上提起,如果對於 Vue 有初階認識的,或許可以跳過這裡(應該吧?)


Component a.k.a 元件

如果撇除掉應用程式進入點的 main.js 的話,那麼打從 App.vue 開始,就可以當作是一個元件來看。所以,還記得 上一篇 所提到的 Lifecycle 嗎?如果在不使用其他的套件的情況下,他就是 Vue 元件當中完整的生命週期。

那麼,元件的使用流程粗略概述是這樣,

  1. 將你的元件 import 到檔案中,也就是 import 你的 .vue 檔案。
  2. components 要把 import 進來的元件加上去。
  3. 然後在 <template> 區塊中,使用你的元件。

簡單來說,他就是將你的 某部分的 DOM 拆出來,放到另一個地方,並且給他起個名字,然後他就可以當作是被拿來使用的元件之一。

實際上,在 Vue 裡面,對於元件的規則有以下幾個建議(或者你要說是實作面的建議也行),

  1. 預設來說元件都放在 components 資料夾 底下,可以的話自己決定要放哪裡比較好(分門別類)。
  2. 元件命名方式沒有固定,但建議使用 駝峰式命名 來增加識別性。
  3. 元件參數 components 可以使用 key-value 的方式,用以改變元件在 <template> 內的命名方式,除非必要,不然大可不需要改。
  4. <template> 裡面,使用元件需要輸入正確名稱,而,當你使用駝峰式命名時,你的標籤除了 <HelloWorld> 外,也可寫為 <hello-world> 這個方式。
  5. 該元件的 props 若有設定,就會對應到該元件的屬性(attributes),除了 Vue 本身保留關鍵字以外(例如 ref 或是 v-if 爾等)。
  6. 元件裡面的生命週期,跟使用這個元件的父元件,基本上是分開的。以上述例子來說,created 的順序是 App > Component,而 mounted 順序則是 Component > App。
  7. 不要太相信誰先 mounted 或是 created 這件事情,特別是當你使用了 Vue-Router 之後,會更複雜。

元件基本結構

簡單來說,他就是一個 .vue 檔案(結案)。

https://vuejs.org/v2/guide/components.html

整個元件包含了三個部分,

  • <template>
  • <script>
  • <style>

這三個部分組合了這個元件需要用到的部分。除了你可以直接在區塊內撰寫所需要的樣版、程式碼與樣式外,你也可以使用 src 這個屬性將檔案從外部引入。

<template src="./template.html"></template>
<script src="./script.js"></script>
<style src="./style.css"></style>

但是,這樣的使用方式請留意一點。如果你有兩個元件,都使用這種方式的話,那麼,Vue 在封裝過程中,會把他當成 同一個 元件來使用。而,為了避免這種情況發生,請你把會共用的 <script> 的部分,利用 mixins 的方式來載入。關於 mixins 的部分,後續會繼續提到。


Template 樣版

顧名思義,他就是可以讓你寫 HTML 的地方。如果你有安裝第三方樣版語言工具的話,諸如 pug,那麼,你就可以在 Template 上面加上 lang 來指定你所想要使用的樣版語言。

<template lang="pug">
section.container
  .row
</template>

關於樣版這件事,Vue 元件有個規定,就是 根節點只能有一個,換句話說,底下這樣的寫法是會出錯誤的,

<template>
  <section class="container">
  </section>

  <!-- 不可以有兩個以上的根節點 -->
  <section classs="container">
  </section>
</template>

而樣版裡面呢,有不少屬性可以使用,

  • v-bind 或是簡寫為冒號 : 用以設定對應屬性的 變數 值。
    • .sync 修飾子,用於做綁定屬性值同步時適用。
  • v-text 用以綁定 DOM 的內容,且會顯示為純文字。
    • 另外一種寫法是使用雙大刮號 {{ }} 來填充你想要的文字。
  • v-html 用以綁定 DOM 的內容,且會將 HTML 渲染出來。
  • v-model 用以指定 表單元件 的數值,有三個專用修飾子,條列如下:
    • .lazy 延遲表單元件內容更新。
    • .number 限制表單元件內容僅接受數字。
    • .trim 清除表單元件輸入內容前後的空白字元。
  • v-bind:is 或簡寫為 :is 物件魔術方法,動態套用元件。
  • v-on 或是簡寫為 @ 用以綁定 事件
  • ref 若該節點是元件的話,將會在父層元件 $refs 中作為連結的參考元件。
  • 事件專用的修飾子,條列如下:
    • .stop
    • .prevent 不可與 .passive 共用。
    • .capture
    • .self
    • .once
    • .passive 不可與 .prevent 共用。
  • 按鍵專用的修飾子,條列如下:
    • .enter
    • .tab
    • .delete 會同時抓取 Delete 與 Backspace 兩個按鈕。
    • .esc 這個按鈕捕捉狀態 IE9 以下不支援。
    • .space
    • .up
    • .down
    • .left
    • .right
    • .keyCode 直接接上按鍵的號碼,例如 @keyup.keyCode.13
    • 除了上述的修飾子以外,也可透過自定義 Key Code 來定義自己的修飾子。
  • 系統組合鍵修飾子,條列如下:
    • .ctrl
    • .alt
    • .shift
    • .meta
    • .extra 修飾子,可以設定 僅按下 上述按鈕的動作才觸發。
  • 滑鼠按鍵修飾子,條列如下:
    • .left
    • .right
    • .middle

針對 :is 的部分,我們後面的章節會繼續著墨於這一塊,這裡暫且先不提及這個部分。


除了屬性的部分,元件還提供了一個插槽( slot)的方法,意思是說,當你的元件在使用時,你可以定義一部份的區塊,可以提供給外部做替換。舉例來說,如果你有一個按鈕元件,

<template>
<button type="button"><slot></slot></button>
</template>

那麼,當我外部使用這個元件時,

<template>
  <section>
    <MyButton>我是按鈕</MyButton>
  </section>
</template>

那麼 我是按鈕 這四個字,就會插入元件的 <slot></slot> 的區塊。關於 Slot 的部分我們後續會再提到,這裡先提個初步的概念。

接著是有關於樣版渲染以及邏輯控制的部分,有以下幾個方法可以使用:

  • v-for 使用迴圈來顯示一個元件。
  • v-if, v-else-if, v-else 邏輯控制。
  • v-show 設定一個元件顯示與否(會使用 display 的 CSS 樣式設定)。

上述方法中,官方 不建議v-forv-if 混用,除非你知道你在做什麼。另外,v-showv-else 並不支援 <template> 這樣的結構寫法,也請盡量避開。

v-for 有個特殊屬性,叫做 :key,主要的目的是為了避免迴圈物件被不正確的重用,所以,當你有一組清單需要使用迴圈顯示時,請務必確保指定了唯一的 :key,以避免渲染出來的元件不正確的情況。

另外,v-else 一定得接在 v-if 或是 v-else-if 後面,否則不會動作。再者 v-if 本身是 真的 把整個 DOM 銷毀或是重建,所以注意你必須留意元件上的監聽事件與其子元件。

v-forv-if 混用的模式,由於 v-for 的優先層級大於 v-if,所以實際上混用的運作邏輯會是這樣:

<template>
  <ul>
    <li v-for="link in links" v-if="link.active === true" :key="link.id">
      <a :href="link" v-text="link"></a>
    </li>
  </ul>
</template>
  1. 會優先執行 v-for 的動作。
  2. 當迴圈進行時,會將每一個迴圈的數值作 v-if 的判斷。
  3. 最後再決定是否要顯示這個結果。

所以,當你 以為 是不想要顯示 v-for 的結果,那麼可能會讓你失望。但是,當你需要的是 有條件的 列出迴圈結果的話,那麼這個方式是可以的。

官方之所以 不推薦 這麼做的原因是,當你兩個放在一起使用時,由於迴圈的關係,所以每執行一次迴圈,就會連帶執行一次 v-if 的判斷,並不是不好,而是你有更好的作法。


Style 樣式設定

關於樣式的部分相對單純,除了跟樣版一樣,可以使用第三方預處理器外,其內容就跟你平常在寫 CSS 一樣,沒有什麼太大的差異。

<style lang="scss">
.container {
  .row {
    /* 透過 lang 指定,我們就可以寫 SCSS 了 */
  }
}
</style>

另外,樣式設定上有一個特別的屬性,叫做 scoped。意思是,他會將你所撰寫的樣式表, 套用在這個元件內,與其子元件。實作的方式就是在你的 DOM 上面加上 data-v-* 的屬性,然後你的 CSS 樣式在輸出時,就會被套用一組屬性選擇器。

雖然說 scoped 可以讓你的樣式不會去干擾到其他的元件,但,對於某些第三方元件來說,有時候 scoped 卻是一個問題。亦即,你若想要調整某元件的樣式,但又被 scoped 限制住的時候怎麼辦呢?

這時 Vue Loader 提供了一組深度作用域的選擇器,你可以使用 >>> 或是 /deep/ 來去影響其子元件或是其他的樣式。舉例來說,

<style>
.container {
  .row >>> h1 {
    font-size: 2rem;
  }
}
</style>

這樣的寫法,所渲染出來的 CSS 樣式,就會被寫成這樣,

.container .row[data-v-469af010] .h1 { font-size: 2rem; }

這樣就能有效的 穿透 目前的元件,用以修改或是調整其他元件的樣式了。


Script 程式區塊

這個區塊就是整個元件的核心部分,依據 ES6 的方法,他必須要包含一組 export 區塊,

<script>
export default {
  /* 元件主要的功能寫在這裡面 */
}
</script>

如果你不是使用 Webpack 爾等工具,或是直接使用 Vue.component 來建立元件的話,使用方式就略有不同,

Vue.component(
  'my-component',
  {
    /* 元件主要功能寫在這裡 */
  }
)

而若是 TypeScript 的話,寫法也是略有差異。若你要使用 TypeScript 的話,必須要安裝一些額外的工具,可以參考官方的 說明。我們這裡就不提及 TypeScript 的部分,系列文章之後或許有機會我再來說說 TypeScript 吧。


在這個區塊裡面,有相當多的事情可以做,基本的元件屬性概略有以下這些項目,這裡尚且不列入其他非核心套件的部分,僅列出最原始元件本身支援的東西:

  • name 這個元件實體的名稱。
  • components 載入其他的元件到這個元件實體內。
  • mixins 載入混合模式元件。
  • props 讀取外部傳入的屬性。
  • data () 設定預設值。
  • inheritAttrs 設定該元件是否繼承相關屬性,僅接受布林值。
  • directives 元件自身的自定義指令。
  • filters 元件自身定義的過濾器。
  • computed 設定計算屬性。
  • methods 設定元件內部方法。
  • watch 監聽工具。
  • 與生命週期有關的函數與呼叫順序:
    • beforeCreate () 元件建立之前。
    • created () 元件建立完成。
    • beforeMount () 元件被放入 DOM 結構樹之前。
    • mounted () 元件放入 DOM 結構樹之後。
    • beforeUpdate () 元件實體更新之前。
    • updated () 元件實體更新之後。
    • beforeDestroy () 元件被銷毀(從 DOM 結構樹移除)之前。
    • destroyed () 元件被銷毀(從 DOM 結構樹移除)之後。

以上是一個元件可以做的事情。我們會逐步的來檢視這些屬性與動作。


首先是關於 components,當你有使用外部元件時,你必須要寫在這裡面,這樣你的 <template> 才能使用他,不然在開發模式下,會被 Vue 警告。

接著是 props,他是用於定義這個元件可以接受哪些屬性的傳入,而這些 被定義 過後的屬性,可以在元件實體當中,使用 this 來取得該數值。props 的定義方式如下,

export default {
  name: 'MyComponent',
  props: {
    hello: {
      type: Number,
      required: true,
      default: 30
    },
    world: {
      type: Object,
      default () {
        return {}
      }
    },
    kitty: {
      validator (value) {
        return /cat/gi.test(value)
      }
    },
    dog: String
  }
}

一個 props 屬性,可以包含:

  • type 這個變數的型別,你也可以不指定,就照單全收。
  • required 規定這個屬性是不是必要存在,僅接受布林值。
  • default 設定該屬性值的預設值,必須注意一點,如果是物件,必須要用函數的方法來返回預設值。
  • validator 自定義的型別判斷。
  • 上述都不做,直接指定一個型別,例如 dog: String

所以說,一個元件要使用外部屬性時,他的寫法就會是這樣,

<template>
  <section>
    <MyComponent
      hello="Hello"
      :world="{ world: true }"
      kitty="cat"
      dog="dog" />
  </section>
</template>

需要留意的地方是,props 的資料流全部都是 單向的,所以如果你想要讓父元件能夠同步更新到子元件,你就必須使用 .sync 這個修飾子,讓子元件的屬性值能夠同步更新。

但是請務必留意,由於 props 傳入子元件後,就是子元件自己的事情,若同時使用了 .sync 之後,要留意更新這件事情是不是你想要的。

如果你有設定 inheritAttrs: true 的話,你的元件會取消所有的屬性繼承,例如 <input> 這樣的元件,如果你取消繼承後,你可以自己設定 value 之類的動作(拿來做壞事或是故意要婊人家還蠻好用的)。


接著來說 data () 的部分,這裡是元件所有需要 預設值 的地方,他必須要使用函數的方式返回一個物件,這些 預設值 是在你的 <template> 當中會使用到的部分,你必須要在這裡先定義他。

至於為什麼,請參考官方文件 Reactivity in Depth

必須要留意的一點,這個 data () 雖然是個函數,但是這裡面的 this 實際上是可以拿到元件實體的。原因在於 data () 本身在生命週期的位置,大概是在 created () 之後才發生的事情,所以在這邊可以使用 this 來做一些事情。

然後跟 data () 類似的部分,就是 computed 這個部分。之所以叫做 computed 就表示他是必須要被 處理 的變數,我們跟剛剛提到的 props 一起來做一個比較。

之所以會需要 computed,其實是為了簡化某些事情,舉例來說,你想要在樣版上面更新某個數值,然後他需要經過某些處理才能顯示,

<template>
  <section>
    <p>I'm {{ age }} years old.</p>
  </section>
</template>

<script>
export default {
  computed: {
    age () {
      return 18
    }
  }
}
</script>

那麼,這樣跟 data () 或是 props 有什麼差異嗎?

data () 定義的變數可被賦值,computedprops 不可以。

如果你想要對 computed 或是 props 賦值的話會有錯誤警告,舉例來說:

<template>
  <section>
    <p>I'm {{ age }} years old.</p>
  </section>
</template>

<script>
export default {
  props: {
    msg: String
  },
  computed: {
    age () {
      return 18
    }
  },
  mounted () {
    /* 這邊賦值的話會有錯誤警告 */
    this.age = 30
    this.msg = 'Hello World'
  }
}
</script>

你會得到以下警告:

那麼 computed 就只有這樣嗎?不是的,通常有的人會拿來跟 watch 來比較,到底他跟元件的觀察者方法的差異在哪裡?當你需要額外函示處理資料時,你使用 watch 的彈性會比較大。而 computed 本身其實也包含了一組 getter 與 setter,用以做一些更複雜的操作。

實際運作方式是這樣,

<script>
export default {
  computed: {
    age: {
      get () {
        return this.gender === 'male' ? 30 : 18
      },
      set () {
        if (this.gender === 'male') {
          this.title = 'Mr'
        } else {
          this.title = 'Miss'
        }
      }
    }
  },
  data () {
    return {
      gender: '',
      title: ''
    }
  }
}
</script>

至於 watch 本身就是函數然後回傳新的與舊的數值而已,很好理解,

<script>
export default {
  watch: {
    age (newAge, oldAge) {
    }
  }
}
</script>

至於元件實體內部的 this.$watch 我們後續有篇章會提到這件事情。


最後是 methods,這個屬性提供了元件實體內部的操作方法。這邊沒有什麼特別的地方,就是你必須要用到的函數方法都要放在這邊。

<script>
export default {
  methods: {
    hello () {
      this.msg = 'Hello World'
      return true
    }
  }
}
</script>

然後,關於 props, computed, data ()methods 這些設定,你就不能使用同樣的名稱來名命,不然會噴錯誤給你。


至於生命週期的部分,我們後續還會繼續討論,這邊只需要知道有那些方法即可。但是有一個地方必須要留意的,在元件實體內,以下這些生命週期的方法,並無法透過 this 來取得元件實體:

  • beforeCreate ()
  • destroyed ()

如果你是使用 Vue.component 來建立元件實體,那麼必須小心 this 這個關鍵字作用域的範圍陷阱。我們後續會繼續討論關於 this 作用域的事情。然後呢,我這邊尚不提及 directivesfilters 的部分,我們再下一個篇章會持續聊聊。

小結

以上就是元件基本的操作狀況,如果還有什麼不明白的地方,官方文件是你很好的朋友。

https://vuejs.org/v2/guide/components.html

ITHome 鐵人賽同步刊登 Component 基本入門 Day 2