vue從入門到進階:組件Component詳解(六)

一.什麼是組件?

組件 (Component) 是 Vue.js 最強大的功能之一。組件能夠擴展 HTML 元素,封裝可重用的代碼。在較高層面上,組件是自定義元素,Vue.js 的編譯器爲它添加特殊功能。在有些狀況下,組件也能夠表現爲用 is 特性進行了擴展的原生 HTML 元素。html

全部的 Vue 組件同時也都是 Vue 的實例,因此可接受相同的選項對象 (除了一些根級特有的選項) 並提供相同的生命週期鉤子。

二.註冊組件

全局註冊

html代碼:vue

<div id="example">
  <my-component></my-component>
</div>

JS代碼:jquery

// 註冊
Vue.component('my-component', {
  template: '<div>A custom component!</div>'
})
var vm = new Vue({
  el: '#example',
  data: {
       
  } 
})

渲染結果爲:webpack

<div id="example">
  <div>A custom component!</div>
</div>

或者另一種註冊方式,經過 全局API:Vue.extend()
代碼以下:web

// 註冊
var MyComponent = Vue.extend({
  template: '<div>A custom component!</div>'
});

// 註冊
Vue.component('my-component', MyComponent);
var vm = new Vue({
  el: '#example',
  data: {
       
  }
})

Vue.extend()使用說明

下面說明下Vue.extend( options )的使用。
參數:{Object} options
用法:使用基礎 Vue 構造器,建立一個「子類」。參數是一個包含組件選項的對象
data 選項是特例,須要注意 - 在 Vue.extend() 中它必須是函數vue-router

<div id="mount-point"></div>
// 建立構造器
var Profile = Vue.extend({
  template: '<p>{{firstName}} {{lastName}} aka {{alias}}</p>',
  data: function () {
    return {
      firstName: 'Walter',
      lastName: 'White',
      alias: 'Heisenberg'
    }
  }
})
// 建立 Profile 實例,並掛載到一個元素上。
new Profile().$mount('#mount-point')

結果以下:chrome

<p>Walter White aka Heisenberg</p>

上面又用到了實例方法vm.$mount(),下面說明下它的使用方式。數組

vm.$mount( [elementOrSelector] )使用說明

參數:瀏覽器

  • {Element | string} [elementOrSelector]
  • {boolean} [hydrating]

返回值:vm - 實例自身
用法:
若是 Vue 實例在實例化時沒有收到 el 選項,則它處於「未掛載」狀態,沒有關聯的 DOM 元素。可使用 vm.$mount() 手動地掛載一個未掛載的實例。緩存

若是沒有提供 elementOrSelector 參數,模板將被渲染爲文檔以外的的元素,而且你必須使用原生 DOM API 把它插入文檔中。

這個方法返回實例自身,於是能夠鏈式調用其它實例方法。

var MyComponent = Vue.extend({
  template: '<div>Hello!</div>'
})

// 建立並掛載到 #app (會替換 #app)
new MyComponent().$mount('#app')

// 同上
new MyComponent({ el: '#app' })

// 或者,在文檔以外渲染而且隨後掛載
var component = new MyComponent().$mount()
document.getElementById('app').appendChild(component.$el)

局部註冊

你沒必要把每一個組件都註冊到全局。你能夠經過某個 Vue 實例/組件的實例選項 components 註冊僅在其做用域中可用的組件:

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

new Vue({
  // ...
  components: {
    // <my-component> 將只在父組件模板中可用
    'my-component': Child
  }
})

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

DOM 模板解析注意事項

<ul>、<ol>、<table>、<select> 這樣的元素裏容許包含的元素有限制,而另外一些像 <option> 這樣的元素只能出如今某些特定元素的內部。

例如:

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.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 都有它本身內部的狀態了,不會相互影響。

三.Prop

最多見的應用就是:組件 A 在它的模板中使用了組件 B。它們之間必然須要相互通訊:父組件可能要給子組件下發數據,子組件則可能要將它內部發生的事情告知父組件。

在 Vue 中,父子組件的關係能夠總結爲 prop 向下傳遞,事件向上傳遞。父組件經過 prop 給子組件下發數據,子組件經過事件給父組件發送消息。

使用 Prop 傳遞數據

實例1:

Vue.component('child', {
  // 聲明 props
  props: ['message'],
  // 就像 data 同樣,prop 也能夠在模板中使用
  // 一樣也能夠在 vm 實例中經過 this.message 來使用
  template: '<span>{{ message }}</span>'
})

<child message="hello!"></child>

結果:hello!

實例2:
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

<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 做爲原始數據傳入,由子組件處理成其它數據輸出。

對這兩種狀況,正確的應對方式是:
1.定義一個局部變量,並用 prop 的值初始化它:

props: ['initialCounter'],
data: function () {
  return { counter: this.initialCounter }
}

2.定義一個計算屬性,處理 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 函數裏,諸如 datacomputedmethods 等實例屬性還沒法使用。

其它實例:

Vue.component('modal', {
  template: '#modal-template',
  props: {
    show: {
      type: Boolean,
      required: true,
      twoWay: true    
    }
  }
});

twoWay Prop 的參數 移除。Props 如今只能單向傳遞。爲了對父組件產生反向影響,子組件須要顯式地傳遞一個事件而不是依賴於隱式地雙向綁定。因此上面的的最後一個實例只是貼出來代碼而已,最新版本已經移除了。

四.非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。

五.自定義事件

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

使用 v-on 綁定自定義事件

每一個 Vue 實例都實現了事件接口,即:

  • 使用 $on(eventName) 監聽事件
  • 使用 $emit(eventName) 觸發事件
Vue 的事件系統與瀏覽器的 EventTarget API 有所不一樣。儘管它們的運行起來相似,可是 $on $emit 並非 addEventListener dispatchEvent 的別名。

另外,父組件能夠在使用子組件的地方直接用 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
    }
  }
})

給組件綁定原生事件.native

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

<my-component v-on:click.native="doTheThing"></my-component>

.sync 修飾符(2.3.0+)

<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 事件並將新值做爲參數

例子1:

<div id="app">
      <custom-input v-model="something"></custom-input>
    <br/>
    {{something}}
</div>
// 註冊
Vue.component('custom-input', {
  props:['something'],
  template: '<input type="text"  v-bind:value="something" v-on:input="updateValue($event.target.value)"/>',
  methods:{
      updateValue:function(value){
           this.$emit('input', value)
      }
  }
})
var vm = new Vue({
  el: '#app',
  data: {
       something:''
  }
  
})

例子2:貨幣輸入的自定義控件

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

實例3:更加完善的貨幣過濾器

<div id="app">
  <currency-input 
    label="Price" 
    v-model="price"
  ></currency-input>
  <currency-input 
    label="Shipping" 
    v-model="shipping"
  ></currency-input>
  <currency-input 
    label="Handling" 
    v-model="handling"
  ></currency-input>
  <currency-input 
    label="Discount" 
    v-model="discount"
  ></currency-input>
  
  <p>Total: ${{ total }}</p>
</div>
Vue.component('currency-input', {
  template: '\
    <div>\
      <label v-if="label">{{ label }}</label>\
      $\
      <input\
        ref="input"\
        v-bind:value="value"\
        v-on:input="updateValue($event.target.value)"\
        v-on:focus="selectAll"\
        v-on:blur="formatValue"\
      >\
    </div>\
  ',
  props: {
    value: {
      type: Number,
      default: 0
    },
    label: {
      type: String,
      default: ''
    }
  },
  mounted: function () {
    this.formatValue()
  },
  methods: {
    updateValue: function (value) {
      var result = currencyValidator.parse(value, this.value)
      if (result.warning) {
        this.$refs.input.value = result.value
      }
      this.$emit('input', result.value)
    },
    formatValue: function () {
      this.$refs.input.value = currencyValidator.format(this.value)
    },
    selectAll: function (event) {
      // Workaround for Safari bug
      // http://stackoverflow.com/questions/1269722/selecting-text-on-focus-using-jquery-not-working-in-safari-and-chrome
      setTimeout(function () {
          event.target.select()
      }, 0)
    }
  }
})

new Vue({
  el: '#app',
  data: {
    price: 0,
    shipping: 0,
    handling: 0,
    discount: 0
  },
  computed: {
    total: function () {
      return ((
        this.price * 100 + 
        this.shipping * 100 + 
        this.handling * 100 - 
        this.discount * 100
      ) / 100).toFixed(2)
    }
  }
})

自定義組件的 v-model(2.2.0 新增)

默認狀況下,一個組件的 v-model 會使用 value prop 和 input 事件。可是諸如單選框、複選框之類的輸入類型可能把 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

完整的代碼:
html:

<div id="app">
      <my-checkbox v-model="foo" value="some value"></my-checkbox>
    {{foo}}
</div>

JS代碼:

Vue.component('my-checkbox', {
  model: {
    prop: 'checked',
    event: 'change'
  },
  props: {
    checked: Boolean,
    // 這樣就容許拿 `value` 這個 prop 作其它事了
    value: String
  },
  template:'<input type="checkbox" @change="changefun(ischecked)"/>',
  data:function(){
      return {
          ischecked:this.checked
      }
  },
  methods:{
      changefun(state){
          this.ischecked = !state;
          this.$emit('change', this.ischecked);
      }
  }
})
var vm = new Vue({
  el: '#app',
  data: {
       foo:false
  }
  
})

非父子組件的通訊

有時候,非父子關係的兩個組件之間也須要通訊。在簡單的場景下,可使用一個空的 Vue 實例做爲事件總線:

var bus = new Vue()
// 觸發組件 A 中的事件
bus.$emit('id-selected', 1)
// 在組件 B 建立的鉤子中監聽事件
bus.$on('id-selected', function (id) {
  // ...
})

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

來看一個完整的例子:
html代碼:

<div id="app">
     <comp-a v-on:id-selected="getdate"></comp-a>
     <comp-b></comp-b>
</div>

JS代碼:

var bus = new Vue();

Vue.component('comp-a', {
  template:'<button class="compa" @click="comfuna">組件A</button>',
  data:function(){
      return {
          
      }
  },
  methods:{
      comfuna(){
          bus.$emit('id-selected', 1);
          this.$emit('id-selected', 1);
      }
  }
})
Vue.component('comp-b', {
  template:'<div class="compb">組件B</div>',
  data:function(){
      return {
          
      }
  },
  mounted(){
      // 在組件 B 建立的鉤子中監聽事件
    bus.$on('id-selected', function (id) {
         console.log('在B組件中獲得的值:'+id);
    })
  }
 
})
var vm = new Vue({
  el: '#app',
  data: {},
  methods:{
      getdate(value){
          console.log('獲得當前的值:'+value);
      }
  }
  
})

六.使用插槽slot分發內容

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

<app>
  <app-header></app-header>
  <app-footer></app-footer>
</app>

注意兩點:

  • <app> 組件不知道它會收到什麼內容。這是由使用 <app> 的父組件決定的。
  • <app> 組件極可能有它本身的模板。

爲了讓組件能夠組合,咱們須要一種方式來混合父組件的內容與子組件本身的模板。使用特殊的 <slot> 元素做爲原始內容的插槽。

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

<!-- 無效 -->
<child-component v-show="someChildProperty"></child-component>

正確作法:

Vue.component('child-component', {
  // 有效,由於是在正確的做用域內
  template: '<div v-show="someChildProperty">Child</div>',
  data: function () {
    return {
      someChildProperty: true
    }
  }
})

單個插槽

假定 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>

做用域插槽(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
  }
})

keep-alive

若是把切換出去的組件保留在內存中,能夠保留它的狀態或避免從新渲染。爲此能夠添加一個 keep-alive 指令參數:

<keep-alive>
  <component :is="currentView">
    <!-- 非活動組件將被緩存! -->
  </component>
</keep-alive>

八.雜項

編寫可複用組件

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

  • Prop 容許外部環境傳遞數據給組件;
  • 事件容許從組件內觸發外部環境的反作用;
  • 插槽容許外部環境將額外的內容組合在組件中。

使用 v-bind 和 v-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

ref v-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')
  }
})

高級異步組件(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 是最通用的使用約定。

遞歸組件

組件在它的模板內能夠遞歸地調用本身。不過,只有當它有 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')
}

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>\
  '
})
相關文章
相關標籤/搜索