Vue学习笔记(三)— 组件化开发
一、组件简介
组件(Component)是Vue中非常强大的一种功能,用来封装可重用的代码,方便在不同的位置进行调用。组件注册后,便可以HTML标签的形式使用。总的来说,组件就是把一个页面划分成一个个的小零件,然后通过这些可重复使用的小零件来构建页面。可以避免相同代码的重复书写。
二、组件注册
1、全局注册
基础语法: Vue.component(‘组件名称’,{}) 第一个参数是所注册组件的名称,用单引号包裹,第二个参数是一个选项对象。全局注册的组件为全局组件,任何Vue实例都可以使用。
代码演示:
<div id="app">
<!--
4、 组件可以重复使用多次
因为data中返回的是一个对象所以每个组件中的数据是私有的
即每个实例可以维护一份被返回对象的独立的拷贝
-->
<button-counter></button-counter>
<button-counter></button-counter>
<button-counter></button-counter>
<!-- 8、如果组件名是驼峰式命名,必须使用短横线的方式使用组件 -->
<hello-world></hello-world>
</div>
<script type="text/javascript">
//5 如果使用驼峰式命名组件,那么在使用组件的时候,只有在字符串模板中才用驼峰的方式使用组件
// 7、在普通的标签模板中,必须使用短横线的方式使用组件
Vue.component('HelloWorld', {
data: function(){
return {
msg: 'HelloWorld'
}
},
template: '<div>{{msg}}</div>'
});
Vue.component('button-counter', {
// 1、组件参数的data值必须是函数
// 同时这个函数要求返回一个对象
data: function(){
return {
count: 0
}
},
// 2、组件模板必须是单个根元素
// 3、组件模板的内容可以是模板字符串
template: `
<div>
<button @click="handle">点击了{{count}}次</button>
<button>测试123</button>
# 6 在字符串模板中可以使用驼峰的方式使用组件
<HelloWorld></HelloWorld>
</div>
`,
methods: {
handle: function(){
this.count += 2;
}
}
})
var vm = new Vue({
el: '#app',
data: {
}
});
</script>
注意事项: 组件参数中的data必须是一个函数,并且返回值必须为一个对象。如果一个组件data中没有数据,可以省略不写。组件模板template中的所有内容要放在同一个根元素中,组件模板的内容可用模板字符串(如组件 button-counter),也可以用引号包裹(如组件)HelloWorld。组件可重复使用,每次使用生成一个实例对象,各对象之间相互独立。在使用组件的时候,只有在字符串模板中才用驼峰的方式使用组件,其他时候都使用短横线的方式。
2、局部注册
基础语法: new Vue({ components: { ‘组件名’: 组件模板} })。组件模板需要在该Vue实例前进行定义。局部注册的组件称为局部组件,只能在注册他的Vue实例中使用。
代码演示:
<div id="app">
<my-component></my-component>
</div>
<script>
// 定义组件的模板
var Child = {
template: '<div>A custom component!</div>'
}
new Vue({
//局部注册组件
components: {
// <my-component> 将只在父模板可用 一定要在实例上注册了才能在html文件中使用
'my-component': Child
}
})
</script>
三、Vue各组件之间进行传值
1、父组件向子组件传值
父组件发送数据时是以子组件标签属性的方式绑定数据到子组件身上,然后子组件在组件中通过props接收,并且props中的值,可直接调用。传的值可以是数字、对象、数组等等,可以是动态的数据也可以是静态的。props 是组件的自定义属性,在封装通用组件的时,合理使用props可以极大的提高组件的复用性。
代码演示:
<div id="app">
<div>{{pmsg}}</div>
<!--1、menu-item 在 app中嵌套着 故 menu-item 为 子组件 app为父组件 -->
<!-- 给子组件传入一个静态的值 -->
<menu-item title='来自父组件的值'></menu-item>
<!-- 2、 需要动态的数据的时候 需要属性绑定的形式设置 此时 ptitle 来自父组件data 中的数据 .
-->
<menu-item :title='ptitle' content='hello'></menu-item>
</div>
<script type="text/javascript">
Vue.component('menu-item', {
// 3、 子组件用属性props接收父组件传递过来的数据
props: ['title', 'content'],
data: function() {
return {
msg: '子组件本身的数据'
}
},
template: '<div>{{msg + "----" + title + "-----" + content}}</div>'
});
var vm = new Vue({
el: '#app',
data: {
pmsg: '父组件中内容',
ptitle: '动态绑定属性'
}
});
</script>
props补充:
① props 是只读的,开发者不能直接修改props中的值,想要对props中的值进行修改,可以将值赋给data中声明的变量,然后对变量进行操作。例如:
ewport default {
props: ['init'],
data() {
return {
// 把props中 init 的值转存到 init2 中
init2: this.init
}
}
}
② props 在声明自定义属性时,可以通过 defult 来定义属性的默认值。例如:
export default {
props: {
init: {
// 默认值为 0
default: 0
}
}
}
但如果props设置的默认值为非原始值(即复杂数据类型)时,如果直接设置,那么该值将会成为,所有当前子组件实例之间共享的引用,相互之间会产生影响。因此我们需要对非原始值(复杂数据类型)使用一个工厂方法,以便于每次使用默认值时都获得一个新的副本,而不是共享同一个引用。
所谓工厂方法,就是一个return返回非原始值(即复杂数据类型)的函数方法,通常使用箭头函数的形式,例如:
export default {
props: {
init: {
// 默认值为 数组对象 复杂数据类型
default: () => [1,2,3]
}
}
}
③ props 在声明自定义属性时,可以通过 type 来定义属性的值类型。例如:
export default {
props: {
init: {
// 默认值为0
default: 0,
// 用type 属性定义属性的值类型
// 如果传递过来的值不符合此类型,则会报错。
type: Number
},
info: {
// 默认值为一个对象
default: () => {},
// 用type 属性定义属性的值类型
// 可以用数组定义一个类型集合 只要属于该集合中的任何一个类型 都不会报错
type: [Object, Number]
}
}
}
④ props 在声明自定义属性是,可以通过 required 选项,设置该属性为必填项,必须传递该属性。
export default {
props: {
init: {
// 默认值为0
default: 0,
// 用type 属性定义属性的值类型
// 如果传递过来的值不符合此类型,则会报错。
type: Number,
// 该属性为必填项
required: true
}
}
}
2、子组件向父组件传值
子组件通过 $emit( ‘事件名称’ , 传递的数据) 触发事件,将数据向父组件传递。第一个参数是自定义的事件名称,第二个参数是传递的数据。父组件在子组件的标签中用 v-on 来监听子组件是否触发事件,传递数据。监听到子组件触发事件后,调用父组件的事件,对传递过来的数据进行处理。
代码演示:
<div id="app">
<div :style='{fontSize: fontSize + "px"}'>{{pmsg}}</div>
<!-- 2 父组件用v-on 监听子组件的事件
这里 enlarge-text 是从 $emit 中的第一个参数对应 handle 为对应的事件处理函数
-->
<menu-item :parr='parr' @enlarge-text='handle($event)'></menu-item>
</div>
<script type="text/javascript" src="js/vue.js"></script>
<script type="text/javascript">
/*
子组件向父组件传值-携带参数
*/
Vue.component('menu-item', {
props: ['parr'],
template: `
<div>
<ul>
<li :key='index' v-for='(item,index) in parr'>{{item}}</li>
</ul>
// 1、子组件用$emit()触发事件
//第一个参数为 自定义的事件名称 第二个参数为需要传递的数据
<button @click='$emit("enlarge-text", 5)'>扩大父组件中字体大小</button>
<button @click='add()'>扩大父组件中字体大小</button>
</div>
`,
methods: {
add() {
this.$emit('enlarge-text',10);
}
}
});
var vm = new Vue({
el: '#app',
data: {
pmsg: '父组件中内容',
parr: ['apple','orange','banana'],
fontSize: 10
},
methods: {
handle(val) {
// 扩大字体大小
this.fontSize += val;
}
}
});
</script>
3、兄弟组件之间进行传值
兄弟组件之间传递数据需要注册一个Vue实例作为事务中心,通过事务中心来传递数据。兄弟组件中传递数据的一方,通过事件触发 $emit( ‘方法名’ , 传递的数据 ) 将数据传出。接收数据的一方,通过 $on(方法名) 来接收数据 。最后可以通过 $off(方法名) 来销毁 $emit 中创建的方法,使其无法再传递数据。
代码演示:
<div id="app">
<div>父组件</div>
<div>
<button @click='handle'>销毁事件</button>
</div>
<test-tom></test-tom>
<test-jerry></test-jerry>
</div>
<script type="text/javascript" src="js/vue.js"></script>
<script type="text/javascript">
/*
兄弟组件之间数据传递
*/
//1、 提供事件中心
var hub = new Vue();
Vue.component('test-tom', {
data: function(){
return {
num: 0
}
},
template: `
<div>
<div>TOM:{{num}}</div>
<div>
<button @click='handle'>点击</button>
</div>
</div>
`,
methods: {
handle: function(){
//2、传递数据方,通过一个事件触发hub.$emit(方法名,传递的数据) 触发兄弟组件的事件
hub.$emit('jerry-event', 2);
}
},
created: function() {
// 3、接收数据方,通过created(){} 钩子中 触发hub.$on(方法名
hub.$on('tom-event', (val) => {
this.num += val;
});
}
});
Vue.component('test-jerry', {
data: function(){
return {
num: 0
}
},
template: `
<div>
<div>JERRY:{{num}}</div>
<div>
<button @click='handle'>点击</button>
</div>
</div>
`,
methods: {
handle: function(){
//2、传递数据方,通过一个事件触发hub.$emit(方法名,传递的数据) 触发兄弟组件的事件
hub.$emit('tom-event', 1);
}
},
mounted: function() {
// 3、接收数据方,通过mounted(){} 钩子中 触发hub.$on()方法名
hub.$on('jerry-event', (val) => {
this.num += val;
});
}
});
var vm = new Vue({
el: '#app',
data: {
},
methods: {
handle: function(){
//4、销毁事件 通过hub.$off()方法名销毁之后无法进行传递数据
hub.$off('tom-event');
hub.$off('jerry-event');
}
}
});
</script>
4、父子组件之间直接调用对方的变量和方法
① 在子组件中直接调用父组件的变量和方法
如果我们想要在子组件中直接去调用父组件的变量和方法,而且不想借用this.$emit
这种麻烦的方法,我们可以通过 this.$parent
来实现:
// 父组件中的方法和变量
data () {
return {
father: 'me'
}
},
methods: {
toLog () {
console.log('这是父组件中的方法')
}
}
// 在子组件中调用父组件的方法和变量
data () {
return {
son: 'you'
}
},
methods: {
toFather () {
console.log('子组件调用父组件中的变量')
this.son = this.$parent.father
console.log('son=' + this.son)
console.log('子组件调用父组件中的方法')
this.$parent.toLog()
}
}
② 在父组件中调用子组件的变量和方法
如果我们想要在父组件中,直接调用子组件的方法和变量,我们可以通过$ref
来引用子组件在父组件中的dom
元素,然后再直接调用对应的变量和方法的方式来实现,例如:this.$ref.sonRef.变量
和this.$ref.sonRef.方法()
。
但要注意的就是在调用子组件的属性和方法的时候,子组件必须已经加载完成,否则无法访问到对应的变量和方法,推荐在父组件的mounted阶段或后续阶段再去直接调用,因为此时子组件必定已经加载完成:
// 子组件定义变量和方法
data () {
return {
son: 'son'
}
},
methods: {
test () {
console.log('这是子组件中方法,尝试是否能被父组件调用')
}
}
// 在父组件中通过ref调用子组件的变量和方法
<Son ref='son' />
mounted () {
// 在mounted阶段调用 因为子组件此时必定已经加载完成
console.log('调用子组件中的方法')
console.log(this.$refs.son.test())
console.log('调用子组件中的变量')
console.log(this.$refs.son.son)
}
四、组件插槽
组件最大的优势特性就是可复用性,组件插槽使用来提高组建可复用能力的。就是在组件模板中添加一个 < slot> 标签 ,然后就可以在使用组件标签的时,在标签中嵌套内容来替换掉slot中本来的内容,拓宽了组件的适用范围。插槽内可以包含任何模板代码,包括 HTML代码。组件插槽分为三种匿名插槽、具名插槽和作用域插槽。
1、匿名插槽
代码演示:
<div id="app">
<!-- 这里的所有组件标签中嵌套的内容会替换掉slot 如果不传值 则使用 slot 中的默认值 -->
<alert-box>有bug发生</alert-box>
<alert-box>有一个警告</alert-box>
<alert-box></alert-box>
</div>
<script type="text/javascript">
/*
组件插槽:父组件向子组件传递内容
*/
Vue.component('alert-box', {
template: `
<div>
<strong>ERROR:</strong>
# 当组件渲染的时候,这个 <slot> 元素将会被替换为“组件标签中嵌套的内容”。
# 插槽内可以包含任何模板代码,包括 HTML
<slot>默认内容</slot>
</div>
`
});
var vm = new Vue({
el: '#app',
data: {
}
});
</script>
</body>
</html>
2、具名插槽
具有名字的插槽,通过 < slot> 标签中的name属性来绑定元素,如果父组件中没有设定slot的值或者slot的值与子组件中的slot的name属性不匹配,则会把相关内容放到匿名插槽中使用。父组件中的 < template> 标签是一个临时的盒子标签,用来将多个普通标签包裹起来,作为一个整体传向相应的插槽,而且其本身并不会在页面加载时进行渲染。具名插槽的渲染顺序,取决于模板中的顺序,而跟父组件中元素的顺序无关。
代码演示:
<div id="app">
<base-layout>
<!-- 2、 通过slot属性来指定, 这个slot的值必须和下面slot组件得name值对应上
如果没有匹配到 则放到匿名的插槽中 -->
<p slot='header'>标题信息</p>
<p>主要内容1</p>
<p>主要内容2</p>
<p slot='footer'>底部信息信息</p>
</base-layout>
<base-layout>
<!-- 注意点:template临时的包裹标签最终不会渲染到页面上 -->
<template slot='header'>
<p>标题信息1</p>
<p>标题信息2</p>
</template>
<p>主要内容1</p>
<p>主要内容2</p>
<template slot='footer'>
<p>底部信息信息1</p>
<p>底部信息信息2</p>
</template>
</base-layout>
</div>
<script type="text/javascript" src="js/vue.js"></script>
<script type="text/javascript">
/*
具名插槽
*/
Vue.component('base-layout', {
template: `
<div>
<header>
### 1、 使用 <slot> 中的 "name" 属性绑定元素 指定当前插槽的名字
<slot name='header'></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name='footer'></slot>
</footer>
</div>
`
});
var vm = new Vue({
el: '#app',
data: {
}
});
</script>
</body>
</html>
3、作用域插槽
主要应用于父组件对子组件的内容进行加工处理的场景。即在父组件中通过slot-scope来获得子组件中v-bind绑定的数据,然后通过条件判断,对不同的数据进行不同的处理。
代码演示:
<div id="app">
<!--
1、当我们希望li 的样式由外部使用组件的地方定义,因为可能有多种地方要使用该组件,
但样式希望不一样 这个时候我们需要使用作用域插槽
-->
<fruit-list :list='list'>
<!-- 2、 父组件中使用了<template>元素,而且包含scope="slotProps",这个属性是用来获取子组件中v-bind所绑定的数据。slotProps在这里只是临时变量,名称是自定义的。
--->
<template slot-scope='slotProps'>
<strong v-if='slotProps.info.id==3' class="current">
{{slotProps.info.name}}
</strong>
<span v-else>{{slotProps.info.name}}</span>
</template>
</fruit-list>
</div>
<script type="text/javascript" src="js/vue.js"></script>
<script type="text/javascript">
Vue.component('fruit-list', {
props: ['list'],
template: `
<div>
<li :key='item.id' v-for='item in list'>
### 3、 在子组件模板中,<slot>元素上有一个通过v-bind绑定的属性,属性名是自定义的。
### 插槽可以提供一个默认内容,如果如果父组件没有为这个插槽提供了内容,会显示默认的内容。如果父组件为这个插槽提供了内容,则默认的内容会被替换掉
<slot :info='item'>{{item.name}}</slot>
</li>
</div>
`
});
var vm = new Vue({
el: '#app',
data: {
list: [{
id: 1,
name: 'apple'
},{
id: 2,
name: 'orange'
},{
id: 3,
name: 'banana'
}]
}
});
</script>
</body>
</html>