文章目录
Day04 组件化开发
组件化,就是把页面拆分成多个组件,每个组件依赖的 CSS、JS、模板、图片等资源放在一起开发和维护。 因为组件是资源独立的,所以组件在系统内部可复用,组件和组件之间可以嵌套,如果项目比较复杂,可以极大简化代码量,并且对后期的需求变更和维护也更加友好。
Vue.js的一个复杂页面可以理解成一个组件树。如下图所示:
一、组件的基本使用
步骤
- 创建组件
const myComp = Vue.extend({
template:
`<div><h2>标题</h2><p>内容</p></div>`
});
- 注册组件
Vue.component('my-comp', myComp);
- 使用组件
<div id="app">
<!-- 使用组件 -->
<my-comp></my-comp>
</div>
注意:不能在没有挂载vue对象的标签内使用组件,浏览器无法识别。
局部组件与全局组件
局部组件:只能在注册该组件的Vue对象绑定的div内使用。
全局组件:可以在任意的绑定Vue对象的div内挂载组件。
<div id="app">
<global-comp></global-comp>
<partial-comp></partial-comp>
</div>
<div id="app2">
<global-comp></global-comp>
<partial-comp></partial-comp>
</div>
<script src="../js/vue.js"></script>
<script>
//1.创建组件
const myComp = Vue.extend({
template:
`<div><h2>全局组件</h2><p>可以在挂载Vue的任意div内使用</p></div>`
});
const myComp2 = Vue.extend({
template:
`<div><h2>局部组件</h2><p>只可以在注册的Vue内使用</p></div>`
});
//2.注册组件
Vue.component('global-comp', myComp);
const app = new Vue({
el: "#app",
components: { //局部组件
'partial-comp': myComp2
}
});
const app2 = new Vue({
el: "#app2",
})
</script>
效果如下:
局部组件只能在注册了组件的标签内使用。
语法糖写法和模板抽离
<div id="app">
<global-comp></global-comp>
<part-comp></part-comp>
</div>
<script type="text/x-template" id="global">
<div>
<h2>全局组件标题</h2>
<p>全局组件内容</p>
</div>
</script>
<template id="part">
<div>
<h2>局部组件标题</h2>
<p>局部组件内容</p>
</div>
</template>
<script src="../js/vue.js"></script>
<script>
Vue.component('global-comp', {
template: '#global'
})
const app = new Vue({
el: "#app",
components: {
'part-comp': {
template: '#part'
}
}
})
</script>
</body>
二、Vue的父子组件通信
父子组件的通信是解决传值的问题。
Vue组件的data
Vue组件无法直接访问Vue实例里的值。
组件对象里也有一个data属性,值的类型为一个函数。(也可以有methods等属性)。该函数的返回值是组件的数据。
<div id="app">
<part-comp></part-comp>
<part-comp></part-comp>
<part-comp></part-comp>
</div>
<template id="comp1">
<div>
<h2>{{title}}</h2>
<p>局部组件内容</p>
</div>
</template>
<script src="../js/vue.js"></script>
<script>
const app = new Vue({
el: "#app",
components: {
'part-comp': {
template: '#comp1',
data() {
return {
title: 'abc'
}
}
}
}
})
</script>
组件的data属性为什么是函数
因为组件对象的值不能共享。使用组件时, 每个组件对象都应该使用自己的值,当data属性是函数时正好可以保证每个组件的数据独立性。
父子组件传值
父组件通过props向子组件传值
应用场景
当父组件请求的数据需要在子组件中展示时不可能再次在子组件中发送请求,这时需要将父组件的值传递到子组件中。
代码
<div id="app">
<cnp :ctitle="message" v-bind:cmovies="movies"></cnp>
<cnp v-bind:cmovies="movies"></cnp>
<cnp></cnp>
</div>
<template id="cnp">
<div>
<h2>{{ctitle}}</h2>
<ul v-for="item in cmovies">
<li>{{item}}</li>
</ul>
</div>
</template>
<script src="../js/vue.js"></script>
<script>
const cnp = {
template: '#cnp',
// props: ['ctitle', 'cmovies'] (很少用到)
// 限定类型
/*props: {
ctitle: String,
cmovies: Array
}*/
props: {
ctitle: {
type: String,
default: '默认标题',
},
cmovies: {
type: Array,
default: function () { //数组和对象必须用返回函数
return [];
}
},
//必须出现
cpropsA: {
type: String,
required: true
},
//传递多个类型
cpropsB: [String, Number],
//自定义验证
cpropsC: {
validator: function (value) {
return ['success', 'warning', 'danger'].indexOf(value) !== -1;
}
}
}
};
const app = new Vue({
el: "#app",
data: {
message: 'Hello, Vue Parent Component',
movies: [
'笑傲江湖',
'雪山飞狐',
'射雕英雄传'
]
},
components: {
'cnp': cnp
}
})
</script>
props支持的类型
- String
- Number
- Boolean
- Array
- Object
- Date
- Function
- Symbol
也可以自定义类型
function Person(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
...
props: {
author: Person
}
...
在Vue组件中如果有多个标签必须包含在div中。
子组件通过自定义事件向父组件传值
应用场景
比如,当子组件中发生事件时,需要通知父组件重新请求改变显示的列表。(在点击分类时根据点击的分类组件,通知父组件点击了哪一个分类进行数据重新请求与展示)。
代码
通过自定义事件实现。
<div id="app">
<!-- 监听子组件事件,进行处理 (省略参数时默认传参数(不是浏览器的点击事件不会传递event对象))-->
<!--<cnp v-on:itemclick="categoryClick"></cnp>-->
<cnp @itemclick="categoryClick"></cnp>
</div>
<!--1.子组件-->
<template id="cnp">
<div>
<!--<button v-for="item in categories" v-on:click="btnClick(item.id)">{{item.name}}</button>-->
<button v-for="item in categories" @click="btnClick(item.id)">{{item.name}}</button>
</div>
</template>
<script src="../js/vue.js"></script>
<script>
const cnp = {
template: "#cnp",
data() {
return {
categories: [
{id: "aaa", name: "热门推荐"},
{id: "bbb", name: "手机数码"},
{id: "ccc", name: "家用电器"},
{id: "ddd", name: "电脑办公"}
]
}
},
methods: {
btnClick(id) {
console.log("btnClick: "+id);
//发射事件: 1.事件名称; 2.传递参数
this.$emit("itemclick", id);
}
}
};
const app = new Vue({
el: "#app",
components: {
cnp
},
methods: {
categoryClick(id) {
console.log("categoryClick: " + "监听子组件事件, id为: "+ id);
}
}
})
</script>
双向绑定案例
需求
子组件双向绑定父组件数据,实现动态改变
- 数据来自于父组件的data
- 将父组件的data传递到子组件中
- 根据子组件的input标签值的改变然后改变父组件的data
步骤
- 使用props将父组件的值传到组件中(不要直接改变props中的值,使用计算属性或者data改变props中的值)
- 使用:value和@input改变子组件中data()中的值,同时向父组件发射事件,改变父组件data
- 父组件监听事件改变data,同时子组件的props中的值也会改变
代码
<div id="app">
<cnp :pnumber1="num1" :pnumber2="num2" @changepnumber1="changepnumber1" @changepnumber2="changepnumber2"></cnp>
</div>
<template id="cnp">
<div>
<h2>props: {{pnumber1}}</h2>
<h2>data: {{dnumber1}}</h2>
<!--<input type="text" v-model="dnumber1" />-->
<input type="text" :value="dnumber1" @input="changenumber1"/>
<h2>props: {{pnumber2}}</h2>
<h2>data(): {{dnumber2}}</h2>
<!--<input type="text" v-model="dnumber2" />-->
<input type="text" :value="dnumber2" @input="changenumber2"/>
</div>
</template>
<script src="../js/vue.js"></script>
<script>
const cnp = {
template: "#cnp",
props: {
pnumber1: Number,
pnumber2: Number,
},
data() {
return {
dnumber1: this.pnumber1,
dnumber2: this.pnumber2
}
},
methods: {
changenumber1(event) {
this.dnumber1 = event.target.value;
this.$emit("changepnumber1", this.dnumber1);
//this.pnumber1 = this.dnumber1;
this.dnumber2 = event.target.value * 100;
this.$emit("changepnumber2", this.dnumber2);
},
changenumber2(event) {
this.dnumber2 = event.target.value;
this.$emit("changepnumber2", this.dnumber2);
//this.pnumber2 = this.dnumber2;
this.dnumber1 = event.target.value / 100;
this.$emit("changepnumber1", this.dnumber1);
}
}
};
const app = new Vue({
el: "#app",
data: {
num1: 1,
num2: 0
},
components: {
cnp
},
methods: {
changepnumber1(value) {
this.num1 = parseFloat(value)
},
changepnumber2(value) {
this.num2 = parseFloat(value)
},
}
})
</script>
效果
改进: watch方式
<div id="app">
<cnp :pnumber1="num1" :pnumber2="num2" @changepnumber1="changepnumber1" @changepnumber2="changepnumber2"></cnp>
</div>
<template id="cnp">
<div>
<h2>props: {{pnumber1}}</h2>
<h2>data: {{dnumber1}}</h2>
<input type="text" v-model="dnumber1"/>
<h2>props: {{pnumber2}}</h2>
<h2>data(): {{dnumber2}}</h2>
<input type="text" v-model="dnumber2"/>
</div>
</template>
<script src="../js/vue.js"></script>
<script>
const cnp = {
template: "#cnp",
props: {
pnumber1: Number,
pnumber2: Number,
},
data() {
return {
dnumber1: this.pnumber1,
dnumber2: this.pnumber2
}
},
watch: { //监控dnumber1变化, 改变dnumber2, num2
dnumber1(newValue) {
this.dnumber2 = newValue * 100;
this.$emit("changepnumber1", newValue);
},
dnumber2(newValue) {
this.dnumber1 = newValue / 100;
this.$emit("changepnumber2", newValue);
},
}
};
const app = new Vue({
el: "#app",
data: {
num1: 1,
num2: 0
},
components: {
cnp
},
methods: {
changepnumber1(value) {
this.num1 = parseFloat(value)
},
changepnumber2(value) {
this.num2 = parseFloat(value)
}
}
})
</script>
三、父子组件的访问方式
前面是父子组件的通信(传值),现在是通过控制子组件或者父组件的对象改变值(访问)
父访问子
使用$children或者$refs 访问子组件的对象或者方法。$children用的很少,一般使用$refs。
通过this.$children会获取到子组件的一个数组,通过索引访问子组件的对象或者方法。
<div id="app">
<!-- 三个类型相同的子组件 -->
<cpn></cpn>
<cpn></cpn>
<cpn></cpn>
<button v-on:click="btnClick">按钮</button>
</div>
<template id="cpn">
<div>
我是子组件
</div>
</template>
<script src="../js/vue.js"></script>
<script>
const app = new Vue({
el: "#app",
components: {
cpn: {
template: "#cpn",
data() {
return {
message: "我是子组件"
};
},
methods: {
showMessage() {
console.log('showMessage: ' + this.message);
}
}
}
},
methods: {
btnClick() {
//访问子组件的对象
console.log(this.$children[0].message);
//访问子组件的方法
this.$children[0].showMessage();
}
}
})
</script>
通过**$refs**可以更加灵活的获取子组件对象,需要在子组件标签里添加ref属性,这种方式在开发中更加常用。
...
<cpn ref="ccc"></cpn>
...
<script>
...
//访问子组件对象
console.log(this.$refs.ccc.message)
...
</script>
子访问父
子访问父不会经常使用(耦合性增强,复用性减弱),可以作为简单了解。使用** p a r e n t 或 者 parent或者 parent或者root** 访问子组件的对象或者方法。
<div id="app">
<cpn></cpn>
</div>
<template id="cpn">
<div>
我是子组件
<button @click="btnClick">子组件按钮</button>
<ccpn></ccpn>
</div>
</template>
<template id="ccpn">
<div>
我是子组件的子组件
<button @click="btnClick">子组件的子组件</button>
</div>
</template>
<script src="../js/vue.js"></script>
<script>
const app = new Vue({
el: "#app",
data: {
message: "vue实例"
},
components: {
cpn: {
template: "#cpn",
data() {
return {
message: "我是子组件"
};
},
methods: {
btnClick() {
console.log(this.$parent.message);
//访问最顶层的vue实例
console.log(this.$root.message);
}
},
components: {
ccpn: {
template: "#ccpn",
data() {
return {
message: "我是子组件的子组件"
};
},
methods: {
btnClick() {
//访问父组件
console.log(this.$parent.message);
//访问根组件(访问最顶层的vue实例)
console.log(this.$root.message);
}
}
}
}
}
},
})
</script>
四、slot插槽
组件化开发中可能会出现结构相同,内容不同的组件。这时候需要抽取相同的内容,然后将不同的内容定义为插槽,然后在引用组件时定义不同的内容,实现组件的扩展。
slot插槽基本使用
用法
在组件中使用<slot></slot>
标签来定义插槽。
默认值
在slot标签中可以设置默认值
<slot><button>默认按钮</button></slot>
注意
如果有多个值替换只有一个slot标签时会一起作为替换元素
<div id="app">
<cpn></cpn>
<cpn>
<p>能不能给我一首歌的时间</p>
<div>我本可以忍受黑暗</div>
</cpn>
</div>
</body>
<template id="cpn">
<div>
<p>
子组件
</p>
<slot>
<button>默认按钮</button>
</slot>
</div>
</template>
<script src="../js/vue.js"></script>
<script>
const app = new Vue({
el: "#app",
data() {
return {
message: 'Hello Vue!'
}
},
components: {
cpn: {
template: '#cpn'
}
}
})
</script>
效果如下
具名插槽的使用
当组件中出现多个slot标签时,需要在slot标签中添加name属性来确定替换哪个具体的插槽,然后替换时在要替换的标签中指定slot属性和name的值一致即可。
<div id="app">
<cpn>
<button slot="left">替换左边</button>
<!--只会替换不具名的插槽-->
<button>默认按钮</button>
</cpn>
</div>
</body>
<template id="cpn">
<div>
<slot name="left">
<span>左边</span>
</slot>
<slot name="center">
<span>中间</span>
</slot>
<slot name="right">
<span>右边</span>
</slot>
<slot>没有具名</slot>
</div>
</template>
<script src="../js/vue.js"></script>
<script>
const app = new Vue({
el: "#app",
data() {
return {
message: 'Hello Vue!'
}
},
components: {
cpn: {
template: '#cpn'
}
}
})
</script>
作用域插槽
编译作用域
<div id="app">
<cpn v-show="isShow"></cpn>
</div>
</body>
<template id="cpn">
<div>
<h2>给我一首歌的时间</h2>
<p>能不能给我一首歌的时间</p>
<p>紧紧的把那拥抱变成永远</p>
<p>在我的怀里你不用害怕失眠</p>
<p>如果你想忘记我也能失忆</p>
<p>能不能给我一首歌的时间</p>
<p>把故事听到最后才说再见</p>
<p>你送我的眼泪 让它留在雨天</p>
<p>哦 越过你划的线我定了勇气 的终点</p>
</div>
</template>
<script src="../js/vue.js"></script>
<script>
const app = new Vue({
el: "#app",
data: {
message: '你好啊',
isShow: true
},
components: {
cpn: {
template: "#cpn",
data() {
return {
isShow: false
}
}
}
}
})
</script>
cpn下的变量是vue实例中的isShow,判断使用的是哪个变量的唯一标准是在哪个模板下使用就用哪个模板的变量。
作用域插槽
父组件提供插槽的标签,但内容由子组件来提供。主要在父组件有对子组件数据显示形式需求有变化的时候使用。
代码示例如下:
<div id="app">
<cpn></cpn>
<cpn>
<template slot-scope="slot">
{{slot.data.join(' - ')}}
</template>
</cpn>
</div>
</body>
<template id="cpn">
<div>
<h2>子组件</h2>
<slot :data="pLanguage">
<ul>
<li v-for="pl in pLanguage">{{pl}}</li>
</ul>
</slot>
</div>
</template>
<script src="../js/vue.js"></script>
<script>
const app = new Vue({
el: "#app",
components: {
cpn: {
template: '#cpn',
data() {
return {
pLanguage: ['Java','Python','C++','C#'],
}
}
}
}
})
</script>
效果如下所示:
注意
:data是可以随便定义的。