Vue.js 第二天: 元件

Karson_Kang發表於2018-01-29

元件

什麼是元件?

元件 (Component) 是 Vue.js 最強大的功能之一。元件可以擴充套件 HTML 元素,封裝可重用的程式碼。在較高層面上,元件是自定義元素,Vue.js 的編譯器為它新增特殊功能。在有些情況下,元件也可以表現為用 is 特性進行了擴充套件的原生 HTML 元素。

所有的 Vue 元件同時也都是 Vue 的例項,所以可接受相同的選項物件 (除了一些根級特有的選項) 並提供相同的生命週期鉤子。

使用元件

全域性註冊

我們已經知道,可以通過以下方式建立一個 Vue 例項:

new Vue({
  el: '#some-element',
  // 選項
})
複製程式碼

要註冊一個全域性元件,可以使用 Vue.component(tagName, options)。例如:

Vue.component('my-component', {
  // 選項
})
複製程式碼

請注意,對於自定義標籤的命名 Vue.js 不強制遵循 W3C 規則(小寫,並且包含一個短槓),儘管這被認為是最佳實踐。

元件在註冊之後,便可以作為自定義元素 <my-component></my-component> 在一個例項的模板中使用。注意確保在初始化根例項之前註冊元件:

<div id="example">
  <my-component></my-component>
</div>
複製程式碼
// 註冊
Vue.component('my-component', {
  template: '<div>A custom component!</div>'
})

// 建立根例項
new Vue({
  el: '#example'
})
複製程式碼

渲染為:

<div id="example">
  <div>A custom component!</div>
</div>
複製程式碼

區域性註冊

你不必把每個元件都註冊到全域性。你可以通過某個 Vue 例項/元件的例項選項 components 註冊僅在其作用域中可用的元件:

var Child = {
  template: '<div>A custom component!</div>'
}

new Vue({
  // ...
  components: {
    // <my-component> 將只在父元件模板中可用
    'my-component': Child
  }
})
複製程式碼

這種封裝也適用於其它可註冊的 Vue 功能,比如指令。

DOM 模板解析注意事項

當使用 DOM 作為模板時 (例如,使用 el 選項來把 Vue 例項掛載到一個已有內容的元素上),你會受到 HTML 本身的一些限制,因為 Vue 只有在瀏覽器解析、規範化模板之後才能獲取其內容。尤其要注意,像 <ul>、<ol>、<table>、<select> 這樣的元素裡允許包含的元素有限制,而另一些像

在自定義元件中使用這些受限制的元素時會導致一些問題,例如:

<table>
  <my-row>...</my-row>
</table>
複製程式碼

自定義元件 <my-row> 會被當作無效的內容,因此會導致錯誤的渲染結果。變通的方案是使用特殊的 is 特性:

<table>
  <tr is="my-row"></tr>
</table>
複製程式碼

應當注意,如果使用來自以下來源之一的字串模板,則沒有這些限制

  • <script type="text/x-template">
  • JavaScript 內聯模板字串
  • .vue 元件

因此,請儘可能使用字串模板。

data 必須是函式

構造 Vue 例項時傳入的各種選項大多數都可以在元件裡使用。只有一個例外:data 必須是函式。實際上,如果你這麼做:

Vue.component('my-component', {
  template: '<span>{{ message }}</span>',
  data: {
    message: 'hello'
  }
})
複製程式碼

那麼 Vue 會停止執行,並在控制檯發出警告,告訴你在元件例項中 data 必須是一個函式。但理解這種規則為何存在也是很有益處的,所以讓我們先作個弊:

<div id="example-2">
  <simple-counter></simple-counter>
  <simple-counter></simple-counter>
  <simple-counter></simple-counter>
</div>
複製程式碼
var data = { counter: 0 }

Vue.component('simple-counter', {
  template: '<button v-on:click="counter += 1">{{ counter }}</button>',
  // 技術上 data 的確是一個函式了,因此 Vue 不會警告,
  // 但是我們卻給每個元件例項返回了同一個物件的引用
  data: function () {
    return data
  }
})

new Vue({
  el: '#example-2'
})
複製程式碼

由於這三個元件例項共享了同一個 data 物件,因此遞增一個 counter 會影響所有元件!這就錯了。我們可以通過為每個元件返回全新的資料物件來修復這個問題:

data: function () {
  return {
    counter: 0
  }
}
複製程式碼

現在每個 counter 都有它自己內部的狀態了:

元件組合

元件設計初衷就是要配合使用的,最常見的就是形成父子元件的關係:元件 A 在它的模板中使用了元件 B。它們之間必然需要相互通訊:父元件可能要給子元件下發資料,子元件則可能要將它內部發生的事情告知父元件。然而,通過一個良好定義的介面來儘可能將父子元件解耦也是很重要的。這保證了每個元件的程式碼可以在相對隔離的環境中書寫和理解,從而提高了其可維護性和複用性。

在 Vue 中,父子元件的關係可以總結為 prop 向下傳遞,事件向上傳遞。父元件通過 prop 給子元件下發資料,子元件通過事件給父元件傳送訊息。看看它們是怎麼工作的。

官網配圖

Prop

使用 Prop 傳遞資料

元件例項的作用域是孤立的。這意味著不能 (也不應該) 在子元件的模板內直接引用父元件的資料。父元件的資料需要通過 prop 才能下發到子元件中。

子元件要顯式地用 props 選項宣告它預期的資料:

Vue.component('child', {
  // 宣告 props
  props: ['message'],
  // 就像 data 一樣,prop 也可以在模板中使用
  // 同樣也可以在 vm 例項中通過 this.message 來使用
  template: '<span>{{ message }}</span>'
})
複製程式碼

然後我們可以這樣向它傳入一個普通字串:

<child message="hello!"></child>
複製程式碼

camelCase vs. kebab-case

HTML 特性是不區分大小寫的。所以,當使用的不是字串模板時,camelCase (駝峰式命名) 的 prop 需要轉換為相對應的 kebab-case (短橫線分隔式命名):

Vue.component('child', {
  // 在 JavaScript 中使用 camelCase
  props: ['myMessage'],
  template: '<span>{{ myMessage }}</span>'
})
複製程式碼
<!-- 在 HTML 中使用 kebab-case -->
<child my-message="hello!"></child>
複製程式碼

如果你使用字串模板,則沒有這些限制。

動態 Prop

與繫結到任何普通的 HTML 特性相類似,我們可以用 v-bind 來動態地將 prop 繫結到父元件的資料。每當父元件的資料變化時,該變化也會傳導給子元件:

<div>
  <input v-model="parentMsg">
  <br>
  <child v-bind:my-message="parentMsg"></child>
</div>
複製程式碼

你也可以使用 v-bind 的縮寫語法:

<child :my-message="parentMsg"></child>
複製程式碼

如果你想把一個物件的所有屬性作為 prop 進行傳遞,可以使用不帶任何引數的 v-bind (即用 v-bind 而不是 v-bind:prop-name)。例如,已知一個 todo 物件:

todo: {
  text: 'Learn Vue',
  isComplete: false
}
複製程式碼

然後:

<todo-item v-bind="todo"></todo-item>
複製程式碼

將等價於:

<todo-item
  v-bind:text="todo.text"
  v-bind:is-complete="todo.isComplete"
></todo-item>
複製程式碼

字面量語法 vs 動態語法

初學者常犯的一個錯誤是使用字面量語法傳遞數值:

<!-- 傳遞了一個字串 "1" -->
<comp some-prop="1"></comp>
複製程式碼

因為它是一個字面量 prop,它的值是字串 "1" 而不是一個數值。如果想傳遞一個真正的 JavaScript 數值,則需要使用 v-bind,從而讓它的值被當作 JavaScript 表示式計算:

<!-- 傳遞真正的數值 -->
<comp v-bind:some-prop="1"></comp>
複製程式碼

單向資料流

Prop 是單向繫結的:當父元件的屬性變化時,將傳導給子元件,但是反過來不會。這是為了防止子元件無意間修改了父元件的狀態,來避免應用的資料流變得難以理解。

另外,每次父元件更新時,子元件的所有 prop 都會更新為最新值。這意味著你不應該在子元件內部改變 prop。如果你這麼做了,Vue 會在控制檯給出警告。

在兩種情況下,我們很容易忍不住想去修改 prop 中資料:

  • Prop 作為初始值傳入後,子元件想把它當作區域性資料來用;
  • Prop 作為原始資料傳入,由子元件處理成其它資料輸出。

對這兩種情況,正確的應對方式是:

  • 定義一個區域性變數,並用 prop 的值初始化它:
props: ['initialCounter'],
data: function () {
  return { counter: this.initialCounter }
}
複製程式碼
  • 定義一個計算屬性,處理 prop 的值並返回:
props: ['size'],
computed: {
  normalizedSize: function () {
    return this.size.trim().toLowerCase()
  }
}
複製程式碼

注意在 JavaScript 中物件和陣列是引用型別,指向同一個記憶體空間,如果 prop 是一個物件或陣列,在子元件內部改變它會影響父元件的狀態。

Prop 驗證

我們可以為元件的 prop 指定驗證規則。如果傳入的資料不符合要求,Vue 會發出警告。這對於開發給他人使用的元件非常有用。

要指定驗證規則,需要用物件的形式來定義 prop,而不能用字串陣列:

Vue.component('example', {
  props: {
    // 基礎型別檢測 (`null` 指允許任何型別)
    propA: Number,
    // 可能是多種型別
    propB: [String, Number],
    // 必傳且是字串
    propC: {
      type: String,
      required: true
    },
    // 數值且有預設值
    propD: {
      type: Number,
      default: 100
    },
    // 陣列/物件的預設值應當由一個工廠函式返回
    propE: {
      type: Object,
      default: function () {
        return { message: 'hello' }
      }
    },
    // 自定義驗證函式
    propF: {
      validator: function (value) {
        return value > 10
      }
    }
  }
})
複製程式碼

type 可以是下面原生構造器:

  • String
  • Number
  • Boolean
  • Function
  • Object
  • Array
  • Symbol

type 也可以是一個自定義構造器函式,使用 instanceof 檢測。

當 prop 驗證失敗,Vue 會丟擲警告 (如果使用的是開發版本)。注意 prop 會在元件例項建立之前進行校驗,所以在 defaultvalidator 函式裡,諸如 data、computed 或 methods 等例項屬性還無法使用。

非 Prop 特性

所謂非 prop 特性,就是指它可以直接傳入元件,而不需要定義相應的 prop。

儘管為元件定義明確的 prop 是推薦的傳參方式,元件的作者卻並不總能預見到元件被使用的場景。所以,元件可以接收任意傳入的特性,這些特性都會被新增到元件的根元素上。

例如,假設我們使用了第三方元件 bs-date-input,它包含一個 Bootstrap 外掛,該外掛需要在 input 上新增 data-3d-date-picker 這個特性。這時可以把特性直接新增到元件上 (不需要事先定義 prop):

<bs-date-input data-3d-date-picker="true"></bs-date-input>
複製程式碼

新增屬性 data-3d-date-picker="true" 之後,它會被自動新增到 bs-date-input 的根元素上。

替換/合併現有的特性

假設這是 bs-date-input 的模板:

<input type="date" class="form-control">
複製程式碼

為了給該日期選擇器外掛增加一個特殊的主題,我們可能需要增加一個特殊的 class,比如:

<bs-date-input
  data-3d-date-picker="true"
  class="date-picker-theme-dark"
></bs-date-input>
複製程式碼

在這個例子當中,我們定義了兩個不同的 class 值:

  • form-control,來自元件自身的模板
  • date-picker-theme-dark,來自父元件

對於多數特性來說,傳遞給元件的值會覆蓋元件本身設定的值。即例如傳遞 type="large" 將會覆蓋 type="date" 且有可能破壞該元件!所幸我們對待 classstyle 特性會更聰明一些,這兩個特性的值都會做合併 (merge) 操作,讓最終生成的值為:form-control date-picker-theme-dark

自定義事件

我們知道,父元件使用 prop 傳遞資料給子元件。但子元件怎麼跟父元件通訊呢?這個時候 Vue 的自定義事件系統就派得上用場了。

使用 v-on 繫結自定義事件

每個 Vue 例項都實現了事件介面,即:

  • 使用 $on(eventName)監聽事件
  • 使用 $emit(eventName)觸發事件

Vue 的事件系統與瀏覽器的 EventTarget API 有所不同。儘管它們的執行起來類似,但是 $on$emit 並不是addEventListenerdispatchEvent 的別名。

另外,父元件可以在使用子元件的地方直接用v-on 來監聽子元件觸發的事件。

不能用 $on 監聽子元件釋放的事件,而必須在模板裡直接用 v-on 繫結,參見下面的例子。

下面是一個例子:

<div id="counter-event-example">
  <p>{{ total }}</p>
  <button-counter v-on:increment="incrementTotal"></button-counter>
  <button-counter v-on:increment="incrementTotal"></button-counter>
</div>
複製程式碼
Vue.component('button-counter', {
  template: '<button v-on:click="incrementCounter">{{ counter }}</button>',
  data: function () {
    return {
      counter: 0
    }
  },
  methods: {
    incrementCounter: function () {
      this.counter += 1
      this.$emit('increment')
    }
  },
})

new Vue({
  el: '#counter-event-example',
  data: {
    total: 0
  },
  methods: {
    incrementTotal: function () {
      this.total += 1
    }
  }
})
複製程式碼

在本例中,子元件已經和它外部完全解耦了。它所做的只是報告自己的內部事件,因為父元件可能會關心這些事件。請注意這一點很重要。

給元件繫結原生事件

有時候,你可能想在某個元件的根元素上監聽一個原生事件。可以使用 v-on 的修飾符 .native。例如:

<my-component v-on:click.native="doTheThing"></my-component>
複製程式碼

.sync 修飾符

2.3.0+

在一些情況下,我們可能會需要對一個 prop 進行“雙向繫結”。事實上,這正是 Vue 1.x 中的 .sync 修飾符所提供的功能。當一個子元件改變了一個帶 .sync 的 prop 的值時,這個變化也會同步到父元件中所繫結的值。這很方便,但也會導致問題,因為它破壞了單向資料流。由於子元件改變 prop 的程式碼和普通的狀態改動程式碼毫無區別,當光看子元件的程式碼時,你完全不知道它何時悄悄地改變了父元件的狀態。這在 debug 複雜結構的應用時會帶來很高的維護成本。

上面所說的正是我們在 2.0 中移除 .sync 的理由。但是在 2.0 釋出之後的實際應用中,我們發現 .sync 還是有其適用之處,比如在開發可複用的元件庫時。我們需要做的只是讓子元件改變父元件狀態的程式碼更容易被區分

從 2.3.0 起我們重新引入了 .sync 修飾符,但是這次它只是作為一個編譯時的語法糖存在。它會被擴充套件為一個自動更新父元件屬性的 v-on 監聽器。

如下程式碼

<comp :foo.sync="bar"></comp>
複製程式碼

會被擴充套件為:

<comp :foo="bar" @update:foo="val => bar = val"></comp>
複製程式碼

當子元件需要更新 foo 的值時,它需要顯式地觸發一個更新事件:

this.$emit('update:foo', newValue)
複製程式碼

使用自定義事件的表單輸入元件

自定義事件可以用來建立自定義的表單輸入元件,使用 v-model 來進行資料雙向繫結。要牢記:

<input v-model="something">
複製程式碼

這不過是以下示例的語法糖:

<input
  v-bind:value="something"
  v-on:input="something = $event.target.value">
複製程式碼

所以在元件中使用時,它相當於下面的簡寫:

<custom-input
  v-bind:value="something"
  v-on:input="something = arguments[0]">
</custom-input>
複製程式碼

所以要讓元件的 v-model 生效,它應該 (從 2.2.0 起是可配置的):

  • 接受一個 value prop
  • 在有新的值時觸發 input 事件並將新值作為引數

我們來看一個非常簡單的貨幣輸入的自定義控制元件:

<currency-input v-model="price"></currency-input>
複製程式碼
Vue.component('currency-input', {
  template: '\
    <span>\
      $\
      <input\
        ref="input"\
        v-bind:value="value"\
        v-on:input="updateValue($event.target.value)"\
      >\
    </span>\
  ',
  props: ['value'],
  methods: {
    // 不是直接更新值,而是使用此方法來對輸入值進行格式化和位數限制
    updateValue: function (value) {
      var formattedValue = value
        // 刪除兩側的空格符
        .trim()
        // 保留 2 位小數
        .slice(
          0,
          value.indexOf('.') === -1
            ? value.length
            : value.indexOf('.') + 3
        )
      // 如果值尚不合規,則手動覆蓋為合規的值
      if (formattedValue !== value) {
        this.$refs.input.value = formattedValue
      }
      // 通過 input 事件帶出數值
      this.$emit('input', Number(formattedValue))
    }
  }
})
複製程式碼

當然,上面的例子還是比較初級的。比如,使用者輸入多個小數點或句號也是允許的,好惡心吧!

自定義元件的 v-model

2.2.0 新增

預設情況下,一個元件的 v-model 會使用 value propinput 事件。但是諸如單選框、核取方塊之類的輸入型別可能把 value 用作了別的目的。model 選項可以避免這樣的衝突:

Vue.component('my-checkbox', {
  model: {
    prop: 'checked',
    event: 'change'
  },
  props: {
    checked: Boolean,
    // 這樣就允許拿 `value` 這個 prop 做其它事了
    value: String
  },
  // ...
})
複製程式碼
<my-checkbox v-model="foo" value="some value"></my-checkbox>
複製程式碼

上述程式碼等價於:

<my-checkbox
  :checked="foo"
  @change="val => { foo = val }"
  value="some value">
</my-checkbox>
複製程式碼

注意你仍然需要顯式宣告 checked 這個 prop。

非父子元件的通訊

有時候,非父子關係的兩個元件之間也需要通訊。在簡單的場景下,可以使用一個空的 Vue 例項作為事件匯流排:

var bus = new Vue()

// 觸發元件 A 中的事件
bus.$emit('id-selected', 1)

// 在元件 B 建立的鉤子中監聽事件
bus.$on('id-selected', function (id) {
  // ...
})
複製程式碼

在複雜的情況下,我們應該考慮使用專門的狀態管理模式。

使用插槽分發內容

在使用元件時,我們常常要像這樣組合它們:

<app>
  <app-header></app-header>
  <app-footer></app-footer>
</app>
複製程式碼

注意兩點:

  • <app> 元件不知道它會收到什麼內容。這是由使用 <app> 的父元件決定的。
  • <app> 元件很可能有它自己的模板。

為了讓元件可以組合,我們需要一種方式來混合父元件的內容與子元件自己的模板。這個過程被稱為內容分發 (即 Angular 使用者熟知的“transclusion”)。Vue.js 實現了一個內容分發 API,參照了當前 Web Components 規範草案,使用特殊的 <slot> 元素作為原始內容的插槽。

編譯作用域

在深入內容分發 API 之前,我們先明確內容在哪個作用域裡編譯。假定模板為:

<child-component>
  {{ message }}
</child-component>
複製程式碼

message 應該繫結到父元件的資料,還是繫結到子元件的資料?答案是父元件。元件作用域簡單地說是:

父元件模板的內容在父元件作用域內編譯;子元件模板的內容在子元件作用域內編譯。

一個常見錯誤是試圖在父元件模板內將一個指令繫結到子元件的屬性/方法:

<!-- 無效 -->
<child-component v-show="someChildProperty"></child-component>
複製程式碼

假定 someChildProperty 是子元件的屬性,上例不會如預期那樣工作。父元件模板並不感知子元件的狀態。

如果要繫結子元件作用域內的指令到一個元件的根節點,你應當在子元件自己的模板裡做:

Vue.component('child-component', {
  // 有效,因為是在正確的作用域內
  template: '<div v-show="someChildProperty">Child</div>',
  data: function () {
    return {
      someChildProperty: true
    }
  }
})
複製程式碼

類似地,被分發的內容會在父作用域內編譯。

單個插槽

除非子元件模板包含至少一個 <slot> 插口,否則父元件的內容將會被丟棄。當子元件模板只有一個沒有屬性的插槽時,父元件傳入的整個內容片段將插入到插槽所在的 DOM 位置,並替換掉插槽標籤本身。

最初在 <slot> 標籤中的任何內容都被視為備用內容。備用內容在子元件的作用域內編譯,並且只有在宿主元素為空,且沒有要插入的內容時才顯示備用內容。

假定 my-component 元件有如下模板:

<div>
  <h2>我是子元件的標題</h2>
  <slot>
    只有在沒有要分發的內容時才會顯示。
  </slot>
</div>
複製程式碼

父元件模板:

<div>
  <h1>我是父元件的標題</h1>
  <my-component>
    <p>這是一些初始內容</p>
    <p>這是更多的初始內容</p>
  </my-component>
</div>
複製程式碼

渲染結果:

<div>
  <h1>我是父元件的標題</h1>
  <div>
    <h2>我是子元件的標題</h2>
    <p>這是一些初始內容</p>
    <p>這是更多的初始內容</p>
  </div>
</div>
複製程式碼

具名插槽

<slot> 元素可以用一個特殊的特性 name 來進一步配置如何分發內容。多個插槽可以有不同的名字。具名插槽將匹配內容片段中有對應 slot 特性的元素。

仍然可以有一個匿名插槽,它是預設插槽,作為找不到匹配的內容片段的備用插槽。如果沒有預設插槽,這些找不到匹配的內容片段將被拋棄。

例如,假定我們有一個 app-layout 元件,它的模板為:

<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>
複製程式碼

父元件模板:

<app-layout>
  <h1 slot="header">這裡可能是一個頁面標題</h1>

  <p>主要內容的一個段落。</p>
  <p>另一個主要段落。</p>

  <p slot="footer">這裡有一些聯絡資訊</p>
</app-layout>
複製程式碼

渲染結果為:

<div class="container">
  <header>
    <h1>這裡可能是一個頁面標題</h1>
  </header>
  <main>
    <p>主要內容的一個段落。</p>
    <p>另一個主要段落。</p>
  </main>
  <footer>
    <p>這裡有一些聯絡資訊</p>
  </footer>
</div>
複製程式碼

在設計組合使用的元件時,內容分發 API 是非常有用的機制。

作用域插槽

2.1.0 新增

作用域插槽是一種特殊型別的插槽,用作一個 (能被傳遞資料的) 可重用模板,來代替已經渲染好的元素。

在子元件中,只需將資料傳遞到插槽,就像你將 prop 傳遞給元件一樣:

<div class="child">
  <slot text="hello from child"></slot>
</div>
複製程式碼

在父級中,具有特殊特性 slot-scope<template> 元素必須存在,表示它是作用域插槽的模板。slot-scope 的值將被用作一個臨時變數名,此變數接收從子元件傳遞過來的 prop 物件:

<div class="parent">
  <child>
    <template slot-scope="props">
      <span>hello from parent</span>
      <span>{{ props.text }}</span>
    </template>
  </child>
</div>
複製程式碼

如果我們渲染上述模板,得到的輸出會是:

<div class="parent">
  <div class="child">
    <span>hello from parent</span>
    <span>hello from child</span>
  </div>
</div>
複製程式碼

2.5.0+slot-scope 能被用在任意元素或元件中而不再侷限於 <template>

作用域插槽更典型的用例是在列表元件中,允許使用者自定義如何渲染列表的每一項:

<my-awesome-list :items="items">
  <!-- 作用域插槽也可以是具名的 -->
  <li
    slot="item"
    slot-scope="props"
    class="my-fancy-item">
    {{ props.text }}
  </li>
</my-awesome-list>
複製程式碼

列表元件的模板:

<ul>
  <slot name="item"
    v-for="item in items"
    :text="item.text">
    <!-- 這裡寫入備用內容 -->
  </slot>
</ul>
複製程式碼

解構

slot-scope 的值實際上是一個可以出現在函式簽名引數位置的合法的 JavaScript 表示式。這意味著在受支援的環境 (單檔案元件或現代瀏覽器) 中,您還可以在表示式中使用 ES2015 解構:

<child>
  <span slot-scope="{ text }">{{ text }}</span>
</child>
複製程式碼

動態元件

通過使用保留的 <component> 元素,並對其 is 特性進行動態繫結,你可以在同一個掛載點動態切換多個元件:

var vm = new Vue({
  el: '#example',
  data: {
    currentView: 'home'
  },
  components: {
    home: { /* ... */ },
    posts: { /* ... */ },
    archive: { /* ... */ }
  }
})
複製程式碼
<component v-bind:is="currentView">
  <!-- 元件在 vm.currentview 變化時改變! -->
</component>
複製程式碼

也可以直接繫結到元件物件上:

var Home = {
  template: '<p>Welcome home!</p>'
}

var vm = new Vue({
  el: '#example',
  data: {
    currentView: Home
  }
})
複製程式碼
<component v-bind:is="currentView">
  <!-- 元件在 vm.currentview 變化時改變! -->
</component>
複製程式碼

也可以直接繫結到元件物件上:

var Home = {
  template: '<p>Welcome home!</p>'
}

var vm = new Vue({
  el: '#example',
  data: {
    currentView: Home
  }
})
複製程式碼

keep-alive

如果把切換出去的元件保留在記憶體中,可以保留它的狀態或避免重新渲染。為此可以新增一個 keep-alive 指令引數:

<keep-alive>
  <component :is="currentView">
    <!-- 非活動元件將被快取! -->
  </component>
</keep-alive>
複製程式碼

在 API 參考中檢視更多 <keep-alive> 的細節。

雜項

編寫可複用元件

在編寫元件時,最好考慮好以後是否要進行復用。一次性元件間有緊密的耦合沒關係,但是可複用元件應當定義一個清晰的公開介面,同時也不要對其使用的外層資料作出任何假設。

Vue 元件的 API 來自三部分——prop、事件和插槽:

  • Prop 允許外部環境傳遞資料給元件;

  • 事件 允許從元件內觸發外部環境的副作用;

  • 插槽 允許外部環境將額外的內容組合在元件中。

使用 v-bindv-on 的簡寫語法,模板的意圖會更清楚且簡潔:

<my-component
  :foo="baz"
  :bar="qux"
  @event-a="doThis"
  @event-b="doThat"
>
  <img slot="icon" src="...">
  <p slot="main-text">Hello!</p>
</my-component>
複製程式碼

子元件引用

儘管有 prop 和事件,但是有時仍然需要在 JavaScript 中直接訪問子元件。為此可以使用 ref 為子元件指定一個引用 ID。例如:

<div id="parent">
  <user-profile ref="profile"></user-profile>
</div>
複製程式碼
var parent = new Vue({ el: '#parent' })
// 訪問子元件例項
var child = parent.$refs.profile
複製程式碼

refv-for 一起使用時,獲取到的引用會是一個陣列,包含和迴圈資料來源對應的子元件。

$refs 只在元件渲染完成後才填充,並且它是非響應式的。它僅僅是一個直接操作子元件的應急方案——應當避免在模板或計算屬性中使用 $refs

非同步元件

在大型應用中,我們可能需要將應用拆分為多個小模組,按需從伺服器下載。為了進一步簡化,Vue.js 允許將元件定義為一個工廠函式,非同步地解析元件的定義。Vue.js 只在元件需要渲染時觸發工廠函式,並且把結果快取起來,用於後面的再次渲染。例如:

Vue.component('async-example', function (resolve, reject) {
  setTimeout(function () {
    // 將元件定義傳入 resolve 回撥函式
    resolve({
      template: '<div>I am async!</div>'
    })
  }, 1000)
})
複製程式碼

工廠函式接收一個 resolve 回撥,在收到從伺服器下載的元件定義時呼叫。也可以呼叫 reject(reason) 指示載入失敗。這裡使用 setTimeout 只是為了演示,實際上如何獲取元件完全由你決定。推薦配合 webpack 的程式碼分割功能 來使用:

Vue.component('async-webpack-example', function (resolve) {
  // 這個特殊的 require 語法告訴 webpack
  // 自動將編譯後的程式碼分割成不同的塊,
  // 這些塊將通過 Ajax 請求自動下載。
  require(['./my-async-component'], resolve)
})
複製程式碼

你可以在工廠函式中返回一個 Promise,所以當使用 webpack 2 + ES2015 的語法時可以這樣:

Vue.component(
  'async-webpack-example',
  // 該 `import` 函式返回一個 `Promise` 物件。
  () => import('./my-async-component')
)
複製程式碼

當使用區域性註冊時,也可以直接提供一個返回 Promise 的函式:

new Vue({
  // ...
  components: {
    'my-component': () => import('./my-async-component')
  }
})
複製程式碼

如果你是 Browserify 使用者,可能就無法使用非同步元件了,它的作者已經表明 Browserify 將“永遠不會支援非同步載入”。Browserify 社群發現了一些解決方法,可能會有助於已存在的複雜應用。對於其他場景,我們推薦使用 webpack,因為它對非同步載入進行了內建、全面的支援。

高階非同步元件

2.3.0 新增

自 2.3.0 起,非同步元件的工廠函式也可以返回一個如下的物件:

const AsyncComp = () => ({
  // 需要載入的元件。應當是一個 Promise
  component: import('./MyComp.vue'),
  // 載入中應當渲染的元件
  loading: LoadingComp,
  // 出錯時渲染的元件
  error: ErrorComp,
  // 渲染載入中元件前的等待時間。預設:200ms。
  delay: 200,
  // 最長等待時間。超出此時間則渲染錯誤元件。預設:Infinity
  timeout: 3000
})
複製程式碼

注意,當一個非同步元件被作為 vue-router 的路由元件使用時,這些高階選項都是無效的,因為在路由切換前就會提前載入所需要的非同步元件。另外,如果你要在路由元件中使用上述寫法,需要使用 vue-router 2.4.0 以上的版本。

元件命名約定

當註冊元件 (或者 prop) 時,可以使用 kebab-case (短橫線分隔命名)、camelCase (駝峰式命名) 或 PascalCase (單詞首字母大寫命名)。

// 在元件定義中
components: {
  // 使用 kebab-case 註冊
  'kebab-cased-component': { /* ... */ },
  // 使用 camelCase 註冊
  'camelCasedComponent': { /* ... */ },
  // 使用 PascalCase 註冊
  'PascalCasedComponent': { /* ... */ }
}
複製程式碼

在 HTML 模板中,請使用 kebab-case:

<!-- 在 HTML 模板中始終使用 kebab-case -->
<kebab-cased-component></kebab-cased-component>
<camel-cased-component></camel-cased-component>
<pascal-cased-component></pascal-cased-component>
複製程式碼

當使用字串模式時,可以不受 HTML 大小寫不敏感的限制。這意味實際上在模板中,你可以使用下面的方式來引用你的元件:

  • kebab-case
  • camelCase 或 kebab-case (如果元件已經被定義為 camelCase)
  • kebab-case、camelCase 或 PascalCase (如果元件已經被定義為 PascalCase)
components: {
  'kebab-cased-component': { /* ... */ },
  camelCasedComponent: { /* ... */ },
  PascalCasedComponent: { /* ... */ }
}
複製程式碼
<kebab-cased-component></kebab-cased-component>

<camel-cased-component></camel-cased-component>
<camelCasedComponent></camelCasedComponent>

<pascal-cased-component></pascal-cased-component>
<pascalCasedComponent></pascalCasedComponent>
<PascalCasedComponent></PascalCasedComponent>
複製程式碼

這意味著 PascalCase 是最通用的宣告約定而 kebab-case 是最通用的使用約定

如果元件未經 slot 元素傳入內容,你甚至可以在元件名後使用 / 使其自閉合:

<my-component/>
複製程式碼

當然,這隻在字串模板中有效。因為自閉的自定義元素是無效的 HTML,瀏覽器原生的解析器也無法識別它。

遞迴元件

元件在它的模板內可以遞迴地呼叫自己。不過,只有當它有 name 選項時才可以這麼做:

name: 'unique-name-of-my-component'
複製程式碼

當你利用 Vue.component 全域性註冊了一個元件,全域性的 ID 會被自動設定為元件的 name

Vue.component('unique-name-of-my-component', {
  // ...
})
複製程式碼

如果稍有不慎,遞迴元件可能導致死迴圈:

name: 'stack-overflow',
template: '<div><stack-overflow></stack-overflow></div>'
複製程式碼

上面元件會導致一個“max stack size exceeded”錯誤,所以要確保遞迴呼叫有終止條件 (比如遞迴呼叫時使用 v-if 並最終解析為 false)。

元件間的迴圈引用

假設你正在構建一個檔案目錄樹,像在 Finder 或資源管理器中。你可能有一個 tree-folder 元件:

<p>
  <span>{{ folder.name }}</span>
  <tree-folder-contents :children="folder.children"/>
</p>
複製程式碼

以及一個 tree-folder-contents 元件:

<ul>
  <li v-for="child in children">
    <tree-folder v-if="child.children" :folder="child"/>
    <span v-else>{{ child.name }}</span>
  </li>
</ul>
複製程式碼

當你仔細看時,會發現在渲染樹上這兩個元件同時為對方的父節點和子節點——這是矛盾的!當使用 Vue.component 將這兩個元件註冊為全域性元件的時候,框架會自動為你解決這個矛盾。如果你已經是這樣做的,就跳過下面這段吧。

然而,如果你使用諸如 webpack 或者 Browserify 之類的模組化管理工具來 require/import 元件的話,就會報錯了:

Failed to mount component: template or render function not defined.
複製程式碼

在我們的例子中,可以選擇讓 tree-folder 元件中來做這件事。我們知道引起矛盾的子元件是 tree-folder-contents,所以我們要等到 beforeCreate 生命週期鉤子中才去註冊它:

beforeCreate: function () {
  this.$options.components.TreeFolderContents = require('./tree-folder-contents.vue')
}
複製程式碼

問題解決了!

內聯模板

如果子元件有 inline-template 特性,元件將把它的內容當作它的模板,而不是把它當作分發內容。這讓模板編寫起來更靈活。

<my-component inline-template>
  <div>
    <p>這些將作為元件自身的模板。</p>
    <p>而非父元件透傳進來的內容。</p>
  </div>
</my-component>
複製程式碼

但是 inline-template 讓模板的作用域難以理解。使用 template 選項在元件內定義模板或者在 .vue 檔案中使用 template 元素才是最佳實踐。

X-Template

另一種定義模板的方式是在 JavaScript 標籤裡使用 text/x-template 型別,並且指定一個 id。例如:

<script type="text/x-template" id="hello-world-template">
  <p>Hello hello hello</p>
</script>
複製程式碼
Vue.component('hello-world', {
  template: '#hello-world-template'
})
複製程式碼

這在有很多大模板的演示應用或者特別小的應用中可能有用,其它場合應該避免使用,因為這將模板和元件的其它定義分離了。

對低開銷的靜態元件使用 v-once

儘管在 Vue 中渲染 HTML 很快,不過當元件中包含大量靜態內容時,可以考慮使用 v-once 將渲染結果快取起來,就像這樣:

Vue.component('terms-of-service', {
  template: '\
    <div v-once>\
      <h1>Terms of Service</h1>\
      ...很多靜態內容...\
    </div>\
  '
})
複製程式碼

相關文章