在VUE中,组件是一个很重要的设计思想。为什么会有组件,为什么这么设计,明白了这些才能把组件用好。
1、组件理解
一个应用的功能往往是比较复杂的,需要多个功能点相互配合。如果这些功能都设计在一个VUE对象中,可以想象,这个对象是极其庞大的。为了便于管理,我们可以将功能拆分为不同的小块,最后再组装到一起。每一个小块我们可以理解为一个组件,功能复杂的组件可以继续分拆为更小的组件。
这样做的好处是我们只需要关注如何合理拆分以及拆分后的每个小功能即可。更重要的,我们可以在不同的地方重用这些组件,使我们的代码更方便组织管理,并且扩展性也更强。
2、基本使用过程
组件是如何被创造,然后使用的呢?组件的使用可以分为3个步骤:定义、注册、使用。
a、组件的定义是通过Vue.extend()方法来实现的:
比如我们定义一个简单的组件my-cpn:
// 1、定义组件
const cpnC = Vue.extend({
template: `
<div>
<h2>我是主题</h2>
<p>我是内容,哈哈哈哈</p>
</div>
`
});
// 2、注册组件
Vue.compent('my-cpn', cpnC);
// 3、使用组件
// 在需要的地方,<my-cpn></my-cpn> 即可引用组件
b、组件的作用域
以上的做法,实际上一般是不会这么用的,html代码卸载template中,这使用起来太不方便了,一般我们会使用语法糖的写法:
上述步骤中,注册组件Vue.compent()会将组件注册到vue全局对象上去,这范围太大了,实际我们可能只需要注册到自己的第一层父组件即可。vue对象有components属性,注册到这里,即相当于只注册了这个vue实例:
const app = new Vue({
el: '#app',
data: {},
components: {
my-cpn: cpnC; //key为标签名,value为组件构造器
}
})
注册到某个具体的vue实例的组件,即为局部组件。
c、父子组件
多层组件嵌套,会有什么现象呢?‘孙子’组件会不会在‘爷爷’组件中仍然可用?
// 1、第一个组件
const cpn1 = Vue.extend({
template: `
<div>
<h2>我是主题1</h2>
<p>我是内容,哈哈哈哈11111</p>
</div>
`
});
// 2、第二个组件
const cpn2 = Vue.extend({
template: `
<div>
<h2>我是主题2</h2>
<p>我是内容,哈哈哈哈22222</p>
</div>
`,
components: {
myCpn1: cpn1;
}
});
// 顶层页面
const app = new Vue({
el: '#app',
data: {
data: 'hello world'
},
components: {
myCpn2: cpn2
}
});
定义如上,那么在app所在的页面中,能否直接使用
因为实际上在cpn2中,经过编译后,会将cpn1的内容整合到template中,而不是以一个单独的组件的形式出现。
d、语法糖写法
//全局
Vue.component('cpn1',{
template: `
<div>
<h2>我是主题1</h2>
<p>我是内容,哈哈哈哈11111</p>
</div>`
});
//局部
const app = new Vue({
el: '#app',
data: {
data: 'hello world'
},
components: {
myCpn2: {
template: `
<div>
<h2>我是主题2</h2>
<p>我是内容,哈哈哈哈22222</p>
</div>
`,
components: {
myCpn1: cpn1;
}
}
}
});
额,,,貌似只是方便了一丢丢?能不能把html扔出去,这太难受了。。。当然可以的。
e、组件模板的抽离写法
Vue提供了两种方案来定义html模块内容:
1、使用<script>标签(注意type类型);
2、使用<template>标签;
<!--script写法-->
<script type="text/x-template" id="cpn">
<div>
<h2>我是标题</h2>
<p>我是内容,哈哈哈</p>
</div>
</script>
<!--template写法-->
<template id="cpn2">
<div>
<h2>我是标题</h2>
<p>我是内容,哈哈哈</p>
</div>
</template>
Vue.compent('cpn',{
template: '#cpn'
});
注意type="text/x-template",且id跟下方的#cpn一致;
f、data为啥必须是函数
以上内容中,组件中的数据是写死的。现实中,这显然不能满足我们的要求。
如果数据是动态的,应该怎么写呢?
<template id="cpn">
<span>{{message}}</span>
</template>
Vue.compent('cpn',{
template: '#cpn',
data(){
return {
message: "I am compent"
}
}
});
整个结构跟vue对象是非常类似的,但data从js对象变成了函数。为什么这么设计呢?
这么写是为了让组件的每个实例的数据不相互干扰。
注意:
1、子组件不能直接使用父组件数据;
2、组件为了保证实例数据的隔离,应避免return 共享变量;
g、父子组件通信
子组件是不能直接访问父组件的数据的。但开发中,往往一些数据需要从上层传递到下层,这就涉及到组件间的数据通信问题。
父传子: 通过props
子传父: 通过事件发射$emit
<div id="app">
<cpn :cmovies="movies" :cmessage="message"></cpn>
</div>
<template id="cpn">
<div></div>
</template>
const cpn = {
template: '#cpn',
props: ['cmovies', 'cmessage'],
data(){},
methods:{}
}
const app = new Vue({
el: '#app',
data: {
message: '雷吼啊',
movies: ['火影',海贼王']
},
components: {
cpn
}
})
子组件要用v-bind将两个变量进行绑定。props有两种写法:
1、字符串数组,数组中的字符就是传递的变量名。
2、对象,对象可以设置传递时的类型,也可以设置默认值等。
示例:
Vue.componment('my-component',{
props: {
propA: Number,
propB: [String, Number], //类型为string或者number
propC: {
type: String,
required: true //该字段必须,缺失将报错
},
propD: {
type: Number,
default: 100
},
propE: {
type: Object,
default(){ //对象跟数组的默认值,需要用函数实现--变量隔离防止相互污染
return {message: 'hello'}
}
},
propF: {
validator(value){ //数据校验
return ['success','warning','danger'].indexOf(value) != -1;
}
}
}
})
当然,除了js默认的数据类型,vue组件也支持自定义类型,比如我们可以自定义一个类Persion,然后指定type为Person也是可以的。
注意:
vue标签跟html标签一样,不支持驼峰命名。而props中的变量名是要在组件使用时在标签书写的,这会导致变量名称不匹配的问题。vue的解决办法是标签中的驼峰用短横线代替,比如myChildName,在vue标签中应写作my-child-name。在其它地方,比如{{}}中则不受此限制,仍然可以使用驼峰。
<div id="app">
<cpn @itemclick="cpnclick"></cpn> <!--响应自定义itemclick事件-->
</div>
<template id="cpn">
<div>
<button v-for="item in categories" @click="btnClick(item)">
{{item.name}}
</button>
</div>
</template>
const cpn = {
template: '#cpn',
data(){
return {
categories:[
{id:'aaa', name:'热门推荐'},
{id:'bbb', name:'手机数码'},
{id:'ccc', name:'家用家电'},
{id:'ddd', name:'电脑办公'},
{id:'eee', name:'休闲户外'},
]
}
},
methods:{
btnClick(item){
thiss.$emit("itemclick", item); //发射事件(把自定义事件传出去)
}
}
}
const app = new Vue({
el: '#app',
componments:{cpn},
methods:{
cpnclick(item){
console.log(item);
}
}
});
自定义事件,父组件中默认第一个参数是子组件传递的数据,而不是event。浏览器自带的事件,默认第一个参数是event。
注意,自定义事件一样有驼峰命名不支持的问题。但脚手架书写的vue是可以的,因为脚手架会重新编译我们写的代码。
父子组件的数据双向绑定,怎么做?
直接在子组件绑定父组件传过来的数据,控制台会报错,而且只能在子组件数据绑定,无法绑定父组件。
正确做法是在data中定义新变量,初始化为父组件传过来的值,然后定义事件,将变化传递给父组件,再在父组件进行相应的操作。
h、父子组件对象的直接访问
父访问子:$children或者$refs,注意$children是数组,$refs是对象,需要ref属性配合;
子访问父:$parent
<div id="app">
<cpn ref="aaa"></cpn>
</div>
<template id="cpn">
<span>子组件</span>
</template>
const app = new Vue({
el: '#app',
data: {
message: '你好'
},
methods: {
myclick(){
//children方式
console.log(this.$children[0].name);
//refs方式
console.log(this.$refs.aaa.name);
}
},
components: {
cpn: {
template: '#cpn',
data(){
return {
name: "子组件的name"
}
}
}
}
});
$refs可以跟标签配合,精准拿到指定的子组件对象,相对来说,比$children好用,因为很多时候组件的下标是不固定的。父组件可以直接$parent调用,是一个对象,根组件可以通$root调用。
$parent相对来说用的很少(其实这几个都不常用),因为vue拆分组件的目的就是代码的可复用性,而一旦有了$parent这样的代码,实际是将当前组件跟父组件实现了强耦合,这是非常不推荐的。
3、slot 插槽
组件中的有些内容可能并不是固定的,有些内容需要根据外部父组件的不同需求进行调整,这局需要我们在子组件中预留位置,这个预留的位置就是slot(插槽)。
一个组件可以拥有多个slot。
父组件给slot填充的内容中的数据可能来自slot,这就会有数据传输的问题,这就是作用域插槽的功能了。
<div id = "app">
<cpn1>
<span slot='aaa'>1111</span> <!--注意slot跟name值一致-->
<span slot='bbb'>222</span>
</cpn1>
<cpn2>
<template slot-scope="slot">
<p v-for="item in slot.mymovies">{{item}} </p> <!--注意mymovies跟:mymovies一致-->
</template>
</cpn2>
</div>
<!--组件1-->
<template id = "cpn1">
<div>
<slot name = "aaa"></slot>
<slot name = "bbb"></slot>
</div>
</template>
<!--组件2-->
<template id = "cpn2">
<div>
<slot :mymovies="movies" >
<ul>
<li v-for="item in movies">{{item}} </li>
</ul>
</slot>
</div>
</template>
const cpn1 = Vue.extend({
template: '#cpn1',
});
const cpn2 = Vue.extend({
template: '#cpn2',
data(){
return {movies: ['魔戒再现','双塔奇兵','王者归来']}
}
});
const app = new Vue({
el: '#app',
data: {msg:"message"},
components: {
cpn1, cpn2
}
});
组件部分,over