什么是组件化?
人面对复杂问题的处理方式: 任何一个人处理信息的逻辑能力都是有限的
所以,当面对一个非常复杂的问题时,我们不太可能一次性搞定一大堆的内容。
但是,我们人有一种天生的能力,就是将问题进行拆解。 如果将一个复杂的问题,拆分成很多个可以处理的小问题,再将其放在整体当中,你会发现大的问题也会迎刃而解。
组件化也是类似的思想: 如果我们将一个页面中所有的处理逻辑全部放在一起,处理起来就会变得非常复杂,而且不利于后续的管理以及扩展。 但如果,我们讲一个页面拆分成一个个小的功能块,每个功能块完成属于自己这部分独立的功能,那么之后整个页面的管理和维护就变得非常容易了。
组件化是Vue.js中的重要思想
它提供了一种抽象,让我们可以开发出一个个独立可复用的小组件来构造我们的应用。
任何的应用都会被抽象成一颗组件树。
组件化思想的应用: 有了组件化的思想,我们在之后的开发中就要充分的利用它。 尽可能的将页面拆分成一个个小的、可复用的组件。 这样让我们的代码更加方便组织和管理,并且扩展性也更强。
那么我们如何注册组件呢?
组件的使用分成三个步骤:
创建组件构造器 注册组件 使用组件。
我们来看看通过代码如何注册组件 查看运行结果: 和直接使用一个div看起来并没有什么区别。 但是我们可以设想,如果很多地方都要显示这样的信息,我们是不是就可以直接使用<my-cpn></my-cpn>来完成呢?
代码实现:
<body>
<div id="app">
<!-- 3.组件使用-->
<my-cpn></my-cpn>
<my-cpn></my-cpn>
<my-cpn></my-cpn>
</div>
<script src="../js/vue.js"></script>
<script>
// <!--1.创建组件构造器对象-->
const cpnC = Vue.extend({
template:`<div>
<h2>组件标题</h2>
<p>我是组件中的一个段落内容</p>
<p>我是组件中的一个段落内容</p>
</div>
`
})
// <!--2.注册组件-->
Vue.component('my-cpn',cpnC)
const app=new Vue({
el:'#app',
data:{
message:'你好啊'
}
})
</script>
①、创建组件构造器对象
使用Vue中的extend方法,里面需要传入一个对象,对象内用template关键字写入我们的组件模板
Vue.extend(): 调用Vue.extend()创建的是一个组件构造器。 通常在创建组件构造器时,传入template代表我们自定义组件的模板。 该模板就是在使用到组件的地方,要显示的HTML代码。
// <!--1.创建组件构造器对象-->
const cpnC = Vue.extend({
template:`<div>
<h2>组件标题</h2>
<p>我是组件中的一个段落内容</p>
<p>我是组件中的一个段落内容</p>
</div>
`
})
②、注册组件
Vue.component(): 调用Vue.component()是将刚才的组件构造器注册为一个组件,并且给它起一个组件的标签名称。 所以需要传递两个参数:1、注册组件的标签名 2、组件构造器
使用Vue中的component方法,括号内写的参数第一个是我们即将注册的组件名,第二个参数则是传入我们的模板
Vue.component('my-cpn',cpnC)
③、使用组件
直接在div块中使用即可
组件必须挂载在某个Vue实例下,否则它不会生效。
我们来看下面我使用了三次<my-cpn></my-cpn> 而第三次其实并没有生效:
而我们上述使用注册组件的方法注册的全部都是全局组件,那么我们如何注册局部组件呢?
很简单,我们只需要在Vue实例中进行注册即可
在Vue实例中多写一个components属性。cpn为使用组件时的标签名,cpnC为组件构造器
const app=new Vue({
el:'#app',
data:{
message:'你好啊'
},
components:{
mycpn :cpnC
}
})
<div id="app">
<!-- 3.组件使用-->
<mycpn></mycpn>
<mycpn></mycpn>
<mycpn></mycpn>
</div>
<div id="app2">
<mycpn></mycpn>
<mycpn></mycpn>
<mycpn></mycpn>
<mycpn></mycpn>
</div>
当我们通过调用Vue.component()注册组件时,组件的注册是全局的。这意味着该组件可以在任意Vue示例下使用。
如果我们注册的组件是挂载在某个实例中, 那么就是一个局部组件
父子组件
在前面我们看到了组件树: 组件和组件之间存在层级关系 而其中一种非常重要的关系就是父子组件的关系
// <!-- 创建第一个组件构造器(子组件) -->
const cpnC1 = Vue.extend({
template:`<div>
<h2>我是标题1</h2>
<p>我是内容哈哈哈哈哈哈</p>
</div>
`
})
// <!-- 创建第二个组件构造器(父组件)-->
const cpnC2 = Vue.extend({
template:`<div>
<h2>我是标题2</h2>
<p>我是内容呵呵呵呵呵呵呵</p>
<cpn1></cpn1>
</div>
`,}
在这里我们创建了两个全局组件
我们可以在cpnC2中写一个template属性,并且为cpnC1注册组件
⭐注意:在这里注册的cpn1不可以在Vue实例中使用,它的作用域限定在了cpnC2中
const cpnC2 = Vue.extend({
template:`<div>
<h2>我是标题2</h2>
<p>我是内容呵呵呵呵呵呵呵</p>
<cpn1></cpn1>
</div>
`,
components:{
cpn1:cpnC1
}
})
<div id="app">
<cpn2></cpn2>
</div>
<script>
const app=new Vue({
el:'#app',
data:{
message:'你好啊'
},
components:{
cpn2:cpnC2
}
})
</script>
与此同时我们在Vue实例对象中注册cpn2组件,并且在Vue实例对象中调用它
这种情况我们就将cpn2称为父组件,cpn1称为子组件
⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐回到刚刚我们所说的注意事项:在这里注册的cpn1不可以在Vue实例中使用,它的作用域限定在了cpnC2中,什么意思呢?⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
其实这是一种父子组件错误用法:
以子标签的形式在Vue实例中使用
因为当子组件注册到父组件的components时,Vue会编译好父组件的模块 该模板的内容已经决定了父组件将要渲染的HTML(相当于父组件中已经有了子组件中的内容了)
<child-cpn></child-cpn>是只能在父组件中被识别的。
类似这种用法,<child-cpn></child-cpn>是会被浏览器忽略的。
注册组件语法糖
在上面注册组件的方式,可能会有些繁琐。 Vue为了简化这个过程,提供了注册的语法糖。 主要是省去了调用Vue.extend()的步骤,而是可以直接使用一个对象来代替。
全局组件的语法糖注册方法:
Vue.component('cpn1',{
template:`<div>
<h2>我是标题1</h2>
<p>我是内容呵呵呵呵呵呵呵</p>
</div>
`})
局部组件的语法糖注册方法:
模板的分离写法不知道同学们在写代码的时候有没有感觉写在script标签中十分的麻烦且杂乱?
<template id="cpn">
<div>
<h2>我是标题1</h2>
<p>我是内容1</p>
</div>
</template>
<script>
Vue.component('cpn',{
template:'#cpn'
})
</script>
所以Vue给我们提供了一个模板分离写法,我们可以在HTML中写入我们的模板.我们只需要在HTML中添加我们的template标签,并且为我们的标签添加一个id,即可在HTML文件中写入我们的模板。随后在我们的JS文件中为他注册组件即可,注册的时候需要注意用到#我们的标签选择器
组件可以访问Vue实例数据吗?
首先,答案是否定的,组件不可以访问Vue实例的数据
组件是一个单独功能模块的封装: 这个模块有属于自己的HTML模板,也应该有属性自己的数据data。
组件自己的数据存放在哪里呢?
组件对象也有一个data属性(也可以有methods等属性,下面我们有用到)
只是这个data属性必须是一个函数
而且这个函数返回一个对象,对象内部保存着数据
<div id="app">
<cpn></cpn>
<cpn></cpn>
<cpn></cpn>
<cpn></cpn>
<cpn></cpn>
<cpn></cpn>
</div>
<template id="cpn">
<div>
<h2>{{title}}</h2>
<p>我是内容1</p>
</div>
</template>
<script>
Vue.component('cpn',{
template:'#cpn',
data(){
return {
title:'我是标题'
}
}
})
那么这样我们的组件便拥有了自己的数据
为什么data在组件中必须是一个函数呢?
首先,如果不是一个函数,Vue直接就会报错。
其次,原因是在于Vue让每个组件对象都返回一个新的对象,因为如果是同一个对象的,组件在多次使用后会相互影响。
”
<div id="app">
<cpn></cpn>
<cpn></cpn>
<cpn></cpn>
<cpn></cpn>
<cpn></cpn>
<cpn></cpn>
<cpn></cpn>
</div>
<script src="../js/vue.js"></script>
<template id="cpn">
<div>
<h2>当前计数:{{counter}}</h2>
<button @click="increment">+</button>
<button @click="decrement">-</button>
</div>
</template>
<script>
Vue.component('cpn',{
template:'#cpn',
data(){
return{
counter:0
}
},
methods:{
increment(){
this.counter++
},
decrement(){
this.counter--
}
}
})
在上述代码段中,因为data是以函数的形式写入的组件,所以每一个组件对象都是一个新的对象,他们之间counter的结果互不影响
上述代码块中,因为data中写的函数每次都会返回同一个obj对象,所以四个计数器的结果会相互影响。所以这就是data为什么要用函数的解释
父子组件间通信
如何进行父子组件间的通信呢?Vue官方提到 通过props向子组件传递数据
通过$emit事件向父组件发送消息
一、父组件向子组件传递数据
在组件中 使用选项props来声明需要从父级接收到的数据。
<div id="app">
<cpn :cmovies="movies" :cmessage="message"></cpn>
</div>
<template id="cpn" >
<div>
<ul>
<li v-for="item in cmovies">{{item}}</li>
</ul>
<h2>{{cmessage}}</h2>
</div>
</template>
<script>
const cpn= {
template:'#cpn',
data(){
return{}
},
methods:{},
components:{
},
props:{
cmovies:{
type:Array,
default(){
return[]
}
},
cmessage:{
type:String,
default:'丘梓安'
}
}
}
const app=new Vue({
el:'#app',
data:{
message:'你好啊',
movies:['海王','海贼王','海尔兄弟']
},
components:{
cpn
}
})
props的值有两种方式: 方式一:字符串数组,数组中的字符串就是传递时的名称。
⭐方式二:对象,对象可以设置传递时的类型,也可以设置默认值等。(此方法使用较多)
子级向父级传递(重点难点)
子级向父级传递数据我们需要自定义事件进行传递
什么时候需要自定义事件呢?
当子组件需要向父组件传递数据时,就要用到自定义事件了。 我们之前学习的v-on不仅仅可以用于监听DOM事件,也可以用于组件间的自定义事件。自定义事件的流程:
在子组件中,通过$emit()来触发事件。
在父组件中,通过v-on来监听子组件事件。
我们通过以下代码来进行讲解
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<!--父组件模板-->
<div id="app">
<cpn @itemclick="cpnclick"></cpn>
</div>
<!--子组件模板-->
<template id="cpn">
<div>
<button v-for="item in categories"
@click="btnclick(item)">{{item.name}}</button>
</div>
</template>
<script src="../js/vue.js"></script>
<script>
<!-- 子组件-->
const cpn = {
template:'#cpn',
data(){
return{
categories:[
{id:'第一个产品',name:'iPad'},
{id:'第二个产品',name:'iPone'},
{id:'第三个产品',name:'iWatch'},
{id:'第四个产品',name:'iMac'}
]
}
},
methods:{
btnclick(item){
this.$emit('itemclick',item)
}
}
}
//父组件
const app=new Vue({
el:'#app',
data:{
message:'你好啊'
},
components:{
cpn
},
methods:{
cpnclick(item){
console.log('cpnclick',item);
}
}
})
</script>
</body>
</html>
我们首先分析我们干了什么?
我们写了四个按钮,这四个按钮的id和name我们都以函数形式保存在了我们的子组件内随后我们在子组件的模板内进行监听点击事件,
<button v-for="item in categories" @click="btnclick(item)">{{item.name}}</button>
按钮被点击后会执行我们子组件内写的methods方法
而我们这个methods写的正是我们的自定义事件,用到了$emit关键字自定义了一个itemclick事件
与此同时我们在父组件内利用v-on进行监听
当父组件监听到了我们的itemclick事件,即可执行我们的cpnclick方法,而我们的cpnclick方法是写在我们父组件模板内的methods
cpnclick方法写出我们会在控制台输出'cpnclick'字符串和item的名字
这就是我们的子组件向父组件传递数据的方法
父子组件的访问方式: $children和$refs
有时候我们需要父组件直接访问子组件,子组件直接访问父组件,或者是子组件访问根组件。
父组件访问子组件:使用$children或$refs reference(引用)
子组件访问父组件:使用$parent
我们先来看下$children的访问
this.$children是一个数组类型,它包含所有子组件对象。我们这里通过一个遍历,取出所有子组件的状态。
console.log(this.$children);
for (let c of this.$children) {
console.log(c.name);
c.showMessage();
}
$children的缺陷:
通过$children访问子组件时,是一个数组类型,访问其中的子组件必须通过索引值。 但是当子组件过多,我们需要拿到其中一个时,往往不能确定它的索引值,甚至还可能会发生变化。 有时候,我们想明确获取其中一个特定的组件,这个时候就可以使用$refs
$refs的访问
<cpn ref="aaa"></cpn>
console.log(this.$refs.aaa.name);
$refs和ref指令通常是一起使用的。
首先,我们通过ref给某一个子组件绑定一个特定的ID。
其次,通过this.$refs.ID就可以访问到该组件了。
slot插槽的基本使用
<!--1.插槽的基本使用 <slot></slot>--> <!--2.插槽的默认值 <slot>button</>slot>--> <!--3.如果有多个值,同时放入组件进行替换时,一起作为替换元素-->
组件的插槽也是为了让我们封装的组件更加具有扩展性。 让使用者可以决定组件内部的一些内容到底展示什么。
<!--1.插槽的基本使用 <slot></slot>-->
<!--2.插槽的默认值 <slot>button</>slot>-->
<!--3.如果有多个值,同时放入组件进行替换时,一起作为替换元素-->
<div id="app">
<!-- 插槽用button替换-->
<cpn> <button>按钮</button></cpn>
<!-- 插槽用span替换-->
<cpn> <span>哈哈哈</span></cpn>
<!-- 插槽用i替换-->
<cpn><i>呵呵呵</i></cpn>
<!-- 插槽用p/h2/h1一起替换,和我们说的第三个点一样,他会一起替换slot元素-->
<cpn>
<p>p元素</p>
<h2>h2元素</h2>
<h1>h1元素</h1>
</cpn>
<!-- 没有任何替换,那就会使用我们的默认值button-->
<cpn></cpn>
</div>
<template id="cpn">
<div>
<h2>我是组件</h2>
<p>我是组件哈哈哈哈哈</p>
<slot><button>按钮</button></slot>
</div>
</template>
具名插槽slot
当子组件的功能复杂时,子组件的插槽可能并非是一个。
比如我们封装一个导航栏的子组件,可能就需要三个插槽,分别代表左边、中间、右边。
那么,外面在给插槽插入内容时,如何区分插入的是哪一个呢?
这个时候,我们就需要给插槽起一个名字
如何使用具名插槽呢?
非常简单,只要给slot元素一个name属性即可 <slot name='myslot'></slot>
<div id="app">
<cpn><span slot="center">我是标题</span></cpn>
</div>
<template id="cpn">
<div>
<slot name="left"><span>左边</span></slot>
<slot name="center"><span>中间</span></slot>
<slot name="right"><span>右边</span></slot>
</div>
</template>
作用域插槽的使用
用域插槽是slot一个比较难理解的点,而且官方文档说的又有点不清晰。
这里,我们用一句话对其做一个总结,然后我们在后续的案例中来体会:
父组件替换插槽的标签,但是内容由子组件来提供。
我们先提出一个需求:
子组件中包括一组数据,比如:pLanguages: ['JavaScript', 'Python', 'Swift', 'Go', 'C++'] 需要在多个界面进行展示: 某些界面是以水平方向一一展示的, 某些界面是以列表形式展示的, 某些界面直接展示一个数组
<div id="app">
<cpn></cpn>
<cpn></cpn>
<cpn></cpn>
</div>
<template id="cpn">
<div>
<ul>
<li v-for="item in pLanguages">{{item}}</li>
</ul>
</div>
</template>
const app=new Vue({
el:'#app',
data:{
message:'你好啊',
},
components:{
cpn:{
template:'#cpn',
data(){
return{
pLanguages: ['C++','JavaScript','python','C','C#','Swift']
}
}
},
}
})
如果我们的代码是如上代码段的话,那么界面将会这么显示
但是我们的需求是某些界面是以水平方向一一展示的, 某些界面是以列表形式展示的, 某些界面直接展示一个数组
那么我们该如何修改代码呢?
①、首先为我们的子组件模板在根目录里新增一个slot标签,
并为slot标签绑定一个变量名="我们的目标数据"(:变量名="子组件内的目标数据")
<template id="cpn">
<div>
<slot :data="pLanguages">
<ul>
<li v-for="item in pLanguages">{{item}}</li>
</ul>
</slot>
</div>
</template>
②、在我们父组件调用组件的时候,
在cpn标签内部新增一个template标签,并在标签内部跟上slot-scope="slot"
并在template标签内写入我们需求的模板,
记住,此时slot.data中的data就是我们的pLanguages
<cpn>
<!-- 目的是获取子组件的数据pLanguages-->
<template slot-scope="slot">
<span>{{slot.data.join(' - ')}}</span>
</template>
</cpn>
<cpn>
<template slot-scope="slot">
<span>{{slot.data.join(' * ')}}</span>
</template>
</cpn>
这就是我们更改代码后的效果啦