当前位置:   article > 正文

深入理解Vue.js组件!_需要完成三个组件:home、list和content。需要注意的是,这三个组件应该都有自己的

需要完成三个组件:home、list和content。需要注意的是,这三个组件应该都有自己的

组件

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

注册和引用

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

Vue.component(tag, options)

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

  1. <script src="https://unpkg.com/vue/dist/vue.js"></script>
  2. <div id="app">
  3. <tag></tag>
  4. </div>
  5. <script>
  6. Vue.component('tag', {
  7. template: `<div>one component rule all other<div>`
  8. })
  9. new Vue({
  10. el: "#app"
  11. });
  12. </script>
  13. 你也可以局部注册,这样注册的组件,仅仅限于执行注册的Vue实例内:
  14. <script src="https://unpkg.com/vue/dist/vue.js"></script>
  15. <div id="app">
  16. <tag></tag>
  17. </div>
  18. <script>
  19. var tag = {
  20. template: `<div>one component rule all other<div>`
  21. }
  22. new Vue({
  23. el: "#app",
  24. components: {tag}
  25. });
  26. </script>

我们注意到,<tag>是HTML本身并不具备的标签,现在由Vue的组件技术引入,因此被称为是自定义标签。这些自定义标签的背后实现常常是标签、脚本、css的集合体。它的内部可以非常复杂,但是对外则以大家习惯的简单的标签呈现。通过本节这个小小案例,组件技术带来的抽象价值已经展现出来一角了。

动态挂接

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

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

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

  1. <script src="https://unpkg.com/vue/dist/vue.js"></script>
  2. <div id="app">
  3. <component v-bind:is="current">
  4. </component>
  5. </div>
  6. <script>
  7. var app = new Vue({
  8. el: '#app',
  9. data: {
  10. current: 'archive',
  11. i :0,
  12. b : ['home','posts','archive']
  13. },
  14. components: {
  15. home: { template:'<h1>home</h1>'},
  16. posts: { template:'<h1>posts</h1>' },
  17. archive: {template:'<h1>archive</h1>'}
  18. },
  19. methods:{
  20. a(){
  21. this.i = this.i % 3
  22. this.current = this.b[this.i]
  23. this.i++
  24. setTimeout(this.a,2000)
  25. }
  26. },
  27. mounted(){
  28. setTimeout(this.a,2000)
  29. }
  30. })
  31. </script>

引用组件

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

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

  1. <script src="https://unpkg.com/vue/dist/vue.js"></script>
  2. <div id="app">
  3. {{ total }}
  4. <count ref="b1"></count>
  5. <count ref="b2"></count>
  6. <button v-on:click="value">value</button>
  7. </div>
  8. <script>
  9. Vue.component('count', {
  10. template: '<button v-on:click="inc">{{ count }}</button>',
  11. data: function () {
  12. return {count: 0}
  13. },
  14. methods: {
  15. inc: function () {
  16. this.count+= 1
  17. }
  18. },
  19. })
  20. new Vue({
  21. el: '#app',
  22. data: {total:0},
  23. methods: {
  24. value: function () {
  25. this.total = this.$refs.b1.count+this.$refs.b2.count
  26. }
  27. }
  28. })
  29. </script>

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

组件协作

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

使用属性

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

  1. <script src="https://unpkg.com/vue/dist/vue.js"></script>
  2. <div id="app">
  3. <child message="hello"></child>
  4. </div>
  5. <script>
  6. Vue.component('child', {
  7. props: ['message'],
  8. template: '<span>{{ message }}</span>'
  9. })
  10. new Vue({el:'#app'})
  11. </script>

本案例会显示

hello

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

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

  1. <script src="https://unpkg.com/vue/dist/vue.js"></script>
  2. <div id="app">
  3. <child v-bind:message="hello+',world'"></child>
  4. </div>
  5. <script>
  6. Vue.component('child', {
  7. props: ['message'],
  8. template: '<span>{{ message }}</span>'
  9. })
  10. new Vue({
  11. el:'#app',
  12. data:{hello:'hi'}
  13. })
  14. </script>

运行结果为:

hi,world

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

属性验证

当通过属性传递表达式时,有些时候类型是特定的,Vue提供了属性的验证,包括类型验证,范围验证等。比如传递年龄进来的话,要求应该是整数。案例如下:

  1. <script src="https://unpkg.com/vue/dist/vue.js"></script>
  2. <div id="app">
  3. <child v-bind:age="age"></child>
  4. </div>
  5. <script>
  6. Vue.component('child', {
  7. props: {'age':Number},
  8. template: '<span>you are {{ age }}</span>'
  9. })
  10. new Vue({
  11. el:'#app',
  12. data:{age:'30'}
  13. })
  14. </script>

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

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

当把age的那一行修改为数字,即:

data:{age:30}

警告就会消失。属性名称后可以加入类型,类型检查除了使用Number,还可以有更多,完整类型列表如下:

  1. String
  2. Number
  3. Boolean
  4. Function
  5. Object
  6. Array

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

  1. <script src="https://unpkg.com/vue/dist/vue.js"></script>
  2. <div id="app">
  3. <child v-bind:age="age"></child>
  4. </div>
  5. <script>
  6. Vue.component('child', {
  7. props: {'age':{
  8. type:Number,
  9. validator: function (value) {
  10. return value > 0 && value<150
  11. },
  12. required:true,
  13. default:50
  14. }
  15. },
  16. template: '<span>you are {{ age }}</span>'
  17. })
  18. new Vue({
  19. el:'#app',
  20. data:{age:'149'}
  21. })
  22. </script>

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

  1. Vue.component('example', {
  2. props: {
  3. // 基础类型检测 (`null` 意思是任何类型都可以)
  4. propA: Number,
  5. // 多种类型
  6. propB: [String, Number],
  7. // 必传且是字符串
  8. propC: {
  9. type: String,
  10. required: true
  11. },
  12. // 数字,有默认值
  13. propD: {
  14. type: Number,
  15. default: 100
  16. },
  17. // 数组/对象的默认值应当由一个工厂函数返回
  18. propE: {
  19. type: Object,
  20. default: function () {
  21. return { message: 'hello' }
  22. }
  23. },
  24. // 自定义验证函数
  25. propF: {
  26. validator: function (value) {
  27. return value > 10
  28. }
  29. }
  30. }
  31. })

使用事件

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

  1. 发射事件:$on(event)

  2. 接收事件:$emit(event)

我们假设一个案例来说明事件通讯。此案例中,有一个父组件绑定在#app上,还有两个按钮组件,点击任何一个按钮让自己的计数器加1,并且让父组件内的一个计数器加1。图例:

{%}

使用一个案例,来演示事件的使用:

  1. <script src="https://unpkg.com/vue/dist/vue.js"></script>
  2. <div id="app">
  3. {{ total }}
  4. <count ref="b1" ></count>
  5. <count ref="b2" ></count>
  6. </div>
  7. <script>
  8. Vue.component('count', {
  9. template: '<button v-on:click="inc">{{ count }}</button>',
  10. data: function () {
  11. return {count: 0}
  12. },
  13. methods: {
  14. inc: function () {
  15. this.count+= 1
  16. this.$emit('inc')
  17. }
  18. },
  19. })
  20. new Vue({
  21. el: '#app',
  22. data: {total: 0},
  23. mounted(){
  24. this.$refs.b1.$on('inc',this.inc)
  25. this.$refs.b2.$on('inc',this.inc)
  26. },
  27. methods: {
  28. inc: function () {
  29. this.total += 1
  30. }
  31. }
  32. })
  33. </script>

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

this.$refs.childRefName

来引用子组件实例。除了在js代码内通过$on方法设置监听代码外,也可以使用指令v-on在HTML内达成类似效果:

  1. <script src="https://unpkg.com/vue/dist/vue.js"></script>
  2. <div id="app">
  3. {{ total }}
  4. <count v-on:inc='inc'></count>
  5. <count v-on:inc='inc'></count>
  6. </div>
  7. <script>
  8. Vue.component('count', {
  9. template: '<button v-on:click="inc">{{ count }}</button>',
  10. data: function () {
  11. return {count: 0}
  12. },
  13. methods: {
  14. inc: function () {
  15. this.count+= 1
  16. this.$emit('inc')
  17. }
  18. },
  19. })
  20. new Vue({
  21. el: '#app',
  22. data: {total: 0},
  23. methods: {
  24. inc: function () {
  25. this.total += 1
  26. }
  27. }
  28. })
  29. </script>

这种方法的好处是:

  1. 省下了ref属性的声明,因为不必在代码中引用组件。

  2. 在HTML就可以一目了然地看到监听的是哪个子组件。

内容分发

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

  1. <div class='wrapper'>
  2. <div class='navigator'>navigator...</div>
  3. <div class='content'>
  4. <div class='topics'>topics...</div>
  5. <div class='userinfo'>userinfo...</div>
  6. </div>
  7. </div>

所有的内容全部呈现在一个HTML内。可以想见此文件巨大,并且还会随着需求的变化而继续增长。如果使用组件来做分解,那么首页可以变为:

  1. <wrapper>
  2. <navigator></navigator>
  3. <content1>
  4. <topics><topics>
  5. <userinfo></userinfo>
  6. </content1>
  7. <wrapper>

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

本来嵌入在div内的内容,现在可以分解到一个个的组件内。比如topics,形如:

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

如下是一个可运行的案例:

  1. <script src="https://unpkg.com/vue/dist/vue.js"></script>
  2. <div id="app">
  3. <wrapper>
  4. <navi></navi>
  5. <content1>
  6. <topics></topics>
  7. <userinfo></userinfo>
  8. </content1>
  9. <wrapper>
  10. </div>
  11. <script>
  12. Vue.component('topics',{
  13. template: `<div class="topics">topics ...<div>`
  14. })
  15. Vue.component('userinfo',{
  16. template: `<div class="userinfo">userinfo ...<div>`
  17. })
  18. Vue.component('content1',{
  19. template: `<div class="content"><slot></slot><div>`,
  20. })
  21. var navi = Vue.component('navi',{
  22. template: `<div class="navigator">navigator ...<div>`
  23. })
  24. var wrapper = Vue.component('wrapper',{
  25. template: `<div class="wrapper"><slot></slot><div>`,
  26. })
  27. new Vue({
  28. el: "#app",
  29. components:{
  30. wrapper
  31. }
  32. });
  33. </script>

请留意到wrapper组件模板内使用了<slot>标签,content1组件内也使用了<slot>。标签<slot>的语义是——请把使用此组件自定义标签内的全部内容抓取过来,放置到<slot>所在的位置上。以content1为例,它在自定义标签内的全部内容为:

  1. <topics></topics>
  2. <userinfo></userinfo>

这里内容会直接被抓取过来,放置到<slot>处,从而混合得到content最终的模板:

  1. <div class="content">
  2. <topics></topics>
  3. <userinfo></userinfo>
  4. <div>

这个过程奇妙而难解,但是非常有用。这意味着,可以通过<slot>,把父组件内的HTML片段传递到组件内,从而完成一种另类的父子数据传递。

随即发生的,是<topics><userinfo>代表的组件的内容也混入到content1内,变成

  1. <div class="content">
  2. <div class="topics">topics ...<div>
  3. <div class="userinfo">userinfo ...<div>
  4. <div>

就这样,我们通过<slot>技术,一步步从组件还原出最初的HTML。这个技术被称为内容分发。slot,也就是插槽,是内容分发的重要标签。

详解插槽

如果子组件模板包含<slot>,父组件的内容就会被插入到<slot>位置上并替换掉<slot>标签本身,否则父组件内的内容将会被丢弃。

如果<slot>标签中本身是有内容的,那么这些内容如何和插入的内容合并呢?这些本有的内容被视为备用内容。也就是说,如果父组件内元素为空,备用内容会保留,否则就会被丢弃。

假定子组件foo有下面模板:

  1. <div>
  2. <slot>
  3. 备用内容
  4. </slot>
  5. </div>

父组件内容如下:

  1. <div>
  2. <foo>
  3. <p>parent content</p>
  4. </foo>
  5. </div>

渲染结果:

  1. <div>
  2. <div>
  3. <p>parent content</p>
  4. </div>
  5. </div>

如果父组件为:

  1. <div>
  2. <foo>
  3. </foo>
  4. </div>

那么渲染结果为:

  1. <div>
  2. <div>
  3. 备用内容
  4. </div>
  5. </div>

如下是一个整合后的案例:

  1. <script src="https://unpkg.com/vue/dist/vue.js"></script>
  2. <div id="app">
  3. <foo><p>parent content</p></foo>
  4. </div>
  5. <script>
  6. Vue.component('foo', {
  7. template: `
  8. <div>
  9. <slot>
  10. 备用内容
  11. </slot>
  12. </div>`,
  13. })
  14. new Vue({
  15. el: '#app'
  16. })
  17. </script>

你可以试试删除<p>parent content</p>,对比父组件有无内容带来的差别。

多个插槽

嵌入到子组件标签的内容,可以通过给予属性slot不同的值,来区别不同的插槽。在子组件内使用<slot name=''>方式来引用它们。有了命名插槽,内容分发可以变得更加灵活。在多个插槽的场景下,如果找不到匹配的插槽,可以使用一个备用的插槽来承载内容。如果内容既找不到命名插槽,也没有备用插槽的话,就会被丢弃。

假设一个子组件<app-layout>模板为:

  1. <div class="container">
  2. <header>
  3. <slot name="header"></slot>
  4. </header>
  5. <body>
  6. <slot></slot>
  7. </body>
  8. </div>

父组件模板为:

  1. <app-layout>
  2. <h1 slot="header">title</h1>
  3. <p>content</p>
  4. </app-layout>

那么渲染结果:

  1. <div class="container">
  2. <header>
  3. <h1>title</h1>
  4. </header>
  5. <body>
  6. <p>content</p>
  7. </body>
  8. </div>
综合案例:插槽

现在我们看一个高级的案例,我来做一个即时贴(sticky)组件,用来显示一个有标题和主体的即时贴。组件会定义好即时贴的结构和外观,而具体的标题和内容值则使用内容分发技术来传入组件:

  1. <script src="https://unpkg.com/vue/dist/vue.js"></script>
  2. <div class="" id="app">
  3. <sticky>
  4. <div slot="title">
  5. <h3>Title</h3></div>
  6. <div slot="body"><p>
  7. Body foo bar baz ddd
  8. </p></div>
  9. </sticky>
  10. </div>
  11. <script>
  12. Vue.component('sticky', {
  13. template: `
  14. <div>
  15. <div class="wrapper">
  16. <div>
  17. <div class="title">
  18. <slot name="title"></slot>
  19. </div>
  20. <div class="body">
  21. <slot name="body"></slot>
  22. </div>
  23. </div>
  24. </div>
  25. </div>`
  26. });
  27. new Vue({
  28. el: "#app"
  29. });
  30. </script>
  31. <style>
  32. .wrapper {
  33. display: flex;
  34. width: 180px;
  35. height: 150px;
  36. background: yellow;
  37. border-radius: 10px;
  38. }
  39. .title {
  40. border-bottom:1px solid red
  41. }
  42. .body {
  43. border-bottom:1px solid blue
  44. }
  45. </style>

本案例内,使用上下文通过属性slot创建了两个插槽,分别为title和body,在组件的模板内通过<slot>标签引用对应名称的插槽(title和body),并把它注入到插槽标签占据的位置上。

使用事件总线

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

举例说明。此案例代码中有两个按钮,点击一个按钮会让另一个按钮的组件的count加1。代码如下:

  1. <script src="https://unpkg.com/vue/dist/vue.js"></script>
  2. <div id="app">
  3. <foo></foo>
  4. <bar></bar>
  5. </div>
  6. <script>
  7. var bus = new Vue({})
  8. Vue.component('foo', {
  9. template: '<button v-on:click="inc">{{ count }}</button>',
  10. data: function () {
  11. return {count: 0}
  12. },
  13. mounted(){
  14. bus.$on('foo-inc',this.doinc)
  15. },
  16. methods: {
  17. inc: function () {
  18. bus.$emit('bar-inc',this)
  19. },
  20. doinc: function () {
  21. this.count++
  22. }
  23. },
  24. })
  25. Vue.component('bar', {
  26. template: '<button v-on:click="inc">{{ count }}</button>',
  27. data: function () {
  28. return {count: 0}
  29. },
  30. mounted(){
  31. bus.$on('bar-inc',this.doinc)
  32. },
  33. methods: {
  34. inc: function () {
  35. bus.$emit('foo-inc',this)
  36. },
  37. doinc: function () {
  38. this.count++
  39. }
  40. }})
  41. new Vue({
  42. el: '#app'
  43. })
  44. </script>

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

综合案例

为了演示说明vue的组件通讯,我们从一个假设的todo应用开始(再次:)。UI当然还是类似这样的:

{%}todoapp.png

但是这次的新意在于,我们会把整个app分为三个组件,层次关系如下:

  1. app
  2. --newTodo
  3. --todoList

app作为父应用组件,newTodo作为一个子组件负责用户输入,并且获取新的todo;而todoList作为另外一个子组件,它需要负责显示全部todo项目到列表中。

这样的分工在稍微大的app中非常常见,由此达成分而治之的目的。但是组件之间势必需要通讯,比如newTodo组件必须把新的todo字符串通知到todoList组件,以便后者更新todo列表并由此更新用户界面。

基于组件结构的通讯

在Vue.js 1.0版本内,从子组件到父组件的通讯,Vue.js提供了$dispatch方法,而从父组件到子组件则是通过 $broadcast方法。我们在如下的代码中使用了此技术:

  1. <html>
  2. <head>
  3. <script src="https://cdn.jsdelivr.net/vue/1.0.28/vue.min.js"></script>
  4. </head>
  5. <body>
  6. <div id="todo-app">
  7. <h1>todo app</h1>
  8. <new-todo></new-todo>
  9. <todo-list></todo-list>
  10. <div>
  11. <script>
  12. var newTodo = {
  13. template:'<div><input type="text" autofocus v-model="newtodo"/><button @click="add">add</button></div>',
  14. data(){
  15. return{
  16. newtodo:''
  17. }
  18. },
  19. methods:{
  20. add:function(){
  21. this.$dispatch('newtodo',this.newtodo)
  22. this.newtodo = ''
  23. }
  24. }
  25. }
  26. var todoList = {
  27. template:'<ul> \
  28. <li v-for="(index,item) in items">{{item}} \
  29. <button @click="rm(index)">X</button></li> \
  30. </ul>',
  31. data(){
  32. return{
  33. items:['item 1','item 2','item 3'],
  34. }
  35. },
  36. methods:{
  37. rm:function(i){
  38. this.items.splice(i,1)
  39. }
  40. },
  41. events: {
  42. 'newtodo': function (newtodo) {
  43. this.items.push(newtodo)
  44. }
  45. },
  46. }
  47. var app= new Vue({
  48. el:'#todo-app',
  49. components:{
  50. newTodo:newTodo,
  51. todoList:todoList
  52. },
  53. events: {
  54. 'newtodo': function (newtodo) {
  55. this.$broadcast('newtodo',newtodo)
  56. }
  57. }
  58. })
  59. </script>
  60. </body>
  61. </html>

整个通讯过程是这样的:

  1. 组件newTodo在用户点击按钮后,会把新的todo字符串通过$dispatch发出。

  2. 而父组件app在event内截获此事件,随即通过$broadcast方法发送到子组件。

  3. 子组件todoList在event内截获此事件取出payload,加入它到数据items内。

这就是组件通讯的方法。Vue.js并没有为兄弟组件提供直接的通讯方法,如果兄弟组件之间需要通讯,只能先发给父组件,父组件向子组件广播,侦听此事件的子组件随后获取此事件。

通过$broadcast+$dispatch完成组件通讯是可行的,但是问题不少:

  1. 依赖于树形组件结构,你得知道组件的结构是怎么样的。

  2. 组件结构复杂的话,必然降低通讯效率。

  3. 兄弟组件直接不能直接通讯,必须通过父组件间接完成。

在Vue2.0版本内,此方法已经被废弃。

集中化的eventBus

实际上,我们只是为了让两个组件交换数据,这个过程并不应该和组件的结构(父子关系的组件,兄弟关系的组件)捆绑在一起。因此,一个变通的方式是引入一个新的组件,用它作为组件之间的通讯中介,此技术被称为Event Bus。如下代码正式利用了此技术:

  1. <html>
  2. <head>
  3. <script src="https://cdn.jsdelivr.net/vue/1.0.28/vue.min.js"></script>
  4. </head>
  5. <body>
  6. <div id="todo-app">
  7. <h1>todo app</h1>
  8. <new-todo></new-todo>
  9. <todo-list></todo-list>
  10. <div>
  11. <script>
  12. var eventHub =new Vue( {
  13. data(){
  14. return{
  15. todos:['A','B','C']
  16. }
  17. },
  18. created: function () {
  19. this.$on('add', this.addTodo)
  20. this.$on('delete', this.deleteTodo)
  21. },
  22. beforeDestroy: function () {
  23. this.$off('add', this.addTodo)
  24. this.$off('delete', this.deleteTodo)
  25. },
  26. methods: {
  27. addTodo: function (newTodo) {
  28. this.todos.push(newTodo)
  29. },
  30. deleteTodo: function (i) {
  31. this.todos.splice(i,1)
  32. }
  33. }
  34. })
  35. var newTodo = {
  36. template:'<div><input type="text" autofocus v-model="newtodo"/><button @click="add">add</button></div>',
  37. data(){
  38. return{
  39. newtodo:''
  40. }
  41. },
  42. methods:{
  43. add:function(){
  44. eventHub.$emit('add', this.newtodo)
  45. this.newtodo = ''
  46. }
  47. }
  48. }
  49. var todoList = {
  50. template:'<ul> \
  51. <li v-for="(index,item) in items">{{item}} \
  52. <button @click="rm(index)">X</button></li> \
  53. </ul>',
  54. data(){
  55. return{
  56. items:eventHub.todos
  57. }
  58. },
  59. methods:{
  60. rm:function(i){
  61. eventHub.$emit('delete', i)
  62. }
  63. }
  64. }
  65. var app= new Vue({
  66. el:'#todo-app',
  67. components:{
  68. newTodo:newTodo,
  69. todoList:todoList
  70. }
  71. })
  72. </script>
  73. </body>
  74. </html>

由此代码我们可以看到:

  1. app组件不再承担通讯中介功能,而只是简单的作为两个子组件的容器。

  2. eventBus组件承载了全部的数据(todos),以及对数据的修改,它监听事件add和delete,在监听函数内操作数据。

  3. 子组件todoList的data成员的数据来源改为从eventBus获取,删除todo的方法内不再操作数据,而是转发给eventBus来完成删除。

  4. 子组件newTodo的按钮不再添加数据,而是转发事件给eventBus,由后者完成添加。

这样做,就把本来捆绑到组件结构上的通讯还原为单纯的通讯,并且集中数据和操作到一个对象(eventBus),也就有利于组件的数据共享。当我们谈到eventBus的时候,我们离vuex——一个更加专业的状态管理库就比较近了。后文会谈及vuex。

组件编码风格

Vue组件是很好的复用代码的方法。接下来,我们使用一个微小的案例来讲解组件。我们可以看到HTML代码:

  1. <div id="app">
  2. <span>{{count}}</span>
  3. <button @click="inc">+</button>
  4. </div>

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

  1. <div id="app">
  2. <counter></counter>
  3. </div>

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

集中模板式

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

  1. <script src="https://unpkg.com/vue/dist/vue.js"></script>
  2. <div id="app">
  3. <counter></counter>
  4. </div>
  5. <script>
  6. var counter = {
  7. 'template':'<div><span>{{count}}</span><button v-on:click="inc">+</button></div>',
  8. data () {
  9. return {count: 0}
  10. },
  11. methods: {
  12. inc () {this.count++}
  13. }
  14. }
  15. var app = new Vue({
  16. components:{
  17. counter : counter
  18. }}
  19. )
  20. app.$mount('#app')
  21. </script>

我们对代码稍作解释:

  1. Vue的实例属性template。它的值用来承载模板代码,本来放置在主HTML内的两个标签现在搬移到此处。需要注意的是,两个标签外套上了一个div标签,因为Vue2.0版本要求作为模板的html必须是单根的。

  2. Vue的实例属性components。它可以被用来注册一个局部组件。正是在此处,组件counter被注册,从而在html标签内可以直接使用标签<counter>来引用组件counter。

引入组件技术后,强相关性的html标签和对应的数据、代码内聚到了一起,这是符合软件工程分治原则的行为。

另外,使用template在代码内混合html字符串还是比较烦人的:

  1. 你得小心的在外层使用单引号,在内部使用双引号。

  2. 如果html比较长,产生了跨行,这样的字符串书写比较麻烦。

我们继续查看其它方案。

分离模板式

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

  1. <script src="https://unpkg.com/vue/dist/vue.js"></script>
  2. <script type="x-template" id="t">
  3. <div>
  4. <span>{{count}}</span>
  5. <button v-on:click="inc">+</button>
  6. </div>
  7. </script>
  8. <div id="app">
  9. <counter></counter>
  10. </div>
  11. <script>
  12. var counter = {
  13. 'template':'#t',
  14. data () {
  15. return {count: 0}
  16. },
  17. methods: {
  18. inc () {this.count++}
  19. }
  20. }
  21. var app = new Vue({
  22. components:{
  23. counter : counter
  24. }}
  25. )
  26. app.$mount('#app')
  27. </script>

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

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

  1. <script src="https://unpkg.com/vue/dist/vue.js"></script>
  2. <template id="t">
  3. <div>
  4. <span>{{count}}</span>
  5. <button v-on:click="inc">+</button>
  6. </div>
  7. </template>
  8. <div id="app">
  9. <counter></counter>
  10. </div>
  11. <script>
  12. var counter = {
  13. 'template':'#t',
  14. data () {
  15. return {count: 0}
  16. },
  17. methods: {
  18. inc () {this.count++}
  19. }
  20. }
  21. var app = new Vue({
  22. components:{
  23. counter : counter
  24. }}
  25. )
  26. app.$mount('#app')
  27. </script>

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

  1. <script src="https://unpkg.com/vue/dist/vue.js"></script>
  2. <div id="app">
  3. <counter inline-template>
  4. <div>
  5. <span>{{count}}</span>
  6. <button v-on:click="inc">+</button>
  7. </div>
  8. </counter>
  9. </div>
  10. <script>
  11. var counter = {
  12. data () {
  13. return {count: 0}
  14. },
  15. methods: {
  16. inc () {this.count++}
  17. }
  18. }
  19. var app = new Vue({
  20. components:{
  21. counter : counter
  22. }}
  23. )
  24. app.$mount('#app')
  25. </script>

函数式

Render函数可以充分利用JavaScript语言在创建HTML模板方面的灵活性。实际上,组件的Template最终都会转换为Render函数。对于同一需求,使用Render函数的代码如下:

  1. <script src="https://unpkg.com/vue/dist/vue.js"></script>
  2. <div id="app">
  3. <counter></counter>
  4. </div>
  5. <script>
  6. var a = {
  7. data () {
  8. return {count: 1}
  9. },
  10. methods: {
  11. inc () {this.count++}
  12. },
  13. render:function(h){
  14. // var self = this;
  15. var buttonAttrs = {
  16. on: { click: this.inc },
  17. domProps: {
  18. innerHTML: '+'
  19. },
  20. };
  21. var spanAttrs = {
  22. on: { click: this.inc },
  23. domProps: {
  24. innerHTML: this.count.toString()
  25. },
  26. };
  27. var span = h('span', spanAttrs, []);
  28. var button = h('button', buttonAttrs, []);
  29. return h('div'
  30. ,{},
  31. [
  32. span,
  33. button
  34. ])
  35. }
  36. }
  37. new Vue({
  38. el:'#app',
  39. components:{
  40. counter : a
  41. }}
  42. )
  43. </script>

函数render的参数h,其实是一个名为createElement 的函数,可以用来创建元素。此函数的具体说明,请参考官方手册即可。为了方便,此处完整使用createElement的实例代码抄写自vue.js手册。如下 :

  1. createElement(
  2. // {String | Object | Function}
  3. // An HTML tag name, component options, or function
  4. // returning one of these. Required.
  5. 'div',
  6. // {Object}
  7. // A data object corresponding to the attributes
  8. // you would use in a template. Optional.
  9. {
  10. // (see details in the next section below)
  11. },
  12. // {String | Array}
  13. // Children VNodes. Optional.
  14. [
  15. createElement('h1', 'hello world'),
  16. createElement(MyComponent, {
  17. props: {
  18. someProp: 'foo'
  19. }
  20. }),
  21. 'bar'
  22. ]
  23. )

如果要标签名本身都是可以动态的,怎么办?比如我希望提供一个标签,可以根据属性值动态选择head的层级,像是把

  1. <h1>header1</h1>
  2. <h2>header2</h2>

可以替代为:

  1. <hdr :level="1">header1</hdr>
  2. <hdr :level="2">header2</hdr>

使用render函数解决此类问题是非常便利的。具体做法就是先注册一个组件:

  1. Vue.component('hdr', {
  2. render: function (createElement) {
  3. return createElement(
  4. 'h' + this.level, // tag name
  5. this.$slots.default // array of children
  6. )
  7. },
  8. props: {
  9. level: {
  10. type: Number,
  11. required: true
  12. }
  13. }
  14. })

随后在html内使用此组件:

  1. //javascript
  2. new Vue({
  3. el: '#example'
  4. })
  5. // html
  6. <div id="example">
  7. <hdr :level="1">abc</hdr>
  8. <hdr :level="2">abc</hdr>
  9. </div>

可以执行的代码在此:

  1. <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.0.3/vue.js">
  2. </script>
  3. <div id="example">
  4. <hdr :level="1">abc</hdr>
  5. <hdr :level="2">abc</hdr>
  6. </div>
  7. <script type="text/javascript"> Vue.component('hdr', {
  8. render: function (createElement) {
  9. console.log(this.level)
  10. return createElement(
  11. 'h' + this.level,
  12. this.$slots.default
  13. )
  14. },
  15. props: {
  16. level: {
  17. type: Number,
  18. required: true
  19. }
  20. }
  21. })
  22. new Vue({
  23. el: '#example'
  24. })
  25. </script>

函数render会传入一个createElement函数作为参数,你可以使用此函数来创建标签。在render函数内,可以通过this.$slots访问slot,从而把slot内的元素插入到当前被创建的标签内。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/笔触狂放9/article/detail/69784?site
推荐阅读
相关标签
  

闽ICP备14008679号