一、组件的嵌套

将所有的代码逻辑全部放到一个组件中,代码是非常的臃肿和难以维护的

并且在真实开发中,会有更多的内容和代码逻辑,对于扩展性和可维护性来说都是非常差的

所以,在真实的开发中,会对组件进行拆分,拆分成一个个功能的小组件

App.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<div class="app">
<Header></Header>
<Main></Main>
</div>
</template>

<script>
// 导入组件
import Header from "./Header.vue";
import Main from "./Main.vue";

export default {
// 注册组件
components: { Header, Main },
};
</script>

<style lang="scss" scoped></style>

推荐两个 vscode 对于 vue 的插件

  • vetur :高亮等
  • Vue VSCode Snippets : 代码片段等

css 作用域

如果定义了父子组件

父组件中有一个 h2 标签且给了 css 样式

那么子组件也会有相应的样式

怎么解决?

子组件外边嵌套一个根元素。

二、父子组件通信

父组件传递给子组件:通过 props 属性

子组件传递给父组件:通过$emit 触发事件

父传子

Props 是你可以在组件上注册一些自定义的 attribute

父组件给这些 attribute 赋值,子组件通过 attribute 的名称获取到对应的值

Props 有两种常见的用法:

方式一:字符串数组

  • 数组中的字符串就是 attribute 的名称

父组件 App,vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<template>
<div>
<show-message title="哈哈哈" content="我是哈哈哈"></show-message>
<show-message :n1Children="n1"></show-message>
<show-message :messageTitle="message.title"></show-message>
<show-message :messageTitle2="message"></show-message>
<show-message :="message"></show-message>
</div>
</template>

<script>
import ShowMessage from "./ShowMessage.vue";
export default {
data() {
return {
n1: "我是n1",
message: {
title: "我是message的title",
content: "我是message的content",
},
};
},
components: { ShowMessage },
};
</script>

子组件 ShowMessage.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<div>
<h2>{{ title }}</h2>
<h4>{{ content }}</h4>
<h4>{{ n1Children }}</h4>
<h4>{{ messageTitle }}</h4>
<h4>{{ messageTitle2 }}</h4>
</div>
</template>

<script>
export default {
props: ["title", "content", "n1Children", "messageTitle", "messageTitle2"],
};
</script>

页面效果:

注意:<show-message :="message"></show-message>这种写法会自动进行 message 的解构赋值,也就是解构出来 title,content 属性进行渲染。

方式二:对象类型

  • 对象类型我们可以在指定 attribute 名称的同时,指定它需要传递的类型、是否是必须的、 默认值等等
    • 当使用对象语法的时候,我们可以对传入的内容限制更多
    • 比如指定传入的 attribute 的类型
    • 比如指定传入的 attribute 是否是必传的
    • 比如指定没有传入时,attribute 的默认值
1
2
props: { title: String, content: { type: Number, required: true, defaulr: 1223,
}, },

type 的类型都可以是哪些呢

String

Number

Boolean

Array

Object

Date

Function

Symbol

对象类型的其他写法

为什么对象或数组必须是一个函数?

  • 引用赋值,避免修改一个影响其他引用

Prop 的大小写命名

HTML 中的 attribute 名是大小写不敏感的,所以浏览器会把所有大写字符解释为小写字符比如 class CLASS 一样的

这意味着当使用 DOM 中的模板时,camelCase (驼峰命名法) 的 prop 名需要使用其等价的 kebab-case (短 横线分隔命名) 命名,

1
2
<show-message messageTitle="1111"></show-message>
<show-message message-title="1111"></show-message>

非 Prop 的 Attribute

当我们传递给一个组件某个属性,但是该属性并没有定义对应的 props 或者 emits 时,就称之为 非 Prop 的 Attribute

常见的包括 class、style、id 属性等

Attribute 继承

当组件有单个根节点时,非 Prop 的 Attribute 将自动添加到根节点的 Attribute 中

1
2
3
4
5
6
7
8
9
<template>
<div>
<h2>{{ title }}</h2>
<h4>{{ content }}</h4>
<h4>{{ n1Children }}</h4>
<h4>{{ messageTitle }}</h4>
<h4>{{ messageTitle2 }}</h4>
</div>
</template>

禁用 Attribute 继承和多根节点

如果不希望组件的根元素继承 attribute,可以在组件中设置 inheritAttrs: false

禁用 attribute 继承的常见情况是需要将 attribute 应用于根元素之外的其他元素

可以通过 $attrs 来访问所有的非 props 的 attribute

ShowMessage.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<div>
<h2>{{ title }}</h2>
<h4>{{ content }}</h4>
<h4>{{ n1Children }}</h4>
<h4>{{ messageTitle }}</h4>
<h4>{{ messageTitle2 }}</h4>
<div :class="$attrs.class"></div>
</div>
</template>

<script>
export default {
inheritAttrs: false,
props: ["title", "content", "n1Children", "messageTitle", "messageTitle2"],
};
</script>

<style lang="scss" scoped></style>

多个根节点的 attribute

多个根节点的 attribute 如果没有显示的绑定,那么会报警告

必须手动的指定要绑定到哪一个属性上

1
2
3
4
5
<template>
<h2></h2>
<h2></h2>
<h2 :class="$attrs.class"></h2>
</template>

子传父

什么情况下子组件需要传递内容到父组件

  • 当子组件有一些事件发生的时候,比如在组件中发生了点击,父组件需要切换内容
  • 子组件有一些内容想要传递给父组件的时候

如何完成

  • 首先,我们需要在子组件中定义好在某些情况下触发的事件名称
1
2
3
4
5
6
7
8
9
10
11
<div>
<button @click="increment">+ 1</button
><button @click="decrement">- 1</button>
<button @click="decrementN">+ N</button>
</div>

<script>
export default {
emits: ["add", "sub", "addN"],
};
</script>
  • 其次,在父组件中以 v-on 的方式传入要监听的事件名称,并且绑定到对应的方法中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<template>
<div>计数:{{ counter }}</div>
<counter-operation
@add="addCounter"
@sub="subCounter"
@addN="addCounterN"
></counter-operation>
</template>

<script>
import CounterOperation from "./CounterOperation.vue";
export default {
data() {
return {
counter: 0,
};
},
components: {
CounterOperation,
},
methods: {
addCounter() {
this.counter++;
},
subCounter() {
this.counter--;
},
addCounterN(num, name, age) {
console.log(num, name, age);
this.counter += num;
},
},
};
</script>
  • 最后,在子组件中发生某个事件的时候,根据事件名称触发对应的事件,通过 this.$emit 的方式发出去事件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script>
export default {
emits: ["add", "sub", "addN"],
methods: {
increment() {
this.$emit("add");
},
decrement() {
this.$emit("sub");
},
decrementN(num) {
this.$emit("addN", 100, "xxx", 27);
},
},
};
</script>

vue3 当中,我们可以对传递的参数进行验证,一般使用对象写法

1
2
// emit: ['add', 'sub','addN'], emits: { add: null, sub: null, addN: (num, name,
age) => { if (num > 10) { return true; } return false; }, },

案例练习

点击 tar 栏不同选项切换页面

App.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<template>
<div>
<tar-control :titles="titles" @click="contentT"></tar-control>
<h4>{{ contents[currentIndex] }}</h4>
</div>
</template>

<script>
import TarControl from "./TarControl.vue";
export default {
data() {
return {
currentIndex: 0,
titles: ["衣服", "鞋子", "裤子"],
contents: ["衣服页面", "鞋子页面", "裤子页面"],
};
},
components: {
TarControl,
},
// 监听发射的事件后进行的操作
methods: {
contentT(index) {
this.currentIndex = index;
},
},
};
</script>

<style lang="scss" scoped></style>

TarControl.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<template>
<div class="tar">
<template v-for="(title, index) in titles" :key="title">
<span
class="tar-item"
:class="{ active: currentIndex === index }"
@click="itemClick(index)"
@clilk="titleClick"
>{{ title }}</span
>
</template>
</div>
</template>

<script>
export default {
//定义发射事件名称
emits: ["click"],
data() {
return {
currentIndex: 0,
};
},
props: {
titles: {
type: Array,
default() {
return [];
},
},
},
methods: {
//修改下标 发射事件
itemClick(index) {
(this.currentIndex = index), this.$emit("click", index);
},
},
};
</script>

<style scoped>
.tar {
display: flex;
/* justify-content: center; */
text-align: center;
}
.tar-item {
flex: 1;
}
.tar-item.active {
color: blue;
border-bottom: 3px solid blue;
}
</style>

三、非父子组件通信

provide/inject

Provide/Inject 用于非父子组件之间共享数据

比如有一些深度嵌套的组件,子组件想要获取父组件的部分内容

在这种情况下,如果仍然将 props 沿着组件链逐级传递下去,就会非常的麻烦

对于这种情况下,可以使用 Provide 和 Inject

无论层级结构有多深,父组件都可以作为其所有子组件的依赖提供者

  • 父组件有一个 provide 选项来提供数据
  • 子组件有一个 inject 选项来开始使用这些数据

实际上,可以将依赖注入看作是“long range props”

  • 父组件不需要知道哪些子组件使用它 provide 的 property
  • 子组件不需要知道 inject 的 property 来自哪里

基本使用

函数的写法

如果 Provide 中提供的一些数据是来自 data,那么我们可能会想要通过 this 来获取

这个时候会报错,因为这里的 this 指向的是上层作用域中的也就是 undefined

所以:

1
2
3
export default { data() { return { names: ['xxx', 'rrr', 'ttt'], }; }, provide()
{ return { name: 'xxx', age: 20, length: this.names.length, }; }, components: {
Home, }, };

处理响应式数据

如果修改了 this.names 的内容,会发现对应的子组件中是没有反应的

这是因为当修改了 names 之后,之前在 provide 中引入的 this.names.length 本身并不是响应式的

那么怎么样可以让我们的数据变成响应式的呢?

  • 可以使用响应式的一些 API 来完成这些功能,比如说 computed 函数
  • computed 是 vue3 的新特性

注意:在使用 length 的时候需要获取其中的 value,这是因为 computed 返回的是一个 ref 对象,需要取出其中的 value 来使用

不过目前不需要.value 了,会自动转换 直接 length 就可以