Vue.js 学习笔记 第7章 组件详解

 

本篇目录:

7.1 组件与复用

7.2 使用props传递数据

7.3 组件通讯

7.4 使用slot分发内容

7.5 组件高级用法

7.6 其他

7.7 实战:两个常用组件的开发

 

组件(Component)是Vue.js最核心的功能,也是整个框架设计最精彩的地方,当然也是最难掌握的。
本章将带领你由浅入深地学习组件的全部内容,并通过几个实战项目熟练使用Vue组件。

7.1 组件与复用

7.1.1 为什么使用组件

在正式介绍组件前,我们先来看一个简单的场景,如图7-1所示:
Vue.js 学习笔记 第7章 组件详解_第1张图片

图7-1中是一个很常见的聊天界面,有一些标准的控件,比如右上角的关闭按钮、输入框、发送按钮等。

你可能要问了,这有什么难的,不就是几个

吗?
好,那现在需求升级了,这几个控件还有别的地方要用到。
没问题,复制粘贴呗。

那如果输入框要带数据验证,按钮的图标支持自定义呢?
这样用JavaScript封装后一起复制吧。

那等到项目快完结时,产品经理说,所有使用输入框的地方,都要改成支持回车键提交。
好吧,给我一天的事件,我一个一个加上去。

上面的需求虽然有点变态,但却是业务中很常见的,那就是一些控件、JavaScript能力的复用。
没错,Vue.js的组件就是提高重用性的,让代码可重用。
当学习完组件后,上面的问题就可以分分钟搞定了,再也不用害怕铲平经理的奇葩需求。

我们先看一下图7-1中的示例用组件来编写是怎么的,示例代码如下:

 1 <Card style="width:350px;">
 2     <p slot="title">与 xxx 聊天中p>
 3     <a href="#" slot="extra">
 4         <Icon type="android-close" size="18">Icon>
 5     a>
 6     <div style="height:100px;">div>
 7     <div>
 8         <Row :gutter="16">
 9             <i-col span="17">
10                 <i-input v-model="value" placeholder="请输入...">i-input>
11             i-col>
12             <i-col span="4">
13                 <i-button v-model="primary" icon="paper-airplane">发送i-button>
14             i-col>
15         Row>
16     div>
17 Card>

 

是不是很奇怪,有很多我们从来都没有见过的标签,比如等。
而且整段代码除了内联的几个样式外,一句CSS代码也没有,但最终实现的UI就是图7-1的效果。

这些没见过的自定义标签就是组件,每个标签代表一个组件,在任何使用Vue的地方都可以直接使用。
接下来,我们就看看组件的具体用法。

7.1.2 组件用法

回顾一下我们创建Vue实例的方法:

1 var app = new Vue({
2     el: "#app"
3 });

 

组件与之类似,需要注册之后才可以使用。注册有全局注册和局部注册两种方式。
全局注册后,任何Vue实例都可以使用。全局注册示例代码如下:

1 Vue.component("my-component", {
2     // 选项
3 });

 

my-component就是注册的组件自定义标签名称,推荐使用小写加减号分割的形式命名。

要在父实例中使用这个组件,必须要在实例创建前注册。
之后就可以用的形式来使用组件了。
实例代码如下:

 1 <div id="app">
 2     <my-component>my-component>
 3 div>
 4 
 5 <script>
 6     Vue.component("my-component", {
 7         // 选项
 8     });
 9     
10     var app = new Vue({
11         el: "#app"
12     });
13 script>

 

此时打开页面还是空白的,因为我们注册的组件没有任何内容。
在组件选项中添加template就可以显示组件内容了。
实例代码如下:

1 Vue.component("my-component", {
2     template: "
这里是组件的内容
" 3 });

 

渲染后的结果是:

1 <div id="app">
2     <div>这里是组件的内容div>
3 div>

 

template的DOM结构必须被一个元素包含,如果直接写成“这里是组件的内容”,不带

是无法渲染的。

在Vue实例中,使用components选项可以局部注册组件,注册后的组件只有在该实例作用域下有效。
组件也可以使用components选项来注册组件,该组件可以嵌套。
示例代码如下:

 1 <div id="app">
 2     <my-component>my-component>
 3 div>
 4 
 5 <script>
 6     var Child = {
 7         template: "
局部注册组件的内容
" 8 }; 9 10 var app = new Vue({ 11 el: "#app", 12 components: { 13 "my-component": Child 14 } 15 }); 16 script>

 

Vue组件的模板在某些情况下回收到HTML的限制,比如

内规定只允许是
等这些表格元素,所以在内直接使用组件是无效的。
这种情况下,可以使用特殊的is属性来挂载组件,示例代码如下:

 1 <div id="app">
 2     <table>
 3         <tbody is="my-component">tbody>
 4     table>
 5 div>
 6 
 7 <script>
 8     Vue.component("my-component", {
 9         template: "
这里是组件的内容
" 10 }); 11 12 var app = new Vue({ 13 el: "#app" 14 }); 15 script>

 

在渲染时,会被替换为组件的内容。
常见的限制元素还有
      ", 11 methods: { 12 updateValue: function(event) { 13 this.$emit("input", event.target.value); 14 } 15 } 16 }); 17 18 var app = new Vue({ 19 el: "#app", 20 data: { 21 total: 0 22 }, 23 methods: { 24 handleReduce: function() { 25 this.total--; 26 } 27 } 28 }); 29 script>

       

      实现这样一个具有双向绑定的v-model组件要满足下面两个要求:

      • 接收一个value属性。
      • 在有新的value时触发input事件。

      7.3.3 非父子组件通信

      在实际业务中,除了父子组件通信外,还有很多非父子组件通信的场景,非父子组件一般有两种:兄弟组件和跨多级组件。
      为了更加彻底地了解Vue.js 2.x中的通信方法,我们先来看一下在Vue.js 1.x中是如何实现的,这样便于我们了解Vue.js的设计思想。

      在Vue.js 1.x中,除了$emit()方法外,还提供了$dispatch()$broadcast()这两个方法。
      $dispatch()用于向上级派发事件,只要是它的父级(一般或多级以上),都可以在Vue实例的events选项内接收,示例代码如下:

       1 
       2 <div id="app">
       3     {{message}}
       4     <my-component>my-component>
       5 div>
       6 
       7 <script>
       8     Vue.component("my-component", {
       9         template: "",
      10         methods: {
      11             handleDispatch: function() {
      12                 this.$dispatch("on-message", "来自内部组件的数据");
      13             }
      14         }
      15     });
      16     
      17     var app = new Vue({
      18         el: "#app",
      19         data: {
      20             message: ""
      21         },
      22         methods: {
      23             "on-message": function(msg) {
      24                 this.message = msg;
      25             }
      26         }
      27     });
      28 script>

       

      同理,$broadcast()是由上级向下级广播事件的,用法完全一致,只是方向相反。

      这两种方法一旦发出事件后,任何组件都是可以接收到的,就近原则,而且会在第一次接收到后停止冒泡,除非返回true

      这两个方法虽然看起来很好用,但是在Vue.js 2.x中都废弃了,因为基于组件树结构的事件流方式让人难以理解,并且在组件结构扩展的过程中会变得越来越脆弱,并且不能解决兄弟组件通信的问题。

      在Vue.js 2.x中,推荐使用一个空的Vue实例作为中央事件总线(bus),也就是一个中介。
      为了更形象地了解它,我们举一个生活中的例子。

      比如你需要租房子,你可能会找房产中介来等级你的需求,然后中介把你的信息发给满足要求的出租者,出租者再把报价和看房时间告诉中介,由中介再转达给你。
      整个过程中,买家和卖家并没有任何交流,都是通过中间人来传话的。

      或者你最近可能要换房了,你会找房产中介登记你的信息,订阅与你找房需求相关的资讯。
      一旦有符合你的房子出现时,中介会通知你,并传达你房子的具体信息。

      这两个例子中,你和出租者担任的就是两个跨级的组件,而房产中介就是这个中央事件总线(bus)。
      比如下面的示例代码:

       1 <div id="app">
       2     {{message}}
       3     <my-component-a>my-component-a>
       4 div>
       5 
       6 <script>
       7     var bus = new Vue();
       8     
       9     Vue.component("my-component-a", {
      10         template: "",
      11         methods: {
      12             handleEvent: function() {
      13                 bus.$emit("on-message", "来自组件 component-a 的内容");
      14             }
      15         }
      16     });
      17     
      18     var app = new Vue({
      19         el: "#app",
      20         data: {
      21             message: ""
      22         },
      23         mounted: function() {
      24             var _this = this;
      25             // 在实例初始化时,监听来自bus示例的事件
      26             bus.$on("on-message", function(msg) {
      27                 _this.message = msg;
      28             });
      29         }
      30     });
      31 script>

       

      首先创建了一个名为bus的空Vue实例,里面没有任何内容;然后全局定义了组件component-a;最后创建Vue实例app
      app初始化时,也就是在生命周期mounted钩子函数里监听了来自bus的事件on-message
      而在组件component-a中,点击按钮会通过bus把事件on-message发出去,
      此时app就会接受到来自bus的事件,进而在回调里完成自己的业务逻辑。

      这种方法巧妙而清凉地实现了任何组件间的通信,包括父子、兄弟、跨级,而且Vue 1.x和Vue 2.x都适用。
      如果深入使用,可以扩展bus实例,给它添加datamethodscomputed等选项,这些都是可以公用的。
      在业务中,尤其是协同开发时非常有用,因为经常需要共享一些通用的信息。
      比如用户登录的昵称、性别、邮箱等,还有用户的授权token等,只需在初始化时让bus获取一次,任何时间、任何组件就可以从中直接使用了,在单页面富应用(SPA)中会很实用,我们会在进阶篇中逐步介绍这些内容。

      当你的项目比较大,有更多的小伙伴参与开发时,也可以你选择更好的状态管理解决方案vuex,在进阶篇里会详细介绍关于它的用法。

      除了中央事件总线bus外,还有两种方法可以实现组件间通信:父链和子组件索引。

      父链

      在子组件中,使用this.$parent可以直接访问该组件的父实例或组件,父组件也可以通过this.$children访问它所有的子组件,而且可以递归向上或向下无限访问,直到根实例或最内层的组件。
      示例代码如下:

       1 <div id="app">
       2     {{message}}
       3     <my-component-a>my-component-a>
       4 div>
       5 
       6 <script>
       7     Vue.component("my-component-a", {
       8         template: "",
       9         methods: {
      10             handleEvent: function() {
      11                 // 访问到父链后,可以做任何操作,比如直接修改数据
      12                 this.$parent.message = "来自组件 component-a 的内容";
      13             }
      14         }
      15     });
      16     
      17     var app = new Vue({
      18         el: "#app",
      19         data: {
      20             message: ""
      21         }
      22     });
      23 script>

       

      尽管Vue允许这样操作,但在业务中,子组件应该尽可能地避免依赖父组件的数据,更不应该去主动修改它的数据,因为这样使得父子组件紧耦合,只看父组件,很难理解父组件的状态,因为它可能被任意组件修改,理想情况下,只有组件自己能修改它的状态。
      父子组件最好还是通过props$emit()来通信。

      子组件索引

      当子组件较多时,通过this.$children来一一遍历我们需要的一个组件实例是比较困难的,尤其是组件动态渲染时,它们的序列是不固定的。Vue提供了子组件索引的方法,用特殊的属性ref来为子组件指定一个索引名称。
      示例代码如下:

       1 <div id="app">
       2     <button @click="handleRef">通过ref获取子组件实例button>
       3     <component-a ref="comA">component-a>
       4 div>
       5 
       6 <script>
       7     Vue.component("component-a", {
       8         template: "
      子组件
      ", 9 data: function() { 10 return { 11 message: "子组件内容" 12 }; 13 } 14 }); 15 16 var app = new Vue({ 17 el: "#app", 18 methods: { 19 handleRef: function() { 20 // 通过$refs来访问指定的实例 21 var msg = this.$refs.comA.message; 22 console.log(msg); 23 } 24 } 25 }); 26 script>

       

      在父组件模板中,子组件标签上使用ref指定一个名称,并在父组件内通过this.$refs来访问指定名称的子组件。

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

      与Vue 1.x不同的是,Vue 2.x将v-elv-ref合并为了ref,Vue会自动去判断是普通标签还是组件。
      可以尝试补全下面的代码,分别打印出两个ref看看都是什么:

      1 <div id="app">
      2     <p ref="p">内容p>
      3     <child-component ref="child">child-component>
      4 div>

       

      7.4 使用slot分发内容

      7.4.1 什么是slot

      我们先看一个比较常规的网站布局,如图7-3所示。
      Vue.js 学习笔记 第7章 组件详解_第3张图片

      这个网站由一级导航、二级导航、左侧列表、正文以及底部版权信息5个模块组成。
      如果要将它们都组件化,这个结构可能会是:

      1 <app>
      2     <menu-main>menu-main>
      3     <menu-sub>menu-sub>
      4     <div class="container">
      5         <menu-left>menu-left>
      6         <container>container>
      7     div>
      8     <app-footer>app-footer>
      9 app>

       

      当需要让组件组合使用,混合父组件的内容与子组件的模板时,就会用到slot,这个过程叫做内容分发(transclusion)。
      为例,它有两个特点:

      • 组件不知道它的挂载点会有什么内容。挂载点的内容是由的父组件决定的。
      • 组件很可能有它自己的模板。

      props传递数据、events触发事件和slot内容分发就构成了Vue组件的3个API来源,再复杂的组件也是由这3部分构成的。

      7.4.2 作用域

      正式介绍slot前,需要先知道一个概念:编译的作用域。
      比如父组件有如下模板:

      1 <child-component>
      2     {{message}}
      3 child-component>

       

      这里的message就是一个slot
      但是它绑定的是父组件的数据,而不是组件的数据。

      父组件模板的内容是在父组件作用域内编译,子组件模板的内容是在子组件作用域内编译。
      例如下面的代码示例:

       1 <div id="app">
       2     <child-component v-show="showChild">child-component>
       3 div>
       4 
       5 <script>
       6     Vue.component("child-component", {
       7         template: "
      子组件
      " 8 }); 9 var app = new Vue({ 10 el: "#app", 11 data: { 12 showChild: true 13 } 14 }) 15 script>

       

      这里的状态showChild绑定的是父组件的数据,如果想在子组件上绑定,那应该是:

       1 <div id="app">
       2     <child-component>child-component>
       3 div>
       4 
       5 <script>
       6     Vue.component("child-component", {
       7         template: "
      子组件
      ", 8 data: function() { 9 return { 10 showChild: true 11 }; 12 } 13 }); 14 15 var app = new Vue({ 16 el: "#app" 17 }) 18 script>

       

      因此,slot分发的内容,作用域是在父组件上的。

      7.4.3 solt用法

      单个Slot

      在子组件内使用特殊的元素就可以为这个子组件开启一个slot(插槽),在父组件模板里,插入子组件标签内的所有内容将替代子组件的标签及它的内容。
      示例代码如下:

       1 <div id="app">
       2     <child-component>
       3         <p>分发的内容p>
       4         <p>更多分发的内容p>
       5     child-component>
       6 div>
       7 
       8 <template id="template">
       9     <div>
      10         <slot>
      11             <p>如果父组件没有插入内容,我将作为默认出现p>
      12         slot>
      13     div>
      14 template>
      15 
      16 <script>
      17     Vue.component("child-component", {
      18         template: "#template"
      19     });
      20     var app = new Vue({
      21         el: "#app"
      22     })
      23 script>

       

      子组件child-component的模板内定义了一个元素,并且用一个

      作为默认的内容,在父组件没有使用slot时,会渲染这段默认的文本;如果写入了slot,那就回替换整个
      所以上例渲染后的结果为:

      1 <div id="app">
      2     <div>
      3         <p>分发的内容p>
      4         <p>更多分发的内容p>
      5     div>
      6 div>

       

      提示:
      注意,子组件内的备用内容,它的作用域是子组件本身。

      具名Slot

      元素指定一个name后可以分发多个内容,具名Slot可以与单个Slot共存。
      例如下面的示例:

       1 <div id="app">
       2     <child-component>
       3         <h2 slot="header">标题h2>
       4         <p>正文内容p>
       5         <p>更多的正文内容p>
       6         <div slot="footer">底部信息div>
       7     child-component>
       8 div>
       9 
      10 <template id="template">
      11     <div class="container">
      12         <div class="header">
      13             <slot name="header">slot>
      14         div>
      15         <div class="main">
      16             <slot>slot>
      17         div>
      18         <div class="footer">
      19             <slot name="footer">slot>
      20         div>
      21     div>
      22 template>
      23 
      24 <script>
      25     Vue.component("child-component", {
      26         template: "#template"
      27     });
      28     var app = new Vue({
      29         el: "#app"
      30     })
      31 script>

       

      子组件内声明了3个元素,其中在

      内的没有使用name特性,它将作为默认slot出现,父组件没有使用slot特性的元素与内容都将会出现在这里。

      如果没有指定默认的匿名slot,父组件内多余的内容片断都将被抛弃。

      上例最终渲染后的结果为:

       1 <div id="app">
       2     <div class="container">
       3         <div class="header">
       4             <h2>标题h2>
       5         div>
       6         <div class="main">
       7             <p>正文内容p>
       8             <p>更多的正文内容p>
       9         div>
      10         <div class="footer">
      11             <div>底部信息div>
      12         div>
      13     div>
      14 div>

       

      在组合使用组件时,内容分发API至关重要。

      7.4.4 作用域插槽

      作用域插槽是一种特殊的slot,使用一个可以复用的模板替换已渲染元素。
      概念比较难理解,我们先看一个简单的示例来了解它的基本用法。
      示例代码如下:

       1 <div id="app">
       2     <child-component>
       3         <template scope="props">
       4             <p>来自父组件的内容p>
       5             <p>{{props.msg}}p>
       6         template>
       7     child-component>
       8 div>
       9 
      10 <template id="template">
      11     <div class="container">
      12         <slot msg="来自子组件的内容">slot>
      13     div>
      14 template>
      15 
      16 <script>
      17     Vue.component("child-component", {
      18         template: "#template"
      19     });
      20     var app = new Vue({
      21         el: "#app"
      22     })
      23 script>

       

      观察子组件的模板,在元素上有一个类似props传递数据给组件的写法msg="xxx",数据传到了插槽。
      父组件中使用了