Vue.js(四):组件

组件

Vue.js 引入的组件,让分解单一 HTML 到独立组件成为可能。组件可以自定义元素形式使用,或者使用原生元素但是以 is 特性 做扩展。

注册和引用

使用组件之前,首先需要注册。可以注册为全局的或者是局部的。全局注册可以使用:

Vue.component(tag, options)

注册一个组件。tag 为自定义元素的名字,options 同为创建组件的选项。注册完成后,即可以 <tag> 形式引用此组件。

<div id="app">
  <tag></tag>
</div>
<script>
  Vue.component('tag', {
    template: `<div>one component rule all other</div>`
  })
  new Vue({
    el: "#app"
  });
</script>

也可以局部注册,这样注册的组件,仅仅限于执行注册的 Vue 实例内:

<div id="app">
  <tag></tag>
</div>
<script>
   var tag = {
      template: `<div>one component rule all other</div>`
    }
    new Vue({
      el: "#app",
      components: {tag}
  });
</script>

<tag> 是 HTML 本身并不具备的标签,现在由 Vue 的组件技术引入,因此被称为是自定义标签。这些自定义标签的背后实现常常是标签、脚本、css 的集合体。

动态挂接

多个组件可以使用同一个挂载点,然后动态地在它们之间切换。元素 <component> 可以用于此场景,修改属性 is 即可达成动态切换的效果:

<component v-bind:is="current"></component>

假设我们有三个组件 home、posts、archives,我们可以设置一个定时器,每隔 2 秒修改一次 current,把三个组件的逐个切入到当前挂接点:

<div id="app">
  <component v-bind:is="current">
  </component>
</div>
<script>

var app = new Vue({
  el: '#app',
  data: {
    current: 'archive',
    i :0,
    b : ['home','posts','archive']
  },
  components: {
    home: { template:'<h1>home</h1>'},
    posts: { template:'<h1>posts</h1>' },
    archive: {template:'<h1>archive</h1>'}
  },
  methods:{
    a(){
      this.i = this.i % 3;
      this.current = this.b[this.i];
      this.i++;
      setTimeout(this.a,2000);
    }
  },
  mounted(){
    setTimeout(this.a,2000);
  }
})
</script>

引用组件

一个父组件内常常有多个子组件,有时候为了个别处理,需要在父组件代码内引用子组件实例。Vue.js 可以通过指令 v-ref 设置组件的标识符,并在代码内通过 $refs + 标识符 来引用特定组件。

🌰:假设有三个按钮。其中前两个按钮被点击时,每次对自己的计数器累加1;另外一个按钮可以取得前两个按钮的计数器值,并加总后设置 {{total}} 的值。此时在第三个按钮的事件代码中,就需要引用前两个按钮的实例。

<div id="app">
  {{ total }}
  <count ref="b1"></count>
  <count ref="b2"></count>
  <button v-on:click="value">value</button>
</div>
<script>
 Vue.component('count', {
  template: '<button v-on:click="inc">{{ count }}</button>',
  data: function () {
    return {count: 0}
  },
  methods: {
    inc: function () {
      this.count+= 1
    }
  },
})
new Vue({
  el: '#app',
  data: {total:0},
  methods: {
    value: function () {
      this.total = this.$refs.b1.count+this.$refs.b2.count
    }
  }
})
</script>

标签 button 使用 ref 设置两个按钮分为为 b1、b2,随后在父组件代码内通过 $refs 引用它们。


组件协作

按照组件分解的愿景,一个大型的 HTML 会按照语义划分为多个组件,那么组件之间必然存在协作的问题。Vue.js 提供的协作方式有属性传递、事件传递和内容分发。

🥭 使用属性

此方法用于父组件传递数据给子组件。每个组件的作用域都是和其他组件隔离的,因此,子组件不应该直接访问父组件的数据,而是通过属性传递数据过来。

🌰:传递一个字符串到子组件:

<div id="app">
  <child message="hello"></child>
</div>
<script>
  Vue.component('child', {
    props: ['message'],
    template: '<span>{{ message }}</span>'
  })
  new Vue({el:'#app'})
</script>

父组件为挂接在 #app 上的 Vue 实例,子组件为 child。child 使用 props 声明一个名为 message 的属性,此属性把父组件内的字符串 hello 传递数据到组件内。

如果不是传递一个静态的字符串,而是传递 JavaScript 表达式,那么可以使用指令 v-bind:

<div id="app">
  <child v-bind:message="hello+',world'"></child>
</div>
<script>
  Vue.component('child', {
    props: ['message'],
    template: '<span>{{ message }}</span>'
  })
    new Vue({
    el:'#app',
    data:{hello:'hi'}
  })
</script>

本案例把父组件内的 hello 成员传递给子组件。出现在属性内的 hello 不再指示字面上的字符串,而是指向一个表达式,因此传递进来的是表达式的求值结果。

属性验证

当通过属性传递表达式时,有些时候类型是特定的,Vue 提供了属性的验证,包括类型验证,范围验证等。

🌰:传递年龄进来的话,要求应该是整数。

<div id="app">
  <child v-bind:age="age"></child>
</div>
<script>
  Vue.component('child', {
    props: {'age':Number},
    template: '<span>you are {{ age }}</span>'
  })
    new Vue({
    el:'#app',
    data:{age:'30'}
  })
</script>

如果你使用的是开发版本的 vue.js,那么会在控制台得到一个警告,Vue 将拒绝在子组件上设置此值:

[Vue warn]: Invalid prop: type check failed for prop "age". Expected Number, got String.
(found in component <child>)

当把 age 的那一行修改为数字,即:data:{age:30}。警告就会消失。
属性名称后可以加入类型,类型检查除了使用 Number,还可以有更多,完整类型列表如下:

String
Number
Boolean
Function
Object
Array

还可以在属性名后跟一个对象,在此对象内指定范围检查,提供默认值,或者要求它是必选属性。

官方手册提供了一个相对全面的验证样例:

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

🍋 使用事件

每个Vue实例都有事件接口,组件是一个具体的 Vue 实例,因此也有事件接口,用来发射和接收事件,具体事件如下:

  • 接收事件:$on(event)
  • 发射事件:$emit(event)

事件通讯 🌰。有一个父组件绑定在 #app 上,还有两个按钮组件,点击任何一个按钮让自己的计数器加 1,并且让父组件内的一个计数器加 1。

<div id="app">
  {{ total }}
  <count ref="b1" ></count>
  <count ref="b2" ></count>
</div>
<script>
 Vue.component('count', {
  template: '<button v-on:click="inc">{{ count }}</button>',
  data: function () {
    return {count: 0}
  },
  methods: {
    inc: function () {
      this.count+= 1
      this.$emit('inc')
    }
  },
})
new Vue({
  el: '#app',
  data: {total: 0},
  mounted(){
    this.$refs.b1.$on('inc',this.inc)
    this.$refs.b2.$on('inc',this.inc)
  },
  methods: {
    inc: function () {
      this.total += 1
    }
  }
})
</script>

在父组件的绑定完成钩子函数(函数 mounted)内,通过 $on 方法监听 inc 事件到 this.inc 。在子组件 count 内,完成对自己的计数器 count 加 1 后随即使用 $emit 发射事件给父组件。另外,我们使用了v-ref 指令为每一个子组件一个引用标识符,从而在代码内可以使用形如:this.$refs.childRefName 来引用子组件实例。除了在 js 代码内通过 $on 方法设置监听代码外,也可以使用指令 v-on 在 HTML 内达成类似效果:

<div id="app">
  {{ total }}
  <count v-on:inc='inc'></count>
  <count v-on:inc='inc'></count>
</div>
<script>
 Vue.component('count', {
  template: '<button v-on:click="inc">{{ count }}</button>',
  data: function () {
    return {count: 0}
  },
  methods: {
    inc: function () {
      this.count+= 1
      this.$emit('inc')
    }
  },
})
new Vue({
  el: '#app',
  data: {total: 0},
  methods: {
    inc: function () {
      this.total += 1
    }
  }
})
</script>

这种方法的好处是:

  • 省下了 ref 属性的声明,因为不必在代码中引用组件。
  • 在 HTML 就可以一目了然地看到监听的是哪个子组件。

🍊 内容分发

可以利用组件,把较大的 HTML 分解为一个个自洽的组件。比如常见的论坛首页的 HTML 的架构可能是这样的:

<div class='wrapper'>
    <div class='navigator'>navigator...</div>
    <div class='content'>
        <div class='topics'>topics...</div>
        <div class='userinfo'>userinfo...</div>
    </div>
</div>

所有的内容全部呈现在一个 HTML 内。可以想见此文件巨大,并且还会随着需求的变化而继续增长。使用组件来做分解的话,本来嵌入在 div 内的内容,现在可以分解到一个个的组件内。比如 topics,形如:

var topics = {
  template: `<div class='topics'>topics...</div>`
}

🌰:

<div id="app">
  <wrapper>
    <navi></navi>
    <content1>
        <topics></topics>
        <userinfo></userinfo>
    </content1>
  </wrapper>
</div>
<script>
  Vue.component('topics',{
    template: `<div class="topics">topics ...</div>`
  })
  Vue.component('userinfo',{
    template: `<div class="userinfo">userinfo ...</div>`
  })
  Vue.component('content1',{
    template: `<div class="content"><slot></slot></div>`,
  })
  var navi = Vue.component('navi',{
    template: `<div class="navigator">navigator ...</div>`
  })
  var wrapper = Vue.component('wrapper',{
    template: `<div class="wrapper"><slot></slot></div>`,
  })
  new Vue({
    el: "#app",
    components:{
     wrapper
    }
  });
</script>

注意:使用标签 content1,而不是 content,是因为后者是 html 内置的标签,自定义标签不应该和内置标签冲突。

  • 标签<slot> 的语义是——请把使用此组件自定义标签内的全部内容抓取过来,放置到 <slot> 所在的位置上。

🍇 使用事件总线

如果两个组件之间没有父子关系,但是也需要通讯,可以使用事件总线。具体做法就是创建一个空的 Vue 实例作为中介,事件发起方调用此实例的 $emit 方法来发射事件,而事件监听方使用此实例的 $on 方法来挂接事件。

🌰:有两个按钮,点击一个按钮会让另一个按钮的组件的 count 加 1。代码如下:

<div id="app">
  <foo></foo>
  <bar></bar>
</div>
<script>
 var bus = new Vue({})
  Vue.component('foo', {
  template: '<button v-on:click="inc">{{ count }}</button>',
  data: function () {
    return {count: 0}
  },
  mounted(){
    bus.$on('foo-inc',this.doinc)
  },
  methods: {
    inc: function () {
      bus.$emit('bar-inc',this)
    },
    doinc: function () {
      this.count++
    }
  },
})
Vue.component('bar', {
  template: '<button v-on:click="inc">{{ count }}</button>',
  data: function () {
    return {count: 0}
  },
  mounted(){
    bus.$on('bar-inc',this.doinc)
  },
  methods: {
    inc: function () {
      bus.$emit('foo-inc',this)
    },
    doinc: function () {
      this.count++
    }
  }})
new Vue({
  el: '#app'
})
</script>

上面的例子是同属一个父组件的两个兄弟组件的通讯方法。实际上作为总线方式的 Vue 实例,可以用于任何组件之间的通讯。

综合案例 🌰

todo app —— 集中化的 eventBus

<html>
  <head>
    <script src="https://cdn.jsdelivr.net/vue/1.0.28/vue.min.js"></script>
  </head>
<body>
  <div id="todo-app">
      <h1>todo app</h1>
      <new-todo></new-todo>
      <todo-list></todo-list>
  </div>
  <script>
  var eventHub =new Vue( {
    data(){
      return{
        todos:['A','B','C']
      }
    },
    created: function () {
      this.$on('add', this.addTodo)
      this.$on('delete', this.deleteTodo)
    },
    beforeDestroy: function () {
      this.$off('add', this.addTodo)
      this.$off('delete', this.deleteTodo)
         },
    methods: {
      addTodo: function (newTodo) {
        this.todos.push(newTodo)
      },
      deleteTodo: function (i) {
        this.todos.splice(i,1)
      }
    }
  })
  var newTodo = {
        template:'<div><input type="text" autofocus v-model="newtodo"/><button @click="add">add</button></div>',
        data(){
          return{
            newtodo:''
          }
        },
        methods:{
          add:function(){
            eventHub.$emit('add', this.newtodo)
            this.newtodo = ''
          }
      }
  }
  var todoList = {
      template:'<ul>
        <li v-for="(index,item) in items">{{item}}
          <button @click="rm(index)">X</button></li>
        </ul>',
      data(){
        return{
          items:eventHub.todos
        }
      },
      methods:{
       rm:function(i){
          eventHub.$emit('delete', i)
        }
      }
  }
  var app= new Vue({
    el:'#todo-app',
    components:{
      newTodo:newTodo,
      todoList:todoList
    }
  })
  </script>
</body>
</html>
  • app 组件不再承担通讯中介功能,而只是简单的作为两个子组件的容器。
  • eventBus 组件承载了全部的数据(todos),以及对数据的修改,它监听事件 add 和 delete,在监听函数内操作数据。
  • 子组件 todoList 的 data 成员的数据来源改为从 eventBus 获取,删除 todo 的方法内不再操作数据,而是转发给 eventBus 来完成删除。
  • 子组件 newTodo 的按钮不再添加数据,而是转发事件给 eventBus,由后者完成添加。

组件编码风格

Vue 组件是很好的复用代码的方法。🌰:

<div id="app">
    <span>{{count}}</span>
    <button @click="inc">+</button>
</div>

标签 <span><button> 其实一起合作,完成一个完整的功能,它们是内聚的;因此可以利用组件的概念,用一个语义化的自定义标签 ,把两个标签包装到一个组件内。以此观念,做完后应该得到这样的代码:

<div id="app">
    <counter></counter>
</div>

为此,需要创建一个组件,它可以容纳两个标签以及和它们有关的方法和数据。我们会采用多种方案来完成此组件,从而了解组件的多种编码风格。

✏️ 集中模板模式

以下代码是可以直接保存为 html 文件,并使用浏览器来打开运行的:

<div id="app">
  <counter></counter>
</div>
<script>
var counter = {
        'template':'<div><span>{{count}}</span><button v-on:click="inc">+</button></div>',
         data () {
            return {count: 0}
          },
          methods: {
            inc () {this.count++}
          }
    }

var app = new Vue({
  components:{
    counter : counter
   }}
)
app.$mount('#app')
</script>
  • Vue 的实例属性 template。它的值用来承载模板代码,本来放置在主 HTML 内的两个标签现在搬移到此处。需要注意的是,两个标签外套上了一个 div 标签,因为 Vue2.0 版本要求作为模板的 html 必须是单根的。
  • Vue 的实例属性 components。它可以被用来注册一个局部组件。正是在此处,组件 counter 被注册,从而在 html 标签内可以直接使用标签 <counter> 来引用组件 counter。

✒️ 分离模板模式

为了增加可读性,模板字符串内的 HTML 可以使用多种方式从代码中分离出来。比如采用 x-template 方法:

<script type="x-template" id="t">
    <div>
      <span>{{count}}</span>
      <button v-on:click="inc">+</button>
    </div>
</script>

<div id="app">
  <counter></counter>
</div>
<script>
var counter = {
          'template':'#t',
         data () {
            return {count: 0}
          },
          methods: {
            inc () {this.count++}
          }
    }

var app = new Vue({
  components:{
    counter : counter
   }}
)
app.$mount('#app')
</script>

模板 x-template 使用标签 script,因为这个标签的类型是浏览器无法识别的,故而浏览器只是简单地放在 DOM 节点上。这样你可以使用 getElementById 方法获得此节点,把它作为 HTML 片段使用。

或者使用在 HTML5 引入的新标签 template,看起来稍微干净些:

<template id="t">
    <div>
      <span>{{count}}</span>
      <button v-on:click="inc">+</button>
    </div>
</template>

<div id="app">
  <counter></counter>
</div>
<script>
var counter = {
   'template':'#t',
   data () {
      return {count: 0}
    },
    methods: {
      inc () {this.count++}
    }
}

var app = new Vue({
  components:{
    counter : counter
   }}
)
app.$mount('#app')
</script>

或者如果组件内容并不需要做分发的话,可以通过 inline-template 标记它的内容,把它当作模板:

<div id="app">
  <counter  inline-template>
    <div>
      <span>{{count}}</span>
      <button v-on:click="inc">+</button>
    </div>
  </counter>
</div>
<script>
var counter = {
   data () {
      return {count: 0}
   },
   methods: {
      inc () {this.count++}
   }
}

var app = new Vue({
  components:{
    counter : counter
   }
})
app.$mount('#app')
</script>

函数式

Render 函数可以充分利用 JavaScript 语言在创建 HTML 模板方面的灵活性。实际上,组件的 Template 最终都会转换为 Render 函数。

<div id="app">
  <counter></counter>
</div>
<script>
var a = {
	data () {
	    return {count: 1}
	},
	methods: {
	    inc () {this.count++}
	},
	render:function(h){
	    // var self = this;
	    var buttonAttrs = {
	        on: { click: this.inc },
	        domProps: {
	           innerHTML: '+'
	        },
	    };
	    var spanAttrs = {
	        on: { click: this.inc },
	        domProps: {
	            innerHTML: this.count.toString()
	        },
	     };
	     var span = h('span', spanAttrs, []);
	     var button = h('button', buttonAttrs, []);
	     return h('div',{},[
	       span,
	       button
	     ])
	
	  }
  }

new Vue({
  el:'#app',
  components:{
    counter : a
  }
})
</script>




🔗:

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值