组件模块化
简介
如果我们将一个页面中所有的处理逻辑全部放在一起,处理起来就会变得非常复杂,而且不利于后续的管理以及扩展。但如果,我们讲一个页面拆分成一个个小的功能块,每个功能块完成属于自己这部分独立的功能,那么之后整个页面的管理和维护就变得非常容易了。
组件化是Vue.js中的重要思想,它提供了一种抽象,让我们可以开发出一个个独立可复用的小组件来构造我们的应用。任何的应用都会被抽象成一颗组件树

组件化思想的应用:
- 尽可能将页面拆分成一个个小的、可复用的组件。
- 代码更方便组织和管理,扩展性更强。
组件的使用
组件的使用分成三个步骤:
- 调用Vue.extend()方法创建组件构造器
- 调用Vue.component()方法注册组件
- 在Vue实例的作用范围内使用组件
// HTML
<div id="app">
<!--步骤三:Vue实例的作用范围内使用组件-->
<my-cpn></my-cpn>
</div>
<!--作用范围外,无法渲染-->
<my-cpn></my-cpn>
<script>
<!--步骤一:创建组件构造器-->
<!--传入template代表我们自定义组件的模板。-->
<!--这种写法在Vue2.x的文档中几乎已经看不到了-->
const myComponent = Vue.extend({
template: `
<div>
<h2>组件标题</h2>
</div>
`
});
<!--步骤二:注册组件,并且定义组件标签的名称-->
<!--传递两个参数:1、注册组件的标签名 2、组件构造器-->
Vue.component('my-cpn', myComponent);
const app = new Vue({
el: '#app'
})
</script>
全局组件和局部组件
当我们通过调用**Vue.component()**注册组件时,组件的注册时全局的。这意味着该组件可以在任意Vue实例下使用。
如果我们注册的组件时挂载在某个实例中,那么就是一个局部组件。
<div id="app">
<my-cpn></my-cpn> // 注册在#app下的局部组件,可以渲染
<my-cpn1></my-cpn1> //全局组件,可以渲染
</div>
<div id="app1">
<my-cpn></my-cpn> // 无法渲染
<my-cpn1></my-cpn1> // 可以渲染
</div>
<script>
const myComponent = Vue.extend({
template: `
<div>
<h2>组件标题</h2>
</div>
`
});
<!--全局注册-->
Vue.component('my-cpn1', myComponent);
const app = new Vue({
el: '#app',
components: {
'my-cpn': myComponent
}
})
const app1 = new Vue({
el: '#app1'
})
</script>
父组件和子组件
组件和组件之间存在层级关系,其中一种非常重要的关系就是父子组件的关系。
<div id="app">
<parent-cpn></parent-cpn>
<!--错误用法,子组件标签只能在父组件中被识别-->
<child-cpn></child-cpn>
</div>
<script>
const parentComponent = Vue.extend({
template: `
<div>
<h2>父组件标题</h2>
<!--当子组件注册到父组件的components时,Vue会编译好父组件的模块,将子组件标签替换为子组件的模板内容-->
<child-cpn></child-cpn>
</div>
`,
components: {
'child-cpn': childComponent
}
});
const childComponent = Vue.extend({
template: `
<div>
<h2>父组件标题</h2>
</div>
`
});
const app = new Vue({
el: '#app',
components: {
'parent-cpn': parentComponent
}
})
</script>
注册语法糖
Vue为了简化祖册组件的过程,提供了注册的语法糖,省去了调用VUe.extend()的步骤,而是直接使用一个对象来代替。
<!--语法糖注册全局组件和局部组件-->
<!--全局组件-->
Vue.component('myCpn', {
template:`
<div>
<h2>父组件标题</h2>
</div>
`
})
<!--局部组件-->
const app = new Vue({
el: "#app",
components: {
'my-cpn': {
template: `
<div>
<h2>父组件标题</h2>
</div>
`
}
}
})
模板的分离写法
通过语法糖简化了Vue组件的注册过程,但是也导致了template模块写法较为麻烦. Vue提供了两种方案来将其中的HTML分离出来,然后再挂载到对应的组件上,这样解构会变得非常清晰.
- 使用<script>标签
- 使用<template>标签
<div id="app">
<my-cpn></my-cpn>
</div>
<!--方案一: 使用<script>标签-->
<script type="text/x-template" id="myCpn">
<div>
<h2>组件标题</h2>
</div>
</script>
<!--方案二: 使用<template>标签-->
<template id="myCpn">
<div>
<h2>组件标题</h2>
</div>
</template>
<script>
const app = new Vue({
el: '#app',
components: {
'my-cpn': {
template: '#myCpn'
}
}
})
</script>
组件中的数据访问
组件时一个单独功能模块的封装,这个模块有属于自己的HTML模板,也应该有属于自己的数据data.组件不能直接访问Vue实例中的data.
组件对象也有一个data属性,只是这个data属性必须时一个函数,而且这个函数返回一个对象,对象内部保存着数据.
原因: 首先,如果不是一个函数,Vue直接会报错;其次,原因是在于Vue让每个组件对象都返回一个新的对象,因为如果是同一个对象的,组件在多次使用后会相互影响.
<div id="app">
<my-cpn></my-cpn>
</div>
<template id="myCpn">
<div>
<h2>{{message}}</h2>
</div>
</template>
<script>
const app = new Vue({
el: '#app',
components: {
'my-cpn': {
template: '#myCpn',
data() {
return {
message: 'hello world'
}
}
}
}
})
</script>

父子组件的通信
父子组件间的通信:
- 通过props向子组件传递数组
- 通过时间向父组件发消息
父传子——props基本用法
在组件中,使用选项props来声明需要从父级接收到的数据.
props的值有两种方式:
- 字符串数组,数组中的字符串就是传递时的名称
- 对象,对象可以设置传递时的类型,也可以设置默认值等.
字符串数组用法
<div id="app">
<my-cpn :message="pMassage"></my-cpn>
</div>
<template id="myCpn">
<div>
<h2>{{message}}</h2> // hello world
</div>
</template>
<script>
const app = new Vue({
el: '#app',
data: {
pMassage: 'hello world'
}
components: {
'my-cpn': {
template: '#myCpn',
props: ['message']
}
}
})
</script>
对象用法
类型验证支持的类型:
- String
- Number
- Boolean
- Array
- Object
- Date
- Function
- Symbol
function Person (fName, lName) {
this.firstName = fName;
this.lastName = lName;
}
Vue.component('my-cpn', {
props: {
<!--基础的类型检查(null匹配任何类型)-->
propA: Number,
author: Person,
<!--多个可能的类型-->
propB: [Number, String]
<!--必填的字符串-->
propC: {
type: String,
required: true
},
<!--带有默认值的数字-->
propD: {
type: Number,
defalut: 100
},
<!--带有默认值的对象-->
propE: {
type: Object,
default() {
return {message: 'hello'}
}
},
<!--自定义验证函数-->
propF: {
validator(value) {
return ['success', 'warning', 'danger'].indexof(value) !== -1;
}
}
}
})
子传父——自定义事件
当子组件需要向父组件传递数据时,需要自定义事件,可以通过v-on监听组件间的自定义事件。
自定义事件的流程:
- 在子组件中,通过$emit()来触发事件。
- 在父组件中,通过v-on来监听子组件事件。
<div id="app">
<child-cpn @increment="changeTotal" @decrement="changeTotal"></child-cpn>
<h2>{{total}}</h2>
</div>
<template id="childCpn">
<div>
<button @click = "increment">+1</button>
<button @click = "decrement">-1</button>
</div>
</template>
<script>
const app = new Vue({
el: '#app',
data: {
total: 0
},
methods: {
changeTotal(value) {
this.total = value;
}
},
components: {
'child-cpn': {
template: '#childCpn',
data() {
counter: 0
},
methods: {
increment() {
this.counter++;
this.$emit('increment', this.counter);
},
decrement() {
this.counter--;
this.$emit('decrement', this.counter);
}
}
}
}
})
</script>
父子组件的访问
有时候我们需要父组件直接访问子组件,子组件直接访问父组件,或者子组件访问根组件。
父组件访问子组件:使用$children或$refs
子组件访问父组件:使用$parent
子组件访问根组件:使用$root
父组件访问子组件
$children
this.$children是一个数组类型,它包含所有子组件对象.
缺陷: 通过$children访问子组件时,访问其中的子组件必须通过索引值,但是当子组件过多时,往往不能确定它的索引值,甚至可能发生变化.
<div id="app">
<parent-cpn></parent-cpn>
</div>
<!--父组件template-->
<template id="parentCpn">
<child-cpn1></child-cpn1>
<child-cpn2></child-cpn2>
<button @click="showChildCpn">显示所有子组件信息</button>
</template>
<!--子组件1 template-->
<template id="childCpn1">
<h2>我是子组件1</h2>
</template>
<!--子组件2 template-->
<template id="childCpn2">
<h2>我是子组件2</h2>
</template>
<script>
Vue.component('parent-cpn', {
template: '#parentCpn',
methods: {
showChildCpn() {
console.log(this.$children); // [VueComponent, VueConponent]
}
},
components: {
'child-cpn1': '#childCpn1',
'child-cpn2': '#childCpn2'
}
})
const app = new Vue({
el: '#app'
})
</script>
$refs(推荐使用)
使用:
- $refs和ref指令通常是一起使用的.
- 通过ref给某一个子组件绑定一个特定的ID
- 通过this.$ref.ID就可以访问到该组件了
<child-cpn ref="child1"></child-cpn>
<child-cpn ref="child2"></child-cpn>
<button @click="showRefsCpn">通过refs访问子组件</button>
showRefsCpn() {
console.log(this.$refs.child1);
console.log(this.$refs.child2);
}
子组件访问父组件($parent)
注意事项:
- 尽管在Vue开发中,我们允许通过$parent来访问父组件,但是在真是开发中尽量不要这么做.
- 子组件应该尽量避免直接访问父组件的数据,因为这样耦合度太高了.
- 如果我们将子组件放在另外一个组件之内,很可能该父组件没有对应的属性,往往会引起问题.
- 另外,更不好做的事通过$parent直接修改父组件的状态,那么父组件中的状态将变得飘忽不定,不利于调试和维护.
<div id="app">
<parent-cpn></parent-cpn>
</div>
<!--父组件template-->
<template id="parentCpn">
<child-cpn1></child-cpn>
</template>
<!--子组件1 template-->
<template id="childCpn">
<button @click="showParent">显示父组件信息</button>
</template>
<script>
Vue.component('parent-cpn', {
template: '#parentCpn',
data() {
return {}
},
components: {
'child-cpn': '#childCpn',
methods: {
showParent() {
console.log(this.$parent); //访问父组件
console.log(this.$root); //访问根组件
}
}
}
})
const app = new Vue({
el: '#app'
})
</script>
编译作用域
父组件模板的所有东西都会在父级作用域内编译;子组件模板的所有东西都会在子级作用域内编译.
<div id="app">
<!--渲染成功-->
<parent-cpn v-show="isShow"></parent-cpn>
</div>
<template id="parentCpn">
<h2>显示组件内容</h2>
</template>
<script>
Vue.component('parent-cpn', {
template: '#parentCpn',
data() {
return {
isShow: false
}
}
})
const app = new Vue({
el: '#app',
data: {
isShow: true
}
})
插槽slot
组件的插槽可以让我们封装的组件更加具有扩展性.
最好的封装方式就是将共性抽取到组件中,将不同暴露为插槽,由使用者根据自己的需求决定插槽中插入什么内容.
基本使用
<div id="app">
<!--无替换元素,显示插槽默认内容-->
<my-cpn></my-cpn>
<!--替换插槽的内容-->
<my-cpn>
<h2>我是替换内容</h2>
<p>我也是替换内容</p>
</my-cpn>
</div>
<template id="myCpn">
<div>
<slot>我是一个插槽中的默认内容</slot>
</div>
</template>
<script>
Vue.component('my-cpn', {
template: '#myCpn'
})
let app = new Vue({
el: '#app'
})
</script>
具名插槽slot
当子组件的功能复杂时,子组件的插槽可能并非一个,这时候我们就需要使用具名插槽(给slot元素一个name属性)来对插槽进行区分,从而能在指定位置替换内容.
<div id="app">
<!--无替换元素,显示插槽默认内容-->
<my-cpn></my-cpn>
<!--替换无名插槽的内容-->
<my-cpn>
<h2>我是替换内容</h2>
<p>我也是替换内容</p>
</my-cpn>
<!--替换指定插槽的内容-->
<my-cpn>
<h2 slot="left">替换左边插槽</h2>
</my-cpn>
<my-cpn>
<h2 slot="left">替换左边插槽</h2>
<h2 slot="mid">替换中间插槽</h2>
<h2 slot="right">替换右边插槽</h2>
</my-cpn>
</div>
<template id="myCpn">
<div>
<slot name="left">左边插槽</slot>
<slot name="mid">中间插槽</slot>
<slot name="right">右边插槽</slot>
<!--一个不带 name 的 <slot> 出口会带有隐含的名字“default”。-->
<slot>无名插槽</slot>
</div>
</template>
<script>
Vue.component('my-cpn', {
template: '#myCpn'
})
let app = new Vue({
el: '#app'
})
</script>
作用域插槽
父组件替换插槽的标签,但是内容由子组件来提供
<div id="app">
<!--1. 列表形式展示-->
<my-cpn>
<!--获取到slotProps属性-->
<template slot-scope="slotProps">
<ul>
<li v-for="item in slotProps.data">{{item}}</li>
</ul>
</template>
</my-cpn>
<!--2. 水平展示-->
<my-cpn>
<!--获取到slotProps属性-->
<template slot-scope="slotProps">
<span v-for="item in slotProps.data">{{item}} </span>
</template>
</my-cpn>
</div>
<template id="myCpn">
<div>
<slot :data="pLanguage"></slot>
</div>
</template>
<script>
Vue.component('my-cpn', {
template: '#myCpn',
data() {
pLanguage: ['JS', 'HTML', 'CSS']
}
})
let app = new Vue({
el: '#app'
})
</script>
Vue 2.6.0更新后的具名插槽slot
<div id="app">
<!--替换指定插槽的内容-->
<!--<template> 元素中的所有内容都将会被传入相应的插槽。任何没有被包裹在带有 v-slot 的 <template> 中的内容都会被视为默认插槽的内容。-->
<!--具名插槽的缩写为: #-->
<my-cpn>
<!-- 可缩写为<template #left> -->
<template v-slot:left>
<h2>替换左边插槽</h2>
</template>
</my-cpn>
<my-cpn>
<!-- 可缩写为<template #left> -->
<template v-slot:left>
<h2>替换左边插槽</h2>
</template>
<!-- 可缩写为<template #mid> -->
<template v-slot:mid>
<h2>替换中间插槽</h2>
</template>
<h2>我是替换内容</h2>
<p>我也是替换内容</p>
<!-- 可通过default明确替换默认内容 -->
<!-- 可缩写为<template #default> -->
<template v-slot:default>
<h2>我是替换内容</h2>
<p>我也是替换内容</p>
</template>
<!-- 可缩写为<template #right> -->
<template v-slot:right>
<h2>替换右边插槽</h2>
</template>
</my-cpn>
</div>
<template id="myCpn">
<div>
<slot name="left">左边插槽</slot>
<slot name="mid">中间插槽</slot>
<slot name="right">右边插槽</slot>
<!--一个不带 name 的 <slot> 出口会带有隐含的名字“default”。-->
<slot>无名插槽</slot>
</div>
</template>
<script>
Vue.component('my-cpn', {
template: '#myCpn'
})
let app = new Vue({
el: '#app'
})
</script>
注意: v-slot 只能添加在 <template> 上 (只有一种例外情况↓).
Vue 2.6.0更新后的作用域插槽slot
独占默认插槽的缩写语法
当被提供的内容只有默认插槽时,组件的标签才可以被当作插槽的模板来使用,这样就可以直接把v-slot用在组件上。
<my-cpn v-slot="slotProps">
<ul>
<li v-for="item in slotProps.data">{{item}}</li>
</ul>
</my-cpn>
<template id="myCpn">
<span>
<!--绑定在 <slot> 元素上的 attribute 被称为插槽 prop-->
<slot :data="pLanguage">
</slot>
</span>
</template>
<script>
Vue.component('my-cpn', {
template: '#myCpn',
data() {
pLanguage: ['JS', 'HTML', 'CSS']
}
})
let app = new Vue({
el: '#app'
})
</script>
注意: 默认插槽的缩写语法不能和具名插槽混用,因为它会导致作用域不明确
<!-- 无效,会导致警告 -->
<current-user v-slot="slotProps">
{{ slotProps.user.firstName }}
<template v-slot:other="otherSlotProps">
slotProps is NOT available here
</template>
</current-user>
只要出现多个插槽,请始终为所有的插槽使用完整的基于 <template> 的语法:
<current-user>
<template v-slot:default="slotProps">
{{ slotProps.user.firstName }}
</template>
<template v-slot:other="otherSlotProps">
...
</template>
</current-user>
Vue 2.6.0 新增动态插槽名
<base-layout>
<template v-slot:[dynamicSlotName]>
...
</template>
</base-layout>