Skip to content
lisongyu edited this page Jan 20, 2017 · 1 revision

###vue之组件

@(Vue)

####1. 什么是组件?

组件(Component)是 Vue.js 最强大的功能之一。组件可以扩展 HTML 元素,封装可重用的代码。在较高层面上,组件是自定义元素, Vue.js 的编译器为它添加特殊功能。在有些情况下,组件也可以是原生 HTML 元素的形式,以 is 特性扩展。


####2. 使用组件

#####2.1注册 我们可以通过以下方式创建一个 Vue 实例:

new Vue({
  el: '#demo',
  // 选项
})

要注册一个全局组件,你可以使用 Vue.component(tagName, options)。 例如:

Vue.component('my-component', {
  // 选项
})

对于自定义标签名,Vue.js 不强制要求遵循 W3C规则 (小写,并且包含一个短杠),尽管遵循这个规则比较好。

组件在注册之后,便可以在父实例的模块中以自定义元素 **<my-component></my-component >**的形式使用。要确保在初始化根实例 之前 注册了组件:

<div id="example">
  <my-component></my-component>
</div>
// 注册(**global.html**)
Vue.component('my-component', {
  template: '<div>A custom component!</div>'
})
// 创建根实例
new Vue({
  el: '#demo'
})

渲染为:

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

A custom component!

#####2.2局部注册(part.html) 不必在全局注册每个组件。通过使用组件实例选项注册,可以使组件仅在另一个实例/组件的作用域中可用:(也就是某一个组件中使用)

var Child = {
  template: '<div>A custom component!</div>'
}
new Vue({
  // ...
  components: {
    // <my-component> 将只在父模板可用
    'my-component': Child
  }
})

#####2.3DOM 模版解析说明 当使用 DOM 作为模版时(例如,将 el 选项挂载到一个已存在的元素上), 你会受到 HTML 的一些限制,因为 Vue 只有在浏览器解析和标准化 HTML 后才能获取模版内容。尤其像这些元素 <ul> , <ol>, <table> , <select> 限制了能被它包裹的元素, <option> 只能出现在其它元素内部。

在自定义组件中使用这些受限制的元素时会导致一些问题,例如:(temerror.html)

<table>
  <my-row>...</my-row>
</table>

自定义组件 被认为是无效的内容,因此在渲染的时候会导致错误。变通的方案是使用特殊的 is 属性:例如:(temIs.html)

<table>
  <tr is="my-row"></tr>
</table>

应当注意,如果您使用来自以下来源之一的字符串模板,这些限制将不适用:

  1. <script type="text/x-template">
  2. JavaScript内联模版字符串
  3. vue 组件(写在vue里面)

因此,尽量使用.vue组件的形式(后缀是is的进行测试)

#####2.4 data 必须是函数 使用组件时,大多数可以传入到 Vue 构造器中的选项可以在注册组件时使用,有一个例外: data 必须是函数。 实际上,如果你这么做:

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

那么 Vue 会在控制台发出警告(dataWarn.html),告诉你在组件中 data 必须是一个函数。最好理解这种规则的存在意义(data.html)。

#####2.5 构成组件 组件意味着协同工作,通常父子组件会是这样的关系:组件 A 在它的模版中使用了组件 B 。它们之间必然需要相互通信:父组件要给子组件传递数据,子组件需要将它内部发生的事情告知给父组件。然而,在一个良好定义的接口中尽可能将父子组件解耦是很重要的。这保证了每个组件可以在相对隔离的环境中书写和理解,也大幅提高了组件的可维护性和可重用性。 在 Vue.js 中,父子组件的关系可以总结为 props down, events up 。父组件通过 props 向下传递数据给子组件,子组件通过 events 给父组件发送消息。看看它们是怎么工作的。Alt text


####3. prop #####3.1 使用 Prop 传递数据 组件实例的作用域是孤立的。这意味着不能并且不应该在子组件的模板内直接引用父组件的数据。可以使用 props 把数据传给子组件。 prop 是父组件用来传递数据的一个自定义属性。子组件需要显式地用 props 选项声明 “prop”:

Vue.component('child', {
  // 声明 props
  props: ['message'],
  // 就像 data 一样,prop 可以用在模板内
  // 同样也可以在 vm 实例中像 “this.message” 这样使用
  template: '<span>{{ message }}</span>'
})

然后向它传入一个普通字符串:

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

结果:(参考事例 propBase.html)

hello!

#####3.2 camelCase vs. kebab-case HTML 特性不区分大小写。当使用非字符串模版时,prop的名字形式会从 camelCase 转为 kebab-case(短横线隔开):

Vue.component('child', {
  // camelCase in JavaScript
  props: ['myMessage'],
  template: '<span>{{ myMessage }}</span>'
})
<!-- kebab-case in HTML -->
<child my-message="hello!"></child>

再次说明,如果你使用字符串模版,不用在意这些限制。 字符串模版:就是用在组件选项里用template: "" 指定的模板,对应地还有一种是在单文件组件里用<template><template/>指定的模板。

#####3.3动态 Prop

类似于用 v-bind 绑定 HTML 特性到一个表达式,也可以用 v-bind 动态绑定 props 的值到父组件的数据中。每当父组件的数据变化时,该变化也会传导给子组件:

<div>
  <input v-model="parentMsg">
  <br>
  <child v-bind:my-message="parentMsg"></child>
</div>

使用 v-bind 的缩写语法通常更简单:

<child :my-message="parentMsg"></child>

#####3.4字面量语法 vs 动态语法 初学者常犯的一个错误是使用字面量语法传递数值:(propActive-string.html)

<!-- 传递了一个字符串"1" -->
<comp some-prop="1"></comp>

因为它是一个字面 prop ,它的值以字符串 "1" 而不是以实际的数字传下去。如果想传递一个实际的 JavaScript 数字,需要使用 v-bind ,从而让它的值被当作 JavaScript 表达式计算:(propActive-dynamic.html)

<!-- 传递实际的数字 -->
<comp v-bind:some-prop="1"></comp>

#####3.5单向数据流

prop 是单向绑定的:当父组件的属性变化时,将传导给子组件,但是不会反过来。这是为了防止子组件无意修改了父组件的状态——这会让应用的数据流难以理解。

另外,每次父组件更新时,子组件的所有 prop 都会更新为最新值。这意味着你不应该在子组件内部改变 prop 。如果你这么做了,Vue 会在控制台给出警告。

通常有两种改变 prop 的情况:

  1. prop 作为初始值传入,子组件之后只是将它的初始值作为本地数据的初始值使用;
  2. prop 作为需要被转变的原始值传入。

更确切的说这两种情况是:

  1. 定义一个局部 data 属性,并将 prop 的初始值作为局部数据的初始值。(propSingle.html)
props: ['initialCounter'],
data: function () {
  return { counter: this.initialCounter }
}
  1. 定义一个 computed 属性,此属性从 prop 的值计算得出。(propComputed.html)
props: ['size'],
computed: {
  normalizedSize: function () {
    return this.size.trim().toLowerCase()
  }
}

注意在 JavaScript 中对象和数组是引用类型,指向同一个内存空间,如果 prop 是一个对象或数组,在子组件内部改变它会影响父组件的状态。(propSingle-arr.html)

#####3.6 Prop 验证

组件可以为 props 指定验证要求。如果未指定验证要求,Vue 会发出警告。当组件给其他人使用时这很有用。 prop 是一个对象而不是字符串数组时,它包含验证要求:(参考:propVerify.html)

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

type 也可以是一个自定义构造器,使用 instanceof 检测。 当 prop 验证失败了,如果使用的是开发版本会抛出一条警告。


####4. 自定义事件 我们知道,父组件是使用 props 传递数据给子组件,但如果子组件要把数据传递回去,应该怎样做?那就是自定义事件! #####4.1 使用 v-on 绑定自定义事件 每个 Vue 实例都实现了事件接口(Events interface),即:

  • 使用 $on(eventName) 监听事件
  • 使用 $emit(eventName) 触发事件

Vue的事件系统分离自浏览器的EventTarget API。尽管它们的运行类似,但是$on 和 $emit 不是addEventListener 和 dispatchEvent 的别名。

另外,父组件可以在使用子组件的地方直接用 v-on 来监听子组件触发的事件。 下面是一个例子:(eventSelf-base.html)

<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="increment">{{ counter }}</button>',
  data: function () {
    return {
      counter: 0
    }
  },
  methods: {
    increment: function () {
      this.counter += 1
      this.$emit('increment')
    }
  },
})
new Vue({
  el: '#counter-event-example',
  data: {
    total: 0
  },
  methods: {
    incrementTotal: function () {
      this.total += 1
    }
  }
})

在本例中,子组件已经和它外部完全解耦了。它所做的只是触发一个父组件关心的内部事件。

#####4.2 给组件绑定原生事件 有时候,你可能想在某个组件的根元素上监听一个原生事件。可以使用 .native 修饰 v-on 。例如:(eventSelf-native.html)

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

#####4.3 使用自定义事件的表单输入组件 自定义事件也可以用来创建自定义的表单输入组件,使用 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 生效,它必须:

  • 接受一个 value 属性
  • 在有新的 value 时触发 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('.') + 3)
      // 如果值不统一,手动覆盖以保持一致
      if (formattedValue !== value) {
        this.$refs.input.value = formattedValue
      }
      // 通过 input 事件发出数值 这是做什么的呢?
      this.$emit('input', Number(formattedValue))
    }
  }
})

上面的实现方式太过理想化了。 比如,用户甚至可以输入多个小数点或句号 - 哦哦!因此我们需要一个更有意义的例子,下面是一个更加完善的货币过滤器:

这个接口不仅仅可以用来连接组件内部的表单输入,也很容易集成你自己创造的输入类型。想象一下:

<voice-recognizer v-model="question"></voice-recognizer>
<webcam-gesture-reader v-model="gesture"></webcam-gesture-reader>
<webcam-retinal-scanner v-model="retinalImage"></webcam-retinal-scanner>

#####4.4 非父子组件通信 有时候非父子关系的组件也需要通信。在简单的场景下,使用一个空的 Vue 实例作为中央事件总线:

var bus = new Vue()

// 触发组件 A 中的事件
bus.$emit('id-selected', 1)

// 在组件 B 创建的钩子中监听事件
bus.$on('id-selected', function (id) {
  // ...
})

在更多复杂的情况下,你应该考虑使用专门的 状态管理模式(vueX).


####5.使用 Slot 分发内容

在使用组件时,常常要像这样组合它们:

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

注意两点(slotproduct.html):

  1. <app> 组件不知道它的挂载点会有什么内容。挂载点的内容是由**<app>**的父组件决定的(自身里边有没有内容?不好确定)。
  2. <app> 组件很可能有它自己的模版(自己模板内容和嵌套的内容如何处理)。

为了让组件可以组合,我们需要一种方式来混合父组件的内容与子组件自己的模板。这个过程被称为 内容分发 (或 “transclusion” 如果你熟悉 Angular)。Vue.js 实现了一个内容分发 API ,参照了当前 Web 组件规范草案,使用特殊的 元素作为原始内容的插槽。 #####5.1编辑作用域 在深入内容分发 API 之前,我们先明确内容的编译作用域。假定模板为:

<child-component>
  {{ message }}
</child-component>

message 应该绑定到父组件的数据,还是绑定到子组件的数据?答案是父组件。组件作用域简单地说是: 父组件模板的内容在父组件作用域内编译;子组件模板的内容在子组件作用域内编译(参考 slotScope.html)。

一个常见错误是试图在父组件模板内将一个指令绑定到子组件的属性/方法:

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

假定 someChildProperty 是子组件的属性,上例不会如预期那样工作。父组件模板不应该知道子组件的状态。 如果要绑定子组件内的指令到一个组件的根节点,应当在它的模板内这么做:

Vue.component('child-component', {
  // 有效,因为是在正确的作用域内
  template: '<div v-show="someChildProperty">Child</div>',
  data: function () {
    return {
      someChildProperty: true
    }
  }
})

类似地,分发内容是在父组件作用域内编译。

#####5.2单个slot

除非子组件模板包含至少一个 <slot> 插口,否则父组件的内容将会被丢弃。当子组件模板只有一个没有属性的 slot 时,父组件整个内容片段将插入到 slot 所在的 DOM 位置,并替换掉 slot 标签本身。(slotSingle.html)

最初在 <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>

#####5.3具名 Slot

<slot> 元素可以用一个特殊的属性 name 来配置如何分发内容。多个 slot 可以有不同的名字。具名 slot 将匹配内容片段中有对应 slot 特性的元素。

仍然可以有一个匿名 slot ,它是默认 slot ,作为找不到匹配的内容片段的备用插槽。如果没有默认的 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 是非常有用的机制。(slotName.html)

#####5.4 作用域插槽 作用域插槽是一种特殊类型的插槽,用作使用一个(能够传递数据到)可重用模板替换已渲染元素。

在子组件中,只需将数据传递到插槽,就像你将 prop 传递给组件一样:

<div class="child">
  <slot text="hello from child"></slot>
</div>

在父级中,具有特殊属性 scope 的 <template> 元素,表示它是作用域插槽的模板。scope 的值对应一个临时变量名,此变量接收从子组件中传递的 prop 对象:

<div class="parent">
  <child>
    <template scope="props">
      <span>hello from parent</span>
      <span>{{ props.text }}</span>
    </template>
  </child>
</div>

如果我们渲染以上结果,得到的输出会是(参考slot.html):

<div class="parent">
  <div class="child">
    <span>hello from parent</span>
    <span>hello from child</span>
  </div>
</div>

作用域插槽更具代表性的用例是列表组件,允许组件自定义应该如何渲染列表每一项:

<my-awesome-list :items="items">
  <!-- 作用域插槽也可以在这里命名 -->
  <template slot="item" scope="props">
    <li class="my-fancy-item">{{ props.text }}</li>
  </template>
</my-awesome-list>

列表组件的模板:

<ul>
  <slot name="item"
    v-for="item in items"
    :text="item.text">
    <!-- 这里是备用内容 -->
  </slot>
</ul>

####6.动态组件 多个组件可以使用同一个挂载点,然后动态地在它们之间切换。使用保留的 <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>

####7. 杂项


#####7.1编写可复用组件 在编写组件时,记住是否要复用组件有好处。一次性组件跟其它组件紧密耦合没关系,但是可复用组件应当定义一个清晰的公开接口。

Vue 组件的 API 来自三部分 - props, events 和 slots :

  • Props 允许外部环境传递数据给组件
  • Events 允许组件触发外部环境的副作用
  • Slots 允许外部环境将额外的内容组合在组件中。

使用 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>

#####7.2子组件索引

尽管有 propsevents ,但是有时仍然需要在 JavaScript 中直接访问子组件。为此可以使用 ref 为子组件指定一个索引 ID 。例如:(参考 ref.html)

<div id="parent">
  <user-profile ref="profile"></user-profile>
</div>
var parent = new Vue({ el: '#parent' })
// 访问子组件
var child = parent.$refs.profile

refv-for 一起使用时, ref 是一个数组或对象,包含相应的子组件。

$refs 只在组件渲染完成后才填充,并且它是非响应式的。它仅仅作为一个直接访问子组件的应急方案——应当避免在模版或计算属性中使用 $refs

#####7.3异步组件 在大型应用程序中,我们可能需要将应用拆分为更小的模块,并且只在实际需要时才从服务器加载组件。为了让异步按需加载组件这件事变得简单,Vue.js 允许将组件定义为一个异步解析组件定义的工厂函数。Vue.js 只在组件实际需要渲染时触发工厂函数,并将缓存结果,用于将来再次渲染。例如:(async.html)

Vue.component('async-example', function (resolve, reject) {
  setTimeout(function () {
    // Pass the component definition to the resolve callback
    resolve({
      template: '<div>I am async!</div>'
    })
  }, 1000)
})

工厂函数接收 resolve 回调函数,在从服务器接收到组件定义时调用。也可以调用 reject(reason) 表明加载失败。这里的 setTimeout 只是为了用于演示;怎么获取组件完全取决于你。比较推荐的方式是配合 Webpack 代码分割功能来使用异步组件:

Vue.component('async-webpack-example', function (resolve) {
  // 这个特殊的 require 语法将指示 webpack 自动将构建的代码,拆分成通过 Ajax 请求加载的 bundle。
  require(['./my-async-component'], resolve)
})

可以在工厂函数中返回一个 Promise,所以使用 Webpack 2 + ES2015 语法后你可以这么做:

Vue.component(
  'async-webpack-example',
  () => System.import('./my-async-component')
)

#####7.4组件命名约定 当注册组件(或者 props)时,可以使用 kebab-case ,camelCase ,或 TitleCase 。Vue 不关心这个

// 在组件定义中
components: {
  // 使用 kebab-case 形式注册
  'kebab-cased-component': { /* ... */ },
  // register using camelCase
  'camelCasedComponent': { /* ... */ },
  // register using TitleCase
  'TitleCasedComponent': { /* ... */ }
}

在 HTML 模版中,请使用 kebab-case 形式:

<!-- 在HTML模版中始终使用 kebab-case -->
<kebab-cased-component></kebab-cased-component>
<camel-cased-component></camel-cased-component>
<title-cased-component></title-cased-component>

当使用字符串模式时,可以不受 HTML 的 case-insensitive 限制。这意味实际上在模版中,你可以使用 camelCase 、 TitleCase 或者 kebab-case 来引用:

<!-- 在字符串模版中可以用任何你喜欢的方式! -->
<my-component></my-component>
<myComponent></myComponent>
<MyComponent></MyComponent>

#####7.5 x-Templates 另一种定义模版的方式是在 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'
})

这在有很多模版或者小的应用中有用,否则应该避免使用,因为它将模版和组件的其他定义隔离了。 #####7.6用 v-once 的低开销静态组件

尽管在 Vue 中渲染 HTML 很快,不过当组件中包含大量静态内容时,可以考虑使用 v-once 将渲染结果缓存起来,就像这样:

Vue.component('terms-of-service', {
  template: '\
    <div v-once>\
      <h1>Terms of Service</h1>\
      ... a lot of static content ...\
    </div>\
  '
})
Clone this wiki locally