Logan 如果前面还有路,答应我,跑下去...

Vue.js入门-6-组件

2018-01-14
Logan
VUE

组件

什么是组件

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

所有的 Vue 组件同时也都是 Vue 的实例,所以可接受相同的选项对象 (除了一些根级特有的选项) 并提供相同的生命周期钩子。

使用组件

全局注册

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

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

组件在注册之后,便可以作为自定义元素 <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
   }
 })

DOM 模板解析注意事项

当使用 DOM 作为模板时 (例如,使用 el 选项来把 Vue 实例挂载到一个已有内容的元素上),你会受到 HTML 本身的一些限制,因为 Vue 只有在浏览器解析、规范化模板之后才能获取其内容。尤其要注意,像 <ul>、<ol>、<table>、

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

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

自定义组件 <my-row> 会被当作无效的内容,因此会导致错误的渲染结果。变通的方案是使用特殊的 is 特性:

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

组件实例中 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 都有它自己内部的状态了

组件组合

组件设计初衷就是要配合使用的,最常见的就是形成父子组件的关系,通过一个良好定义的接口来尽可能将父子组件解耦也是很重要的。这保证了每个组件的代码可以在相对隔离的环境中书写和理解,从而提高了其可维护性和复用性。

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

//结果:

hello!

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 动态语法

如果想要给组件传递一个数字,是不可以使用常量传递的。因为它传递的并不是实际的数字。

<div id="watch-example">
    <demo-component size="10"></demo-component>
</div>
<script type="text/javascript">
    Vue.component("demo-component", { 
        props: ["message", "size"],
        template: "<div><button v-on:click=add>add</button><span>{ { size } }</span></div>",
        methods:{ 
            add:function(){ 
                this.size += 1;  // 1011111111...
             }
         }
     });
    var vm = new Vue({ 
        el: "#watch-example"
     })
</script>

如果想传递一个真正的 JavaScript 数值,则需要使用 v-bind,从而让它的值被当作 JavaScript 表达式计算:

<!-- 传递真正的数值 -->
<comp v-bind:some-prop="1"></comp>

单项数据流

Prop 是单向绑定的:当父组件的属性变化时,将传导给子组件,但是反过来不会。

每次父组件更新时,子组件的所有 prop 都会更新为最新值。这意味着你不应该在子组件内部改变 prop。

  • Prop 作为初始值传入后,子组件想把它当作局部数据来用。正确的应对方式是:定义一个局部变量,并用 prop 的值初始化它:
props: ['initialCounter'],
data: function () { 
  return {  counter: this.initialCounter  }
 }
  • Prop 作为原始数据传入,由子组件处理成其它数据输出。正确的应对方式是:定义一个计算属性,处理 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
       }
     }
   }
 })

非 Prop 特性

非 prop 特性,就是指它可以直接传入组件,而不需要定义相应的 prop。

组件可以接收任意传入的特性,这些特性都会被添加到组件的根元素上。

<bs-date-input data-3d-date-picker="true"></bs-date-input>

添加属性 data-3d-date-picker="true" 之后,它会被自动添加到 bs-date-input 的根元素上。

替换/合并现有的特性

对于多数特性来说,传递给组件的值会覆盖组件本身设定的值。

classstyle 这两个特性的值都会做合并 (merge) 操作,让最终生成的值

自定义事件

父组件是使用props传递数据给子组件,如果子组件要把数据传回给父组件。就需要使用自定义事件

使用 v-on 绑定自定义事件

每个 Vue 实例都实现了事件接口,即:

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

另外,父组件可以在使用子组件的地方直接用 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 修饰符

.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

默认情况下,一个组件的 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>

非父子组件的通信

有时候,非父子关系的两个组件之间也需要通信。在简单的场景下,可以使用一个空的 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> 组件很可能有它自己的模板

为了让组件可以组合,我们需要一种方式来混合父组件的内容与子组件自己的模板。这个过程被称为内容分发。

Vue.js 实现了一个内容分发 API,使用特殊的 <slot> 元素作为原始内容的插槽。

编译作用域

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

上面例子中,message应该绑定到父组件的数据。

组件作用域简单地说是:

父组件模板的内容在父组件作用域内编译;子组件模板的内容在子组件作用域内编译。


如果要绑定子组件作用域内的指令到一个组件的根节点,你应当在子组件自己的模板里做:

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>

作用域插槽

作用域插槽是一种特殊类型的插槽,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>

动态组件

通过使用保留的 <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)
 })

下一篇 Vue.js小白入门

留言

目录