第5章 组件 在前端应用程序开发中,如果所有的实例都写在一起,则必然会导致代码既长又不好理解。组件的出现刚好解决了这一问题,它是带有名字的可复用实例,不仅可以重复使用,还可以扩展。组件(Component)是Vue最核心的功能,也是整个框架设计最精彩的地方,当然也是比较难掌握的。每个开发者都想在软件开发过程中使用之前写好的代码,但又担心引入这段代码对现有的程序产生影响。Web Components的出现提供了一种新的思路,可以自定义tag标签,并拥有自身的模板、样式和交互。另外,Vue 3.x新增了组合API,它是一组附加的、基于函数的API,允许灵活地组合组件逻辑。 5.1什么是组件 在正式介绍组件前,先看一个 Vue 组件的简单示例,大家先感受一下,代码如下: //定义一个名为 button-counter 的组件 const vm=Vue.createApp({}); vm.component('button-counter', { data() { return { count: 0 } }, template: '' }) vm.mount('#app'); 组件是可复用的Vue实例,并且带有一个名字,在这个示例中组件是。x把这个组件作为自定义标签来使用,代码如下:
完整的组件使用示例代码如例51所示。 【例51】自定义组件 //第5章/自定义组件.html
在浏览器中的显示效果如图51所示。 图51自定义组件 这些类似于自定义的标签就是组件,每个标签代表一个组件,这样就可以将组件进行任意次数的复用。 Web的组件其实就是页面组成的一部分,好比是计算机中的每个元器件(如硬盘、键盘、鼠标等),它有一个独立的逻辑和功能或界面,同时又能根据规定的接口规则进行相互融合,从而变成一个完整的应用。 Web页面就是由一个个类似这样的部分组成的,例如导航、列表、弹窗、下拉菜单等。页面只不过是这些组件的容器,组件自由组合形成功能完整的界面,当不需要某个组件或者想要替换某个组件时,可以随时进行替换和删除,而不影响整个应用的运行。 前端组件化的核心思路就是将一个巨大复杂的逻辑和功能分成粒度合理的小逻辑和功能。使用组件的好处如下: (1) 提高开发效率。 (2) 方便重复使用。 (3) 简化调试步骤。 (4) 提升整个项目的可维护性。 (5) 便于协同开发。 组件是Vue.js最强大的功能之一。组件可以扩展HTML元素,封装可重用的代码。在较高层面上,组件是自定义元素,Vue.js的编译器为它添加特殊功能。在有些情况下,组件也可以采用原生HTML元素的形式,以is特性扩展。 组件系统让可以用独立可复用的小组件来构建大型应用,几乎任意类型的应用界面都可以抽象为一棵组件树,如图52所示。 图52Vue组件树 5.2组件的基本使用 为了能在模板中使用,这些组件必须先注册以便 Vue 能够识别。这里有两种组件的注册类型: 全局注册和局部注册。 5.2.1全局注册 全局注册组件使用应用程序实例的component()方法来注册组件。全局注册有以下两种方式。 1. 第1种方式 注册全局组件,代码如下: const vm=Vue.createApp({}) vm.component('my-component', { //选项 }) mycomponent就是注册的组件自定义标签名称,推荐使用小写加分隔符的形式命名。因为组件最后会被解析成自定义的HTML代码,所以可以直接在HTML中使用组件名称作为标签使用,如例52所示。 【例52】全局注册组件方式一 //第5章/全局注册组件方式一.html Document
在浏览器中的显示效果如图53所示。 图53全局注册组件方式一 注意: (1) template的DOM结构必须被一个而且是唯一的根元素包含,如果直接引用,则不被
包裹是无法被渲染的。 (2) 模板(template)声明了数据和最终展现给用户的DOM之间的映射关系。 除了template选项外,组件中还可以有其他的选项,例如data、computed、methods等,代码如下:
Vue组件中data为函数的原因。data为函数,通过return返回对象的复制,使每个实例都有自己独立的对象,实例之间可以互不影响地改变data属性值。 2. 第2种方式 将模板字符串直接定义到script标签中,代码如下: 同时,使用 component()将其定义在组件中,代码如下: 完整示例代码如例53所示。 【例53】全局注册组件方式二 //第5章/全局注册组件方式二.html Document
在浏览器中的显示效果如图54所示。 图54全局注册组件方式二 5.2.2局部注册 如果不需要全局注册,或者只想在一个Vue实例中使用,则可以使用局部注册的方式注册组件。在Vue实例中,可以通过对象的components属性实现局部注册,如例54所示。 【例54】局部注册组件 //第5章/局部注册组件.html Document
在浏览器中的显示效果如图55所示。 图55局部注册组件 可以使用flag标识符结合vif和velse切换组件,如例55所示。 【例55】使用标识符切换组件 //第5章/使用标识符切换组件.html Document
在浏览器中的显示效果如图56所示。 图56使用标识符切换组件效果 5.3使用prop向子组件传递数据 组件是当作元素来使用的,而元素一般是有属性的,同样组件也可以有属性。在使用组件时,给元素设置属性,组件内部如何接受呢?首先需要在组件代码中注册一些自定义的属性,称为prop,这些prop是在组件props选项中定义的,之后在使用组件时,就可以把这些prop的名字作为元素的属性来使用,通过属性向组件传递数据,这些数据将作为组件实例的属性被使用。 5.3.1prop的基本用法 以上章节使用组件主要是将组件模板的内容进行复用,代码如下:
但是组件中更重要的是组件间进行通信,选项props是组件中非常重要的一个选项,起到父子组件间桥梁的作用。 以下是组件选项props常用的几种传值方式。 1. 静态props 组件实例的作用域是孤立的,这意味着不能(也不应该)在子组件的模板内直接引用父组件的数据,示例代码如下:
在以上代码中子组件是接收不到父组件message数据的。 如果要想让子组件使用父组件的数据,则需要通过子组件的props选项实现。子组件要显式地用props选项声明它期待获得的数据,如例56所示。 【例56】子组件接受父组件的数据 //第5章/子组件接受父组件的数据.html
在浏览器中的显示效果如图57所示。 图57子组件接受父组件的数据效果 由于HTML特性是不区分大小写的,因此当使用的不是字符串模板,而是驼峰式命名的props时需要转换为相对应的kebabcase(短横线隔开式) 命名,示例代码如下:
2. 动态props 在模板中,有时传递的数据并不一定是固定的,而是要动态地将父组件的数据绑定到子模板的props,与绑定到任何普通的HTML特性相类似,即使用 vbind。当父组件的数据变化时,该变化也会传递给子组件,如例57所示。 【例57】动态props //第5章/动态props.html
在文本框中输入信息,子组件就会动态地接收到父组件的数据。如在文本框中输入“贝西奇谈”,组件即可接收到该数据。在浏览器中的显示效果如图58所示。 图58动态接收父组件数据效果图 这里使用vmodel绑定了父组件数据parentMessage,当在输入框中输入数据时,子组件接收的props: ['message']也会实时响应,并更新组件模板。 对于初学者常犯的一个错误,如果在父组件中直接传递数字、布尔值、数组、对象,则所传递的值均为字符串; 如果想传递一个实际的值,则需要使用vbind,从而让它的值被当作JavaScript表达式进行计算,示例代码如下:

如果不使用vbind指令,页面显示结果是字符串“1+1”。如果使用vbind指令,则会当作JavaScript表达式计算结果,即数字2。 3. 多值传递 如果组件需要传递多个值,则可以定义多个prop属性,如例58所示。 【例58】传递多个值 //第5章/传递多个值.html Document
在浏览器中的显示效果如图59所示。 图59传递多个值 5.3.2prop验证 当开发一个可复用的组件时,父组件希望通过prop属性传递的数据类型符合要求。例如组件定义一个prop属性是一个对象类型,结果父组件传递的是一个字符串的值,这明显不合适。Vue提供了prop属性的验证规则,在定义props选项时,使用一个带验证需求的对象来代替之前使用的字符串组(props: ['name','price','author'])。 当组件的prop指定验证要求后,如果有一个需求没有被满足,则Vue会在浏览器控制台中发出警告,示例代码如下: vm.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 } } } }) 验证的type类型可以是: String、Number、Boolean、Function、Object、Array等,type 也可以是一个自定义构造器函数,使用 instanceof 检测。 当prop验证失败时,Vue会抛出警告(如果使用的是开发版本)。由于prop会在组件实例创建之前进行校验,所以在default或validator函数里,诸如data、computed或methods等实例属性还无法使用。 【例59】prop 验证 //第5章/prop 验证.html
以上示例中校验的是数字,当传入数字123时,无警告提示。当传入字符串"123"时,控制台会发出警告,如图510所示。 图510prop验证失败时的警告信息 5.3.3单项数据流 所有的prop都使其父子prop之间形成了一个单向下行绑定。父级prop的更新会向下流动到子组件中,但是反过来则不行。之所以这样设计,是因为要尽可能地将父子组件解耦,避免子组件无意中修改了父组件的状态。 另外,每次父级组件发生变更时,子组件中所有的prop都将被刷新为最新的值。这意味着不应该在一个子组件的内部改变prop。如果这样做了,则Vue会在浏览器的控制台中发出警告,如例510所示。 【例510】单项数据传递 //第5章/单项数据传递.html
在浏览器中的显示效果如图511所示。 图511单项数据传递页面显示效果 当父组件数据变化时,子组件数据会实时地响应数据,而当子组件数据变化时,父组件数据不变,并在控制台显示警告,如图512所示。 图512更改子组件时的警告信息 注意: JavaScript中对象和数组是引用类型,指向同一个内存空间,如果 prop 是一个对象或数组,则在子组件内部改变它时会影响父组件的状态。 有两种情况可能需要改变组件的prop属性。 第1种情况是定义一个prop属性,以方便父组件传递初始值,在子组件内将这个prop作为一个本地的prop数据来使用。遇到这种情况,解决办法是在本地data选项中定义一个属性,然后将prop属性值作为其初始值,后续操作只访问这个data属性,示例代码如下: props: ['initData'], data: function () { return { counter: this.initData } } 但定义的局部变量counter只能接收initData的初始值,当父组件要传递的值发生变化时,counter无法接收到最新值,如例511所示。 【例511】改变组件的prop属性(1) //第5章//改变组件的prop属性(1).html Title
以上示例除初始值外,父组件更改的值无法更新到子组件中,如图513所示。 图513改变组件的prop属性(1) 第2种情况是prop属性接收到数据后需要转换后使用,这种情况可以使用计算属性来解决此问题,示例代码如下: props: ['size'], computed: { normalizedSize: function () { return this.size.trim().toLowerCase() } } 但是,由于是计算属性,所以只能显示值,而不能设置值,如例512所示。 【例512】改变组件的prop属性(2) //第5章//改变组件的prop属性(2).html Title
以上示例由于子组件使用的是计算属性,所以子组件的数据无法手动修改,只能通过父组件数据更改来实时获取,如图514所示。 图514改变组件的prop属性(2) 综上方案,更加稳妥的方法是使用变量储存prop的初始值,并使用watch来监听prop的值的变化。当发生变化时,更新变量的值,代码如下:
5.4子组件向父组件传递数据 在Vue组件通信中其中最常见的通信方式就是父子组件之中的通信,而父子组件的设定方式在不同情况下又各有不同,如图515所示。父组件传递数据给子组件使用,当遇到业务逻辑操作时子组件触发父组件的自定义事件。 图515组件通信 作为一个Vue初学者不得不了解的就是组件间的数据通信(暂且不谈Vuex,后面章节会讲到)。通信方式根据组件之间的关系有不同之处。组件关系有3种: 父→子、子→父、非父子。 5.3节已讲解,父组件向子组件通信是通过props传递数据的,就好像方法的传参一样,父组件调用子组件并传入数据,子组件接收到父组件传递的数据后进行验证使用。 那么子组件如何向父组件传递数据呢?具体可看下面的讲解。 5.4.1自定义事件 当子组件需要向父组件传递数据时,就要用到自定义事件。von指令除了可以监听DOM事件外,还可以用于组件之间的自定义事件。 在子组件中用$emit()来触发事件以便将内部的数据报告给父组件,如例513所示。 【例513】子组件向父组件传递数据 //第5章//子组件向父组件传递数据.html
在浏览器中的显示效果如图516所示。 图516子组件向父组件传递数据效果 接下来解析上面示例中的代码。 (1) 子组件在自己的方法中将自定义事件及需要发出的数据通过以下代码发送出去: this.$emit('myclick','这是我暴露出去的数据1', '这是我暴露出去的数据2') 第1个参数是自定义事件的名字。 后面的参数是依次想要发送出去的数据。 (2) 父组件利用von为事件绑定处理器,代码如下: 这样,在Vue实例的methods方法中就可以调用传进来的参数了。 5.4.2sync修饰符 在有些情况下,可能需要对一个prop属性进行“双向绑定”,但是真正的双向绑定会带来维护上的问题,因为子组件可以变更父组件,并且父组件和子组件都没有明显的变更来源。Vue推荐update.myPropName模式触发事件实现,如例514所示。 【例514】设计购物的数量 其中子组件中的代码如下: 在这个子组件中有一个prop属性value,在按钮的click事件处理器中,调用$emit()方法触发update:value事件,并将加1后的计数值作为事件的附加参数。 在父组件中,使用von指令监听update:value事件,这样就可以接收到子组件传来的数据,然后使用vbing指令绑定子组件的prop属性value,这样就可以给子组件传递父组件的数据了,从而实现了双向数据绑定。父组件中的代码如下:
父组件:购买{{counter}}件
其中,$event是自定义事件的附加参数。 完整的示例代码如下: //第5章/设计购物的数量.html
父组件:购买{{counter}}件
在浏览器中运行程序,单击5次“增加”按钮,可以看到父组件和子组件中的购买数量是同步变化的,结果如图517所示。 图517同步更新父组件和子组件的数据 为了方便起见,Vue为prop属性的“双向绑定”提供了一个缩写,即.sync修饰符,修改上面示例的的代码如下: 注意,带有.sync修饰符的vbind不能和表达式一起使用,bind:title.sync="doc.title+'!'"是无效的,代码如下: v-bind:value.sync="doc.title+'!'" 上面的代码是无效的,取而代之的是,只提供想要绑定的属性名,类似于vbind。 当用一个对象同时设置多个prop属性时,也可以将.sync修饰符和vbind配合使用: 这样会把doc对象中的每个属性都作为一个独立的prop传进去,然后各自添加,用于更新von监听器。 5.5内容分发 在实际项目开发中,时常会把父组件的内容与子组件自己的模板混合起来使用,而这样的一个过程在Vue中被称为内容分发。也常常被称为slot(插槽),其主要参照了当前Web Components规范草案,使用特殊的元素作为原始内容的插槽。 5.5.1基础用法 由于slot是一块模板,因此对于任何一个组件,从模板种类的角度来分,其实都可分为非插槽模板和插槽模板,其中非插槽模板指的是HTML模板(也就是由HTML的一些元素构成的,例如div、span等),其显示与否及怎么显示完全由插件自身控制,但插槽模板(也就是slot)是一个空壳子,它显示与否及怎么显示完全是由父组件来控制的。不过,插槽显示的位置由子组件自身决定,slot写在组件template的哪块,父组件传过来的模板将来就显示在哪块。 一般定义子组件的代码如下: //省略部分代码
123456
以上代码在浏览器页面的显示结果为“这是子组件内容”,而123456内容并不会显示。 注意: 虽然标签被子组件的child标签所包含,但由于它不在子组件的template属性中,因此不属于子组件。 如果要想标签的内容被显示,则需要在template中添加插槽标签,代码如下: //省略部分代码
123456
以上代码在浏览器页面的显示结果为“123456这是子组件内容”。 我们分步解析内容分发,现在看一个架空的例子,帮助我们理解刚刚说过的严谨而难懂的定义。假设有一个组件,名为mycomponent,其使用上下文,代码如下:

hi,slots

再假设此组件的模板如下:
那么注入后的组件HTML相当于:

hi,slots

标签会把组件使用上下文的内容注入此标签所占据的位置上,可以把标签理解为占位。组件分发的概念简单而强大,因为它意味着对一个隔离的组件除了通过属性、事件交互之外,还可以注入内容,如例515所示。 【例515】插槽 //第5章/插槽.html

实力打造大前端时代,走在时代的前端

《剑指大前端全栈工程师》

在浏览器中的显示效果如图518所示。 图518插槽应用效果图 一个组件如果需要外部传数据,如数字、字符串等,则可以使用property; 如果需要传入JavaScript表达式或对象,则可以使用事件; 如果希望传入的是HTML标签,则使用内容分发就再好不过了,所以尽管内容分发这个概念看起来极为复杂,但实际上可以简化理解为把HTML标签传入组件的一种方法,所以归根结底,内容分发是一种为组件传递参数的方法。 5.5.2编译作用域 在深入讲解内容分发 API 之前,先明确内容在哪个作用域里编译。假定模板如下: {{ message }} 这里的message应该是绑定到父组件的数据,还是绑定到子组件的数据?答案是message就是一个slot,但它绑定的是父组件的数据,而不是组件的数据。 组件作用域简单地说就是父组件模板的内容在父组件作用域内编译,子组件模板的内容在子组件作用域内编译,如例516所示。 【例516】编译作用域 //第5章/编译作用域.html
由于这里someChildProperty绑定的是父组件的数据,所以是无效的,获取不到数据,如图519所示。 图519编译作用域 如果想获取数据,则someChildProperty需绑定在子组件上,修改后的代码如下:
这样便可获取数据,因此,slot分发的内容是在父作用域内编译的。 5.5.3默认内容 如果要父组件在子组件中插入内容,则必须在子组件中声明slot 标签 ; 如果子组件模板不包含插口,则父组件的内容将会被丢弃,如例517所示。 【例517】默认内容 //第5章/默认内容.html

首页

新闻

军事

手机

浏览器页面的显示结果为index。所有子组件中的内容都不会被显示,即被丢弃。如果要想父组件在子组件中插入内容,则必须在子组件中声明slot 标签,更改后的示例代码如下: 在浏览器中的显示效果如图520所示。 图520默认内容效果图 5.5.4命名插槽 在组件开发中,有时需要使用多个插槽。元素有一个特殊的属性name,它用来命名插槽,因此,可以定义多个名字不同的插槽。如例518所示。 具体使用方法如下: (1) 父组件在命名插槽提供内容时,可以在一个