本文整理来自深入Vue3+TypeScript技术栈-coderwhy大神新课,只作为个人笔记记录使用,请大家多支持王红元老师。
认识组件的嵌套
前面我们是将所有的逻辑放到一个App.vue组件中,如果我们将所有的代码逻辑都放到一个App.vue组件中,我们会发现,代码是非常的臃肿和难以维护的。并且在真实开发中,我们会有更多的内容和代码逻辑,对于扩展性和可维护性来说都是非常差的。所以,在真实开发中,我们会对组件进行拆分,拆分成一个个功能的小组件,再将这些组件组合嵌套在一起,最终形成我们的应用程序。
组件的拆分
原来的代码:
我们可以按照如下的方式进行拆分:
按照如上的拆分方式后,我们开发对应的逻辑只需要去对应的组件编写就可。
推荐插件
Vue代码高亮的插件:Vetur、Volar。
代码片段插件:Vue VSCode Snippets、Vue3 Snippets。
对于组件的导入,不加后缀名也不会报错,因为VueCLI是基于webpack的,而webpack又有resolve.extensions用来解析扩展名,Vue已经内置在extensions里面添加了.vue,所以不写.vue后缀名也可以。
但是最好带上后缀名,比如:import Header from './Header.vue';
,如果不带有两个问题:
- 使用组件的时候没有提示
- 点击路径不会跳转到对应组件代码
如果加上.vue后缀就没有上面两个问题了。
Vue3的scoped偶尔失效的问题
vue2中我们给样式添加scoped就会避免样式被污染的问题,这是因为标签上被添加上了一个属性,设置样式的时候用了属性选择器,有这个属性才会设置此样式。
但是vue3中经常会出现样式污染的问题,因为vue2中组件都有根元素,但是vue3中可以没有根元素,没有根元素的时候就会出现这个bug。因为没有根元素,上个组件的属性会穿透到下个组件,所以他们就有重复的属性了,样式就会被污染,如下:
所以,Vue3组件也要有个根元素。
组件的通信
上面的嵌套逻辑如下,它们存在如下关系:
App组件是Header、Main、Footer组件的父组件;
Main组件是Banner、ProductList组件的父组件;
在开发过程中,我们会经常遇到需要组件之间相互进行通信。比如App可能使用了多个Header,每个地方的Header展示的内容不同,那么我们就需要使用者传递给Header一些数据,让其进行展示。又比如我们在Main中一次性请求了Banner数据和ProductList数据,那么就需要传递给它们来进行展示。也可能是子组件中发生了事件,需要由父组件来完成某些操作,那就需要子组件向父组件传递事件。
总之,在一个Vue项目中,组件之间的通信是非常重要的环节,所以接下来我们就具体学习一下组件之间是如何相互之间传递数据的。
父子组件之间通信的方式
父子组件之间如何进行通信呢?
- 父组件传递给子组件:通过props属性;
- 子组件传递给父组件:通过$emit触发事件;
父组件给子组件传递数据
在开发中很常见的就是父子组件之间通信,比如父组件有一些数据,需要子组件来进行展示,这个时候我们可以通过props来完成组件之间的通信。
什么是Props呢?
Props是你可以在组件上注册一些自定义的attribute,父组件给这些attribute赋值,子组件通过attribute的名称获取到对应的值。
Props有两种常见的用法:
方式一:字符串数组,数组中的字符串就是attribute的名称;
方式二:对象类型,对象类型我们可以在指定attribute名称的同时,指定它需要传递的类型、是否是必须的、默认值等等;
Props的数组用法
上面是传递写死的值,如果传递data里面的值就需要进行绑定:
//直接绑定
<show-message :title="title" :content="content"></show-message>
//绑定对象的属性
<show-message :title="message.title" :content="message.content"></show-message>
//绑定对象,就会把对象的所有属性绑定到组件上,这种写法和上一行效果一样
<show-message v-bind="message"></show-message>
//数据
data() {
return {
title: "嘻嘻嘻",
content: "我是嘻嘻嘻嘻",
message: {
title: "嘿嘿嘿",
content: "我是嘿嘿嘿"
}
}
}
Props的对象用法
数组用法中我们只能说明传入的attribute的名称,并不能对其进行任何形式的限制,接下来我们来看一下对象的写法是如何让我们的props变得更加完善的,真实开发中我们就使用对象用法。
当使用对象语法的时候,我们可以对传入的内容限制更多:
- 比如指定传入的attribute的类型;
- 比如指定传入的attribute是否是必传的;
- 比如指定没有传入参数时,attribute的默认值;
那么type的类型都可以是哪些呢?
- String
- Number
- Boolean
- Array
- Object
- Date
- Function
- Symbol
Props对象语法补充
如果有多个可能的类型,按如下写法:
props: {
//可能是String也可能是Number
IDNumber: [String, Number]
}
- 对象或数组的默认值必须从一个工厂函数中获取。这是因为组件是复用的,如果默认传递一个对象,那么这个对象(引用类型)也会被其他组件引用,所以我们传递一个函数,返回一个对象。
- 我们也可以自定义验证函数,保证传递的值是我们指定的值。
Prop的大小写命名
- 如果是浏览器解析,因为HTML 中的 attribute 名是大小写不敏感的,所以下面camelCase (驼峰命名)方式的的messageInfo会被解析成messageinfo,就会有问题,所以我们可以换成等价的kebab-case (短横线分隔命名) 写法。
- 但是在.vue文件中,template是给vue-loader解析的,vue-loader解析的就不会有问题,所以在.vue文件中,两种写法都可以,但是官方还是推荐kebab-case (短横线分隔命名)。
非Prop的Attribute
什么是非Prop的Attribute呢?
当我们传递给一个组件某个属性,但是该属性并没有定义对应的props或者emits时,就称之为非Prop的Attribute,常见的包括class、style、id属性等。
Attribute继承:当组件有单个根节点时,非Prop的Attribute将自动添加到组件的根节点的Attribute中。想想也很容易理解,因为给组件添加属性没啥意义啊,所以只要组件有单个根节点就会把属性添加到单个根节点上。
如果我们不希望组件的根节点继承attribute,可以在组件中设置 inheritAttrs: false
。禁用attribute继承的常见情况是需要将attribute应用于根元素之外的其他元素。比如下面我们不想将class属性添加到div上,可以在组件中设置inheritAttrs: false
,然后通过$attrs来访问所有的非props的attribute,然后再设置到h2上就行了。
当非props的attribute比较多的时候,我们也可以直接绑定对象:
<div>
我是NotPropAttribute组件
<h2 v-bind="$attrs"></h2>
</div>
多个根节点的attribute如果没有显示的绑定,那么会报警告,我们必须手动的指定要绑定到哪一个属性上:
子组件给父组件传递数据
什么情况下子组件需要传递内容到父组件呢?
当子组件有一些事件发生的时候,比如在组件中发生了点击,父组件需要切换内容;
子组件有一些内容想要传递给父组件的时候;
我们如何完成上面的操作呢?
首先,我们需要在子组件中定义好在某些情况下触发的事件名称,其次,在父组件中以v-on的方式传入要监听的事件名称,并且绑定到对应的方法中,最后,在子组件中发生某个事件的时候,根据事件名称触发对应的事件。
传递流程
我们封装一个CounterOperation.vue的组件,内部其实是监听两个按钮的点击,点击之后通过this.$emit的方式发出去事件。
子组件通过$emit触发事件,并且通过emits指明我们都有哪些触发事件。
父组件通过v-on监听事件:
上面代码和vue2比,其实就是多了emits: ["add", "sub", "addN"]。
传递参数和验证
自定义事件的时候,我们也可以传递一些参数给父组件:
在vue3中,emits除了是数组,还可以是个对象,对象写法的目的是为了进行参数的验证。实际开发中,emits一般我们都是使用数组。
// emits: ["add", "sub", "addN"],
// 对象写法的目的是为了进行参数的验证
emits: {
add: null, //不需要验证
sub: null,
addN: (num, name, age) => {
console.log(num, name, age);
if (num > 10) {
return true
}
return false; //如果验证不通过,参数还是可以传过去,只不过会报警告
}
}
组件间通信案例练习
我们来做一个相对综合的练习:父组件给子组件传递标题数据,点击子组件通知父组件切换到相应页面。
父组件App.vue代码:
<template>
<div>
<tab-control :titles="titles" @titleClick="titleClick"></tab-control>
<h2>{{contents[currentIndex]}}</h2>
</div>
</template>
<script>
import TabControl from './TabControl.vue';
export default {
components: {
TabControl
},
data() {
return {
titles: ["衣服", "鞋子", "裤子"],
contents: ["衣服页面", "鞋子页面", "裤子页面"],
currentIndex: 0
}
},
methods: {
titleClick(index) {
this.currentIndex = index;
}
}
}
</script>
<style scoped>
</style>
子组件TabControl.vue代码:
<template>
<div class="tab-control">
<div class="tab-control-item"
:class="{active: currentIndex === index}"
v-for="(title, index) in titles"
:key="title"
@click="itemClick(index)">
<!-- span里面放标题, 设置下面的红色横线 -->
<span>{{title}}</span>
</div>
</div>
</template>
<script>
export default {
emits: ["titleClick"],
props: {
titles: {
type: Array,
default() {
return []
}
}
},
data() {
return {
currentIndex: 0
}
},
methods: {
itemClick(index) {
this.currentIndex = index;
this.$emit("titleClick", index);
}
}
}
</script>
<style scoped>
.tab-control {
display: flex;
}
.tab-control-item {
flex: 1;
text-align: center;
}
.tab-control-item.active {
color: red;
}
.tab-control-item.active span {
border-bottom: 3px solid red;
padding: 5px 10px;
}
</style>