你的位置:首页 > Java教程

[Java教程]vue.js学习之组件(下篇)

 本文的Demo和源代码已放到GitHub,如果您觉得本篇内容不错,请点个赞,或在GitHub上加个星星!

https://github.com/zwl-jasmine95/Vue_test

 以下所有知识都是基于vue.js 2.0版本


 

一、组件编译作用域

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

message 应该绑定到父组件的数据,组件作用域简单地说是:

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

组件的模板是在其作用域内编译的,那么组件选项对象中的数据也应该是在组件模板中使用的。

 1  <div id="component-demo"> 2   <!-- #component-demo是Vue实例挂载的元素,应该在挂载元素范围内使用组件--> 3   <hello-component></hello-component> 4  </div> 5  6 <script type="text/javascript"> 7  Vue.component('hello-component',{ 8   template:'<h1>hello component!</h1>' 9  });10  var vm = new Vue({11   el:'#component-demo'12  });13 14 </script>

在创建一个Vue实例时,除了将它挂载到某个HTML元素下,还要编译组件,将组件转换为HTML片段。
除此之外,Vue实例还会识别其所挂载的元素下的<hello-component>标签,然后将<hello-component>标签替换为HTML片段。

实际上浏览器仍然是不理解<hello-component>标签的,

组件在使用前,经过编译已经被转换为HTML片段了,组件是有一个作用域的,那么组件的作用域可以将它理解为组件模板包含的HTML片段,组件模板内容之外就不是组件的作用域了。

例如,hello-component组件的作用域只是下面这个小片段:

通俗地讲,在子组件中定义的数据,只能用在子组件的模板。在父组件中定义的数据,只能用在父组件的模板。如果父组件的数据要在子组件中使用,则需要子组件定义props。

 


 

二、使用slot分发内容

 1、什么是“内容分发”?

在使用组件时,往往会这样:

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

注意两点:

  1. <app> 组件不知道它会收到什么内容。这是由使用 <app> 的父组件决定的。

  2. <app> 组件很可能有它自己的模版。

为了让组件可以组合,我们需要一种方式来混合父组件的内容与子组件自己的模板。这个过程被称为 内容分发。Vue.js 实现了一个内容分发 API,参照了当前 Web 组件规范草案,使用特殊的 <slot> 元素作为原始内容的插槽。

 

2、单个slot

 1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4  <meta charset="UTF-8"> 5  <title>单个slot</title> 6  <script type="text/javascript" src="../lib/js/vue.js"></script> 7 </head> 8 <body> 9  <div id="demo">10   <h1>我是父组件</h1>11   <my-component>12    <p>这是初始内容1</p>13    <p>这是初始内容2</p>14   </my-component>15  </div>16 17  <template id="myComponent">18   <div>19    <h1>我是子组件的标题</h1>20    <slot>没有分发内容的时候才会显示</slot>21   </div>22  </template>23 24  <script type="text/javascript">25 26   Vue.component('my-component',{27    template:'#myComponent'28   });29 30   var vm = new Vue({31    el:'#demo'32   });33  </script>34 35 </body>36 </html>
View Code

结果:

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

最初在 <slot> 标签中的任何内容都被视为备用内容。备用内容在子组件的作用域内编译,并且只有在宿主元素为空,且没有要插入的内容时才显示备用内容。

  •  其他情况1:删除父组件模板中的内容

     

  •  其他情况2:删除子组件模板里的<slot>

  • 其他情况3:子组件里有多个<slot>

 

(当然,这里有两个匿名<slot>会有警告,应该用特殊的属性 name 来配置如何分发内容。详见第三节 具名slot)

 

 3、具名slot

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

仍然可以有一个匿名 slot,它是默认 slot,作为找不到匹配的内容片段的备用插槽。如果没有默认的 slot,这些找不到匹配的内容片段将被抛弃。

 demo :

 1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4  <meta charset="UTF-8"> 5  <title>具名slot</title> 6  <script type="text/javascript" src="../lib/js/vue.js"></script> 7 </head> 8 <body> 9  <div id="demo">10   <h1>我是父组件</h1>11   <my-component>12    <h1 slot="header">这里可能是一个页面标题</h1>13    <p>主要内容的一个段落。</p>14    <p>另一个主要段落。</p>15    <p slot="footer">这里有一些联系信息</p>16   </my-component>17  </div>18 19  <template id="myComponent">20   <div class="container">21    <header>22     <slot name="header"></slot>23    </header>24    <main>25     <slot>这里是匿名slot</slot>26    </main>27    <footer>28     <slot name="footer"></slot>29    </footer>30   </div>31  </template>32 33  <script type="text/javascript">34 35   Vue.component('my-component',{36    template:'#myComponent'37   });38 39   var vm = new Vue({40    el:'#demo'41   });42  </script>43 </body>44 </html>
View Code

 

 

 4、作用域插槽

2.1.0新增

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

 1   <div class="parent"> 2   <child> 3    <template scope="props"> 4     <span>hello from parent</span> 5     <span>{{ props.text }}</span> 6    </template> 7   </child> 8  </div> 9 10  <template id="myComponent">11   <div class="child">12    <slot text="hello from child">没有分发内容的时候才会显示</slot>13   </div>14  </template>15 16  <script type="text/javascript">17   Vue.component('child',{18    template:'#myComponent'19   });20   21   var vm = new Vue({22    el:'.parent'23   });24  </script>

 

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

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

 

 作用域插槽更具代表性的用例是列表组件,允许组件自定义应该如何渲染列表每一项。作用域插槽也可以是具名的。(线上demo)

 1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4  <meta charset="UTF-8"> 5  <title>作用域插槽-列表</title> 6  <script type="text/javascript" src="../lib/js/vue.js"></script> 7 </head> 8 <body> 9  <div class="parent">10   <my-list :items="items">11    <template slot="item" scope="props">12     <li class="my-fancy-item">{{ props.text }}</li>13    </template>14   </my-list>15  </div>16 17  <template id="myComponent">18   <ul>19    <slot name="item" v-for="item in items" :text="item.text"></slot>20   </ul>21  </template>22 23  <script type="text/javascript">24   Vue.component('my-list',{25    template:'#myComponent',26    data:function () {27     return {28      items:[29       {id:1,text:'列表1'},30       {id:2,text:'列表2'},31       {id:3,text:'列表3'},32       {id:4,text:'列表4'}33      ]34     }35    }36   });37 38   var vm = new Vue({39    el:'.parent',40    data:{41     items:[]42    }43   });44  </script>45 46 </body>47 </html>
View Code

 

 

(这里代码中删除两处对效果并没有什么影响)


 

三、动态组件

 通过使用保留的 <component> 元素,动态地绑定到它的 is 特性,我们让多个组件可以使用同一个挂载点,并动态切换:

var vm = new Vue({ el: '#example', data: { currentView: 'component1' //默认选中的组件 }, components: { component1: { /* ... */ }, component2: { /* ... */ }, component13: { /* ... */ } }})
<component v-bind:is="currentView"> <!-- 组件在 vm.currentview 变化时改变! --></component>

也可以直接绑定到组件对象上:

var Home = { template: '<p>Welcome home!</p>'}var vm = new Vue({ el: '#example', data: { currentView: Home }})

通过具体实例来说明:demo

 1   <div class="container"> 2   <!--导航栏--> 3   <ul class="nav nav-pills"> 4    <li><a href="javascript:void(0)" @click="toggleTab(0)">{{tabText1}}</a></li> 5    <li><a href="javascript:void(0)" @click="toggleTab(1)">{{tabText2}}</a></li> 6    <li><a href="javascript:void(0)" @click="toggleTab(2)">{{tabText3}}</a></li> 7   </ul> 8   <!-- 点击导航后要切换的内容容器 --> 9   <div class="content">10    <!-- 如果把切换出去的组件保留在内存中,可以保留它的状态或避免重新渲染。为此可以添加一个 keep-alive 指令参数 -->11    <keep-alive><component :is="currentView"></component></keep-alive>12   </div>13  </div>14 15  <!-- 点击导航后要切换的内容 -->16  <template id="tab-content1">17   <div>这是第一个选项卡的内容!</div>18  </template>19 20  <template id="tab-content2">21   <div>这是第二个选项卡的内容!</div>22  </template>23 24  <template id="tab-content3">25   <div>这是第三个选项卡的内容!</div>26  </template>27 28  <script type="text/javascript">29   //局部注册组件(选项卡内容)30   var tab1 = {31    template:'#tab-content1'32   };33   var tab2 = {34    template:'#tab-content2'35   };36   var tab3 = {37    template:'#tab-content3'38   };39   40   var vm = new Vue({41    el:'.container',42    data:{43     tabText1:'选项卡1',44     tabText2:'选项卡2',45     tabText3:'选项卡3',46     currentView:tab147    },48    //注册局部组件49    components:{50     tabComponent1:tab1,51     tabComponent2:tab2,52     tabComponent3:tab353    },54    methods:{55     toggleTab:function (i) {56      var arr = ['tabComponent1','tabComponent2','tabComponent3'];57      this.currentView = arr[i];58     }59    }60   61   })62  </script>

如果把切换出去的组件保留在内存中,可以保留它的状态或避免重新渲染。为此可以添加一个 keep-alive 指令参数:

<keep-alive> <component :is="currentView"> <!-- 非活动组件将被缓存! --> </component></keep-alive>

 

四、子组件索引

 尽管有 props 和 events,但是有时仍然需要在 JavaScript 中直接访问子组件。为此可以使用 ref 为子组件指定一个索引 ID。例如:

 1 <div id="parent"> 2   <user-profile ref="profile"></user-profile> 3  </div> 4  5  <script type="text/javascript"> 6   Vue.component('user-profile',{ 7    template:'<p>{{message}}</p>', 8    data:function () { 9     return {10      message:'这里是子组件索引!'11     }12    }13   });14 15   var parent = new Vue({16    el: '#parent'17   });18   // 访问子组件19   var child = parent.$refs.profile;20   console.log(child.$data); //打印子组件的数据

当 ref 和 v-for 一起使用时,ref 是一个数组,包含相应的子组件。

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


 

五、递归组件

组件在它的模板内可以递归地调用自己,不过,只有当它有 name 选项时才可以

name: 'unique-name-of-my-component'

当你利用Vue.component全局注册了一个组件, 全局的ID作为组件的 name 选项,被自动设置.

Vue.component('unique-name-of-my-component', { // ...})

如果你不谨慎, 递归组件可能导致死循环。要确保递归调用有终止条件 (比如递归调用时使用 v-if 并让他最终返回 false)

实例:依次输出123456789

(1)定义一个组件模板,基本标签为<span>0</span>,然后调用该组件。并且将数值加1(如果加1之后不超过10)。注意这些操作一定要放在一个标签内,如下代码中的div,否则会报错。

(2)定义父组件,并且传入初始count值

(3)注册组件,并且定义v-if的成立条件

效果图:

 

 1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4  <meta charset="UTF-8"> 5  <title>递归组件</title> 6  <script type="text/javascript" src="../lib/js/vue.js"></script> 7 </head> 8 <body> 9 <div id="parent">10  <counter-component :count="0"></counter-component>11 </div>12 13 <template id="counterComponent">14  <div>15   <span>{{count}}</span>16   <counter-component :count="count+1" v-if="countNum">{{count}}</counter-component>17  </div>18 </template>19 20 <script type="text/javascript">21  Vue.component('counter-component',{22   name:'counter-component',23   template:'#counterComponent',24   props:['count'],25   computed:{26    countNum:function () {27     return this.count < 10;28    }29   }30  });31 32  var parent = new Vue({33   el: '#parent'34  });35 36 </script>37 </body>38 </html>
View Code

 


六、组件间的循环使用

 假设有两个组件称为 A 和 B,模块系统看到它需要 A,但是首先 A 需要 B,但是 B 需要 A,而 A 需要 B,陷入了一个无限循环,因此不知道到底应该先解决哪个。如下:

当使用Vue.component将这两个组件注册为全局组件的时候,框架会自动为你解决这个矛盾。
然而,如果使用诸如Webpack或者Browserify之类的模块化管理工具来requiring/importing组件的话,就会报错了。

要解决这个问题,我们需要在其中一个组件中 (比如 A) 告诉模块化管理系统,“A 虽然需要 B,但是不需要优先导入 B”

在我们的例子中,我们选择在tree-folder 组件中来告诉模块化管理系统循环引用的组件间的处理优先级,我们知道引起矛盾的子组件是tree-folder-contents,所以我们在beforeCreate 生命周期钩子中去注册它:

beforeCreate: function () { this.$options.components.TreeFolderContents = require('./tree-folder-contents.vue').default}

七、组件综合案例-树形组件

 demo完成效果:

步骤:

1、首先在创建根实例时定义一组数据,在父组件上利用v-for指令循环数据,将每一项数据通过props传递给子组件(列表项li),并将数据的name值在li显示。

 HTML:

 1 <div class="container"> 2  <ul class="list-group"> 3   <tree-node v-for="item in items" :parent_node="item" :key="item.name"></tree-node> 4  </ul> 5 </div> 6 <template id="treeNode"> 7  <li class="list-group-item"> 8   <div> 9    {{parent_node.name}}10   </div>11  </li>12 </template>

JS:

 1 Vue.component('tree-node',{ 2   name:'tree-node', 3   template:'#treeNode', 4   props:['parent_node'] 5  }); 6  7  var vm = new Vue({ 8   el:'.container', 9   data:{10    items:[11     {12      name:'列表1'13     },14     {15      name:'列表2'16     },17     {18      name:'列表3'19     }20    ]21   }22  })

注意:

key 的特殊属性主要用在 Vue的虚拟DOM算法,在新旧nodes对比时辨识VNodes。如果不使用key,Vue会使用一种最大限度减少动态元素并且尽可能的尝试修复/再利用相同类型元素的算法。使用key,它会基于key的变化重新排列元素顺序,并且会移除key不存在的元素。

有相同父元素的子元素必须有独特的key。重复的key会造成渲染错误。最常见的用例是结合 v-for。

<ul> <li v-for="item in items" :key="item.id">...</li></ul>

2、给数据加一项data,用来存放二级列表数据;循环使用组件tree-node,并且将传递给v-for的列表项改为对应二级列表的列表项。

 1 var vm = new Vue({ 2   el:'.container', 3   data:{ 4    items:[ 5     { 6      name:'列表1', 7      data:[ 8       {name:'列表1-1'}, 9       {name:'列表1-2'},10       {name:'列表1-3'}11      ]12     },13     {14      name:'列表2',15      data:[16       {name:'列表2-1'},17       {name:'列表2-2'},18       {name:'列表2-3'}19      ]20     },21     {22      name:'列表3',23      data:[]24     }25    ]26   }27  })
 1 <template id="treeNode"> 2  <li class="list-group-item"> 3   <div> 4    {{parent_node.name}} 5   </div> 6   <ul> 7    <tree-node v-for="child_node in parent_node.data" :parent_node="child_node" :key="child_node.name"></tree-node> 8   </ul> 9  </li>10 </template>

此时效果如下:

3、给一级列表项数据添加open字段,用来显示列表是否展开;

(我默认列表1展开,列表2和列表3不展开)

4、给一级列表添加两个图标(open和close图标),用v-if来判断哪一个图标显示。两个图标的显示条件都是列表项存在第二级列表数据-data。

 1 <template id="treeNode"> 2  <li class="list-group-item"> 3   <div> 4    <span v-if="!parent_node.open && parent_node.data" class="glyphicon glyphicon-folder-close"></span> 5    <span v-if="parent_node.open && parent_node.data" class="glyphicon glyphicon-folder-open"></span> 6    {{parent_node.name}} 7   </div> 8   <ul> 9    <tree-node v-for="child_node in parent_node.data" :parent_node="child_node" :key="child_node.name"></tree-node>10   </ul>11  </li>12 </template>

 

此时效果为:

5、用v-show和v-if给二级列表加上显示条件。当没有data数据的时候,二级列表不存在;当open字段为false的时候,二级列表不显示;

 

 1 <template id="treeNode"> 2   <li class="list-group-item"> 3    <div> 4     <span v-if="!parent_node.open && parent_node.data" class="glyphicon glyphicon-folder-close"></span> 5     <span v-if="parent_node.open && parent_node.data" class="glyphicon glyphicon-folder-open"></span> 6     {{parent_node.name}} 7    </div> 8  9    <ul v-if="parent_node.data" v-show="parent_node.open">10     <tree-node v-for="child_node in parent_node.data" :parent_node="child_node" :key="child_node.name"></tree-node>11    </ul>12 13   </li>14  </template>

 

此时显示效果为:

 

至此demo算是已经完成了,最后一步则是给一级列表加上点击事件。

 

6、给列表项中的div加上点击事件

 


 本文的Demo和源代码已放到GitHub  https://github.com/zwl-jasmine95/Vue_test

如果您觉得本篇内容不错,请点个赞,或在GitHub上加个星星!