这一块很重要,开发中经常用用到
组件化开发
1.1组件化的实现和使用步骤
-
任何人在面对复杂问题的处理方式
- 任何一个人的处理信息的逻辑能力都是有限的
- 所以,当面对一个非常复杂的问题时,我们不太可能一次性搞定一大推的内容
- 但是,我们可以将问题进行拆解
-
组件化开发也是类似的思想
- 如果我们将一个页面中所有的处理逻辑全部放在一起,处理起来就会变得十分复杂,而且不利于后续的管理以及拓展
- 但如果我们将一个页面查分成若干个小的功能块,每个功能块完成属于自己的独立的功能,那么之后整个页面的管理和维护就变得非常容易了
- vue的组件化思想,让我们可以开发出一个个独立可复用的小组件来构造我们的应用
- 任何的应用都会被抽象成一棵组件树
- 组件的使用分成三个步骤:
- 创建组件构造器
- 注册组件
- 使用组件
- 对于Vue.extend()
- 调用这个方法创建的是一个组件构造器
- 通常在创建构造器的地方,传入template代表我们自定义组件的模板
- 模板就是HTML代码
- 事实上,上图的写法基本上不用了,我们使用他的语法糖
- Vue.component()
- 调用这个方法是将组件构造器注册为一个组件,并且给他起一个组件的标签名称
- 需要传递两个参数: 1.注册组件的标签名 2. 组件构造器
- 组件必须挂载在某个Vue实例下,否则他不会生效
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<!-- 我想将下面这段代码封装起来-->
<!-- <h2>我是标题</h2>-->
<!-- <p>我是内容qwq</p>-->
<!-- <p>我是内容23333</p>-->
<my-cpn></my-cpn>
</div>
<!--下面这个在区域外,不会生效-->
<my-cpn></my-cpn>
<script src="../js/vue.js"></script>
<script>
//创建构造器对象
const cpnC = Vue.extend({
template: `<div>
<h2>我是标题</h2>
<p>我是内容qwq</p>
<p>我是内容23333</p>
</div>`
})
//注册组件
Vue.component('my-cpn', cpnC)
let app = new Vue({
el: '#app',//用于挂载要管理的元素
data: { //用于定义数据
}
})
</script>
</body>
</html>
1.2 全局组件和局部组件
全局组件: 组件可以在多个Vue实例中使用
上个案例的代码创建的组件就是全局组件
局部组件: 只能在指定的Vue实例里面使用
注意: 其实在开发中我们一般只创建一个Vue实例
Vue实例中,除了我们之前学习的data,computed,methods属性之外,现在再加上一个属性,components
属性,在这里面定义的组件只能在这个Vue实例中使用
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<cpn></cpn>
</div>
<script src="../js/vue.js"></script>
<script>
const cpnC = Vue.extend({
//这里的``符号是es6特有的,这个符号括起来的字符串允许换行
template: `<div><h2>就是这么自信!</h2></div>`
})
let app = new Vue({
el: '#app',//用于挂载要管理的元素
data: { //用于定义数据
},
components: {
//key是标签名, value是组件构造器
cpn: cpnC
}
})
</script>
</body>
</html>
1.3 父组件和子组件的区别
子组件就是在父组件里面注册的组件
除非子组件也在实例中注册了,不然子组件是无法单独引用的
不多bb,直接上例子
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<!-- cpn2里面cpn1注册了,所以可以显示, cpn2是父组件,cpn1是子组件-->
<cpn2></cpn2>
<!-- 下面这个是不会显示,会报错的,因为儿子在Vue实例里面没有注册-->
<cpn1></cpn1>
</div>
<script src="../js/vue.js"></script>
<script>
const cpnC1 = Vue.extend({
template: `<div><h2>我是儿子!</h2></div>`,
});
const cpnC2 = Vue.extend({
template: `<div>
<h2>我是亲爹!</h2>
<cpn1></cpn1>
</div>`,
components: {
cpn1: cpnC1
}
});
//这个其实也是一个组件,这个是最顶部的组件,叫做Root组件
let app = new Vue({
el: '#app',//用于挂载要管理的元素
data: {},
components: {
cpn2: cpnC2,
}
})
</script>
</body>
</html>
当然,要是父组件中没有注册子组件,那么要想父组件中的子组件能够正常显示,就必须在Root组件中声明子组件
1.4 注册组件的语法糖写法
注册全局组件的语法糖
Vue.component('cpn1', {
template: `
<div>
<h2>我是你爹</h2>
</div>
`
})
//直接这样写组件就注册好了,可以直接在Vue实例中调用
第二个参数其实是调用了extend()方法的,只不过被隐藏了
注册局部组件的语法糖
components: {
'cpn1': {
template: `
<div>
<h2>我是你爹</h2>
</div>
` `
}
1.5 组件模板抽离的写法
如果像上面那样将HTML模板写在了JS里面,那么其实就是很不好看,所以我们要抽离
第一种: 将模板定义在同一文件但是不同script域中 (不常用,不好用)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<cpn1></cpn1>
</div>
<!--分离的模板写在这里-->
<script type="text/x-template" id="cpn">
<div>
<h2>我是你爹</h2>
</div>
</script>
<script src="../js/vue.js"></script>
<script>
let app = new Vue({
el: '#app',
components: {
'cpn1': {
template: '#cpn'
}
}
})
</script>
</body>
</html>
第二种: 使用<template>
标签 (推荐使用)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<cpn1></cpn1>
</div>
<!--分类的模板写在这里-->
<template id="cpn">
<div>
<h2>我是你爹</h2>
</div>
</template>
<script src="../js/vue.js"></script>
<script>
let app = new Vue({
el: '#app',
data: {},
components: {
'cpn1': {
template: '#cpn'
}
}
})
</script>
</body>
</html>
1.6 组件的Data为什么需要是一个function
组件数据的存放
- 组件对象也有一个data属性(里面还可以有prop,methods等) (和Vue实例相似的原因就是因为,组件对象原型指向Vue实例)
- 只不过这个data属性必须是一个函数,这个函数返回一个对象,返回的就是保存着的数据
为什么必须是一个函数
因为我们的组件是要复用的,我们在不同的页面也能会有多个这个组件,为了保证data的数据不共享,所以我们将data作为一个函数对象返回,这样,每一个组件实例所拥有的data对象都是相互独立的,他们的数据也是独立的,不会相互影响
我们可以想象,每个组件实例指向一个data对象, 每个return的到的data对象所处的推空间的地址是不一样的,所以他们是独立的
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<!-- 这三个组件实例的数据是相互独立的-->
<cpn1></cpn1>
<cpn1></cpn1>
<cpn1></cpn1>
</div>
<!--分类的模板写在这里-->
<template id="cpn">
<!-- 注意内容一定要包一个div作为根元素-->
<div>
<h2>计数器{{count}}</h2>
<button @click="increase">+</button>
<button @click="decrease">-</button>
</div>
</template>
<script src="../js/vue.js"></script>
<script>
let app = new Vue({
el: '#app',
data: {},
components: {
'cpn1': {
template: '#cpn',
data() {
return {
count: 0
}
},
methods: {
increase() {
this.count++;
},
decrease() {
this.count--;
}
}
}
}
})
</script>
</body>
</html>
1.7 父子组件通信
1.7.1 父传子props
父组件中data的数据正常下是子组件是无法调用的,所以我们可以用props
第一种写法就是props后面写一个数组 props: ['新的变量名1','新的变量名2']
可读性比较差,比较少用
第二种写法就是使用对象的写法
支持以下类型
下面我演示了一个父传子传子
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<cpn :movies="movies"></cpn>
</div>
<template id="cpn">
<cpn1 :movies="movies"></cpn1>
</template>
<template id="cpn1">
<div>
<ul>
<li v-for="item in movies">{{item}}</li>
</ul>
</div>
</template>
<script src="../js/vue.js"></script>
<script>
const cpn1 = Vue.component('cpn1', {
template: "#cpn1",
props: {
movies: Array
}
})
const cpn = {
template: `#cpn`,
props: {
//这种是简写的方法
//movies: Array
movies: {
//我们在这里可以定义很多东西
type: Array,
//对象是一个对象2或者数组的时候,默认值必须是一个函数
default() {
return ["我是你爹"]
}
}
},
component: {
cpn1
}
}
let app = new Vue({
el: '#app',//用于挂载要管理的元素
data: { //用于定义数据
movies: ['海王', '海贼王', '海尔兄弟']
},
components: {
cpn
}
})
</script>
</body>
</html>
1.7.2 props中的驼峰标识
如果在props中取名是用驼峰命名法取名,那么在v-bind的时候就会有问题, **直接写驼峰标识系统就不认,将大写的那个地方换成小写,同时前面加上一个-
符号就可以了 **
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<!-- 下面这样写是错误的,vue不支持-->
<!-- <cpn :cInfo="info"></cpn>-->
<!-- 把驼峰转化为这个就支持了-->
<cpn :c-info="info"></cpn>
</div>
<template id="cpn">
<h2>{{cInfo}}</h2>
</template>
<script src="../js/vue.js"></script>
<script>
const cpn = {
template: '#cpn',
props: {
cInfo: {
type: Object,
default() {
return { name: '你爹'};
}
}
}
}
let app = new Vue({
el: '#app',//用于挂载要管理的元素
data: { //用于定义数据
info: {
name: 'father',
age:20,
height: 1.88
}
},
components: {
cpn
}
})
</script>
</body>
</html>
1.7.3 子传父(自定义事件)
我们需要通过自定义事件来完成子组件中的data传递到父组件
- 我们之前学习的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 @btn-click="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: 1,
name: '热门推荐'
},{
id: 2,
name: '手机数码'
},{
id: 3,
name: '家用家电'
},{
id: 4,
name: '电脑办公'
}],
id: -1
}
},
methods: {
btnClick(item){
//自定义事件传递到父组件
this.$emit('btn-click',item.id);
}
}
}
let app = new Vue({
el: '#app',
data: {
info: {
name: 'father',
age:20,
height: 1.88
}
},
components: {
cpn
},
methods: {
//这里就是我们在父组件中获取子组件中data的方法的定义
cpnClick(e) {
console.log(e);
}
}
})
</script>
</body>
</html>
1.7.4 子父双向绑定案例
注意: 我们在子组件中,不要修改传来的父组件的值,这样是不对的,我们要通过计算属性,或者一个新的data属性来接受这个值,然后再对计算属性或者data修改就行了
我们这里要做一个计时器的案例,组件里面只有两两个按钮,总数在总页面中创建
所以我们在总页面中创建一个num1
, 将这个初值传进子组件,然后子组件中定义一个num2
,num2的初值等于num1,然后子组件中的两个按钮控制num2的值的修改,将num2的值实时传回父组件,并显示
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<cpn @btn-click="cpnClick" :num1="num1">
</cpn>
<h2>总数: {{num1}}</h2>
</div>
<template id="cpn">
<div>
<button @Click="btnClickAdd">+</button>
<button @Click="btnClickDec">-</button>
</div>
</template>
<script src="../js/vue.js"></script>
<script>
const cpn = {
template: '#cpn',
data(){
return {
num2: this.num1
}
},
props: {
num1: Number
},
methods: {
btnClickAdd(){
//自定义事件传递到父组件
this.num2++;
this.$emit('btn-click',this.num2);
},
btnClickDec(){
//自定义事件传递到父组件
this.num2--;
this.$emit('btn-click',this.num2);
}
}
}
let app = new Vue({
el: '#app',//用于挂载要管理的元素
data: { //用于定义数据
num1: 0
},
components: {
cpn
},
methods: {
cpnClick(e) {
this.num1 = e;
}
}
})
</script>
</body>
</html>
1.7.5 watch属性
除了data,properties,components等属性之外,还有一种特别好用的属性,watch
,它可以监听data中数据的改变,然后做出响应的行为
et app = new Vue({
el: '#app',//用于挂载要管理的元素
data: { //用于定义数据
num1: 0
},
components: {
cpn
},
methods: {
cpnClick(e) {
this.num1 = e;
}
},
watch: {
//XXX就是你想要监听的变量名,下面这个newValue就是watch监听到的新改变的值,oldValue懂得都懂
XXX(newValue,oldValue) {
方法体
}
}
})
1.7.6 父访问子 — children-refs
有时候,我的父组件不只是想要和子组件传递数据,有时候我想要调用子组件里面的方法,那么我就用上面这个进行访问
父组件访问子组件
- 使用
$children
(返回所有的子组件对象的数组) (较少使用) - 使用
$refs
(返回指定的子组件对象,需在组件的标签上面加上ref属性 ) (如果直接this. r e f s , 那 么 他 会 返 回 所 有 的 带 有 r e f 属 性 的 组 件 的 对 象 的 对 象 , ∗ ∗ 所 以 我 们 一 般 使 用 的 时 候 都 是 ‘ t h i s . refs,那么他会返回所有的带有ref属性的组件的对象的对象 , **所以我们一般使用的时候都是`this. refs,那么他会返回所有的带有ref属性的组件的对象的对象,∗∗所以我们一般使用的时候都是‘this.refs.xxx`xxx就是我们在组件标签上面定义的ref的名字 **)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<cpn @btn-click="cpnClick" :num1="num1">
</cpn>
<cpn @btn-click="cpnClick" :num1="num1" ref="aaa">
</cpn>
<cpn @btn-click="cpnClick" :num1="num1" ref="bbb">
</cpn>
<h2 @click="btnClick">总数: {{num1}}</h2>
</div>
<template id="cpn">
<div>
<button @Click="btnClickAdd">+</button>
<button @Click="btnClickDec">-</button>
</div>
</template>
<script src="../js/vue.js"></script>
<script>
const cpn = {
template: '#cpn',
data(){
return {
num2: this.num1
}
},
props: {
num1: Number
},
methods: {
btnClickAdd(){
//自定义事件传递到父组件
this.num2++;
this.$emit('btn-click',this.num2);
},
btnClickDec(){
//自定义事件传递到父组件
this.num2--;
this.$emit('btn-click',this.num2);
}
}
}
let app = new Vue({
el: '#app',//用于挂载要管理的元素
data: { //用于定义数据
num1: 0
},
components: {
cpn
},
methods: {
cpnClick(e) {
this.num1 = e;
},
btnClick() {
//这里打印的就是子组件数组(包含了所有的子组件)
console.log(this.$children);
//这个不行,这个只能打印指定的子组件数组,当指定了具体哪一个的时候,直接返回一个组件对象
console.log(this.$refs.aaa);
}
}
})
</script>
</body>
</html>
1.7.7 子访问父 ---- parent-root
开发中不建议使用,了解即可 一个是访问父组件,一个是访问根组件
1.8 slot插槽
1.8.1 slot插槽的基本使用
-
组件的插槽
- 组件的插槽也是为了让我们封装i起来的组件更加具有拓展性
- 让使用者可以决定组件内部的一些内容到底展示什么
-
怎样封装合适呢?
- 最好的封装方式就是将共性抽取到组件中,将不同暴露为插槽
- 一旦我们预留了插槽,那么我们就可以让使用者根据自己的需求,决定插槽中插入什么内容
插槽的基本使用
-
在template里面加上
slot
标签 -
想要slot有默认值,就在slot标签里面加上一些标签
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="app"> <!-- 有了插槽之后,我们就可以在组件标签里面定义我们想要的内容--> <cpn> <button>按钮</button> </cpn> <cpn>我是你爹</cpn> <cpn></cpn> </div> <template id="cpn"> <div> <h2>我是组件</h2> <p>哈哈哈哈</p> <!--下面这里我们使用插槽,父组件在子组件里面还可以加内容--> <slot> <!-- 这里也可以写东西,这个是默认值,意思是我们在引用组件的时候不额外加东西的时候,这里面的标签就会显示--> <button>我是默认值</button> </slot> </div> </template> <script src="../js/vue.js"></script> <script> const cpn = { template: '#cpn' } let app = new Vue({ el: '#app',//用于挂载要管理的元素 data: { //用于定义数据 message: '我是你爹!' }, components: { cpn } }) </script> </body> </html>
1.8.2 具名插槽的使用
当我们定义了多个插槽的时候,如果没有命名,那么我们在自定义组件的时候,我们自定义的内容就会在所有插槽里面显示出来,这不合理,所以我们要给插槽命名
**那么怎么起名呢? 给slot标签一个name属性
, 在自定义的时候,标签加上slot
属性 **
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<!-- 如果不命名,那么就会实现所有插槽都会变成我是你爹-->
<cpn>
我是你爹
<span slot="aa">我是自定义的1</span>
<span slot="bb">我是自定义的2</span>
<span slot="cc">我是自定义的3</span>
</cpn>
<!-- 页面显示: 我是自定义的1 我是自定义的2 我是自定义的3 我是你爹-->
</div>
<template id="cpn">
<div>
<!--下面这里我们使用插槽,父组件在子组件里面还可以加内容-->
<slot name="aa"><p>我是1</p></slot>
<slot name="bb"><p>我是2</p></slot>
<slot name="cc"><p>我是3</p></slot>
<slot><p>我是默认</p></slot>
</div>
</template>
<script src="../js/vue.js"></script>
<script>
const cpn = {
template: '#cpn'
}
let app = new Vue({
el: '#app',//用于挂载要管理的元素
data: { //用于定义数据
message: '我是你爹!'
},
components: {
cpn
}
})
</script>
</body>
</html>
1.8.3 编译作用域的概念
- 父组件模板中的所有东西都会在父级作用域内编译;子组件模板的所有东西都会在子级作用域内编译
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<div id="app">
<!-- 这里使用的isShow是Vue实例的isShow,所以显示了-->
<cpn v-show="isShow"></cpn>
</div>
<template id="cpn">
<div>
<h2>哈哈哈我是组件</h2>
<!-- 按钮使用的isShow是组件的isShow,所以不显示-->
<button v-show="isShow">就这?</button>
</div>
</template>
<script src="../js/vue.js"></script>
<script>
let app = new Vue({
el: '#app',//用于挂载要管理的元素
data: { //用于定义数据
isShow: true
},
components: {
cpn: {
template: "#cpn",
data() {
return {
isShow: false
}
}
}
}
})
</script>
</body>
</html>
注意: 凡是在div#app 里面写的所有的数据,都是引用Vue实例的数据
1.8.4 作用域插槽
- 作用域插槽是slot比较难理解的点,而且官方文档说的又有点不清晰
总结一句话就是: 父组件替换插槽的标签,但是内容由子组件来提供。
核心代码: v-scpoe = "自定义的引用时的名字"
和 v-slot:组件的名字 = "自定义的引用时的名字"
现在我有一个需求
<!DOCTYPE html>
<html lang="en" xmlns:v-slot="http://www.w3.org/1999/XSL/Transform" xmlns:slot-scope="">
<head>
<meta charset="UTF-8">
<title>Title</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<div id="app">
<!-- 这里是默认显示-->
<cpn></cpn>
<!-- 这里是横着显示-->
<cpn >
<!-- Vue2.5.X以下的版本必须使用template,以上的就不用,div就行-->
<!-- 下面这句话的意思是引用插槽对象,我们这里就可以使用下面的data了-->
<template slot-scope="message">
<span>{{message.data.join(' - ')}}</span>
</template>
</cpn>
<br/>
<!-- 还有一种写法-->
<cpn>
<!-- 如果要指定是哪个slot组件的话,就写v-slot:组件的名称 = "引用的名字" -->
<template v-slot="hehe">
<span>{{hehe.data}}</span>
</template>
</cpn>
</div>
<template id="cpn">
<div>
<!--下面这个操作就是让slot自定义了一个属性来绑定保存了pLanguages的数据-->
<slot :data="pLanguages" >
<ul>
<li v-for="item in pLanguages">{{item}}</li>
</ul>
</slot>
</div>
</template>
<script src="../js/vue.js"></script>
<script>
let app = new Vue({
el: '#app',//用于挂载要管理的元素
data: { //用于定义数据
},
components: {
cpn: {
template: "#cpn",
data() {
return {
pLanguages: ['JavaScript', 'Python', 'Swift', 'Go', 'C++']
}
}
}
}
})
</script>
</body>
</html>