四、Vue组件化开发
Lyk 2022/8/23 Vue组件化开发父子组件的通信方式父传子props子传父$emit事件总线mitt库
# 1、为什么需要组件化开发
# 1.1.人处理问题的方式
- 人面对复杂问题的处理方式:
- 任何一个人处理信息的逻辑能力都是有限的
- 所以,当面对一个非常复杂的问题时,我们不太可能一次性搞定一大堆的内容。
- 但是,我们人有一种天生的能力,就是将问题进行拆解。
- 如果将一个复杂的问题,拆分成很多个可以处理的小问题,再将其放在整体当中,你会发现大的问题也会迎刃而解。
# 1.2.认识组件化开发
- 组件化也是类似的思想:
- 如果我们将一个页面中所有的处理逻辑全部放在一起,处理起来就会变得非常复杂,而且不利于后续的管理以及扩展;
- 但如果,我们讲一个页面拆分成一个个小的功能块,每个功能块完成属于自己这部分独立的功能,那么之后整个页面的管理和维护就变得非常容易了;
- 如果我们将一个个功能块拆分后,就可以像搭建积木一下来搭建我们的项目;
# 1.3.组件化开发
- 现在可以说整个的大前端开发都是组件化的天下,
- 无论从三大框架(Vue、React、Angular),还是跨平台方案的Flutter,甚至是移动端都在转向组件化开发,包括小程序的开发也是采用组件化开发的思想。
- 所以,学习组件化最重要的是它的思想,每个框架或者平台可能实现方法不同,但是思想都是一样的。
- 我们需要通过组件化的思想来思考整个应用程序:
- 我们将一个完整的页面分成很多个组件;
- 每个组件都用于实现页面的一个功能块;
- 而每一个组件又可以进行细分;
- 而组件本身又可以在多个地方进行复用
# 2、Vue的组件化(基本了解使用)
# 2.1.Vue的组件化
- 组件化是Vue、React、Angular的核心思想,也是我们后续课程的重点(包括以后实战项目):
- 前面我们的createApp函数传入了一个对象App,这个对象其实本质上就是一个组件,也是我们应用程序的根组件;
- 组件化提供了一种抽象,让我们可以开发出一个个独立可复用的小组件来构造我们的应用;
- 任何的应用都会被抽象成一颗组件树;
- 接下来,我们来学习一下在Vue中如何注册一个组件,以及之后如何使用这个注册后的组件。
# 2.2.注册组件的方式
- 如果我们现在有一部分内容(模板、逻辑等),我们希望将这部分内容抽取到一个独立的组件中去维护,这个时候如何注册一个组件呢?
- 我们先从简单的开始谈起,比如下面的模板希望抽离到一个单独的组件:
<h2>{{message}}</h2>
<button>点击</button>
1
2
2
- 注册组件分成两种:
- 全局组件:在任何其他的组件中都可以使用的组件;
- 局部组件:只有在注册的组件中才能使用的组件;
# 2.3.注册全局组件
- 我们先来学习一下全局组件的注册:
- 全局组件需要使用我们全局创建的app来注册组件;
- 通过component方法传入组件名称、组件对象即可注册一个全局组件了;
- 之后,我们可以在App组件的template中直接使用这个全局组件:
- 当然,我们组件本身也可以有自己的代码逻辑:
- 比如自己的data、computed、methods等等
<div id="app">
<home-name></home-name>
<about-name></about-name>
<about-name/>
</div>
<template id="home">
<h2>{{message}}</h2>
</template>
<template id="about">
<h2>{{message}}</h2>
<button>点击</button>
</template>
<script src="https://unpkg.com/vue@next"></script>
<script>
const app = Vue.createApp({
data() {
return {
message:"Hello Vue3"
}
}
})
app.component("home-name",{//注册的是全局组件
template:'#home',
data() {
return {
message:'home message'
}
}
})
app.component("AboutName",{//注册的是全局组件
template:'#about',
data() {
return {
message:'about message'
}
}
})
app.mount("#app")
</script>
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
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
# 2.4.组件的名称
- 在通过app.component注册一个组件的时候,第一个参数是组件的名称,定义组件名的方式有两种:
- 方式一:使用kebab-case(短横线分割符)
- 当使用 kebab-case (短横线分隔命名) 定义一个组件时,你也必须在引用这个自定义元素时使用 kebab-case,例如
<my-component-name>
- 当使用 kebab-case (短横线分隔命名) 定义一个组件时,你也必须在引用这个自定义元素时使用 kebab-case,例如
<script>
app.component('my-component-name',{
/*...*/
})
</script>
1
2
3
4
5
2
3
4
5
- 方式二:使用PascalCase(驼峰标识符)
- 当使用 PascalCase (首字母大写命名) 定义一个组件时,你在引用这个自定义元素时两种命名法都可以使用。
- 也就是说
<my-component-name>
和<MyComponentName>
都是可接受的;
<script>
app.component('MyComponentName',{
/*...*/
})
</script>
1
2
3
4
5
2
3
4
5
# 2.5.注册局部组件
- 全局组件往往是在应用程序一开始就会全局组件完成,那么就意味着如果某些组件我们并没有用到,也会一起被注册:
- 比如我们注册了三个全局组件:ComponentA、ComponentB、ComponentC;
- 在开发中我们只使用了ComponentA、ComponentB,如果ComponentC没有用到但是我们依然在全局进行了注册,那么就意味着类似于webpack这种打包工具在打包我们的项目时,我们依然会对其进行打包;
- 这样最终打包出的JavaScript包就会有关于ComponentC的内容,用户在下载对应的JavaScript时也会增加包的大小;
- 所以在开发中我们通常使用组件的时候采用的都是局部注册:
- 局部注册是在我们需要使用到的组件中,通过components属性选项来进行注册;
- 比如之前的App组件中,我们有data、computed、methods等选项了,事实上还可以有一个components选项;
- 该components选项对应的是一个对象,对象中的键值对 是 组件的名称: 组件对象;
# 2.6.局部组件注册代码
<div id="app">
<home-name></home-name>
<about-name></about-name>
<home-name/>
</div>
<template id="home">
<h2>{{message}}</h2>
</template>
<template id="about">
<h2>{{message}}</h2>
<button>点击</button>
</template>
<script src="https://unpkg.com/vue@next"></script>
<script>
const HomeName = {
template: "#home",
data() {
return {
message: 'home message'
}
}
}
const AboutName = {
template: "#about",
data() {
return {
message: 'about message'
}
}
}
const app = Vue.createApp({
components: {//注册局部组件
'home-name': HomeName,
AboutName
},
data() {
return {
message: "Hello Vue3"
}
}
})
app.mount("#app")
</script>
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
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
# 2.7.在脚手架搭建的项目中(局部/全局组件注册)
# 3、Vue的开发模式
- 目前我们使用vue的过程都是在html文件中,通过template编写自己的模板、脚本逻辑、样式等。
- 但是随着项目越来越复杂,我们会采用组件化的方式来进行开发:
- 这就意味着每个组件都会有自己的模板、脚本逻辑、样式等;
- 当然我们依然可以把它们抽离到单独的js、css文件中,但是它们还是会分离开来;
- 也包括我们的script是在一个全局的作用域下,很容易出现命名冲突的问题;
- 并且我们的代码为了适配一些浏览器,必须使用ES5的语法;
- 在我们编写代码完成之后,依然需要通过工具对代码进行构建、代码;
- 所以在真实开发中,我们可以通过一个后缀名为 .vue 的single-file components (单文件组件) 来解决,并且可以使用webpack或者vite或者rollup等构建工具来对其进行处理。
# 4、单文件的特点
- 在这个组件中我们可以获得非常多的特性:
- 代码的高亮;
- ES6、CommonJS的模块化能力;
- 组件作用域的CSS;
- 可以使用预处理器来构建更加丰富的组件,比如:TypeScript、Babel、Less、Sass等;
# 5、如何支持SFC
# 5.1.如何支持SFC
- 如果我们想要使用这一的SFC的.vue文件,比较常见的是两种方式:
- 方式一:使用Vue CLI来创建项目,项目会默认帮助我们配置好所有的配置选项,可以在其中直接使用.vue文件;
- 方式二:自己使用webpack或rollup或vite这类打包工具,对其进行打包处理;
- 我们最终,无论是后期我们做项目,还是在公司进行开发,通常都会采用Vue CLI的方式来完成。
# 5.2.VSCode对SFC文件的支持
- 在前面我们提到过,真实开发中多数情况下我们都是使用SFC( single-file components (单文件组件) )。
- 我们先说一下VSCode对SFC的支持:
- 插件一:Vetur,从Vue2开发就一直在使用的VSCode支持Vue的插件;(vue2推荐)
- 插件二:Volar,官方推荐的插件;(vue3推荐)
# 6、Vue CLI脚手架
# 6.1.Vue CLI脚手架
- 什么是Vue脚手架?
- 我们前面学习了如何通过webpack配置Vue的开发环境,但是在真实开发中我们不可能每一个项目从头来完成所有的webpack配置,这样显然开发的效率会大大的降低;
- 所以在真实开发中,我们通常会使用脚手架来创建一个项目,Vue的项目我们使用的就是Vue的脚手架;
- 脚手架其实是建筑工程中的一个概念,在我们软件工程中也会将一些帮助我们搭建项目的工具称之为脚手架;
- Vue的脚手架就是Vue CLI:
- CLI是Command-Line Interface, 翻译为命令行界面;
- 我们可以通过CLI选择项目的配置和创建出我们的项目;
- Vue CLI已经内置了webpack相关的配置,我们不需要从零来配置;
# 6.2.Vue CLI 安装和使用
- 安装Vue CLI(目前最新的版本是v5.0.8)
- 我们是进行全局安装,这样在任何时候都可以通过vue的命令来创建项目;
# 全局安装脚手架
npm install @vue/cli -g
# 检查脚手架是否安装成功(脚手架具体版本)
vue --version
# 卸载脚手架
npm uninstall @vue/cli -g
1
2
3
4
5
6
2
3
4
5
6
- 升级Vue CLI:
- 如果是比较旧的版本,可以通过下面的命令来升级
# 升级脚手架的版本
npm update @vue/cli -g
1
2
2
- 通过Vue的命令来创建项目
# 创建项目
vue create 项目的名称
1
2
2
# 6.3.vue create 项目的过程
# 6.4.项目的目录结构
# 6.5.Vue CLI的运行原理
# 7、vue组件间的通信
# 7.1.认识组件的嵌套
- 前面我们是将所有的逻辑放到一个App.vue中:
- 在之前的案例中,我们只是创建了一个组件App;
- 如果我们一个应用程序将所有的逻辑都放在一个组件中,那么这个组件就会变成非常的臃肿和难以维护;
- 所以组件化的核心思想应该是对组件进行拆分,拆分成一个个小的组件;
- 再将这些组件组合嵌套在一起,最终形成我们的应用程序;
- 我们来分析一下下面代码的嵌套逻辑,假如我们将所有的代码逻辑都放到一个App.vue组件中:
- 我们会发现,将所有的代码逻辑全部放到一个组件中,代码是非常的臃肿和难以维护的。
- 并且在真实开发中,我们会有更多的内容和代码逻辑,对于扩展性和可维护性来说都是非常差的。
- 所以,在真实的开发中,我们会对组件进行拆分,拆分成一个个功能的小组件
# 7.2.组件的拆分
- 我们可以按照如下的方式进行拆分
- 按照如上的拆分方式后,我们开发对应的逻辑只需要去对应的组件编写就可。
# 7.3.组件的通信
- 上面的嵌套逻辑如下,它们存在如下关系:
- App组件是Header、Main、Footer组件的父组件;
- Main组件是Banner、ProductList组件的父组件;
- 在开发过程中,我们会经常遇到需要组件之间相互进行通信:
- 比如App可能使用了多个Header,每个地方的Header展示的内容不同,那么我们就需要使用者传递给Header一些数据,让其进行展示;
- 又比如我们在Main中一次性请求了Banner数据和ProductList数据,那么就需要传递给它们来进行展示;
- 也可能是子组件中发生了事件,需要由父组件来完成某些操作,那就需要子组件向父组件传递事件;
- 总之,在一个Vue项目中,组件之间的通信是非常重要的环节,所以接下来我们就具体学习一下组件之间是如何相互之间传递数据的;
# 7.4.父子组件之间通信的方式
- 父子组件之间如何进行通信呢?
- 父组件传递给子组件:通过props属性;
- 子组件传递给父组件:通过$emit触发事件;
# 8、父组件传递给子组件(父传子->props)
# 8.1.父组件传递给子组件
- 在开发中很常见的就是父子组件之间通信,比如父组件有一些数据,需要子组件来进行展示:
- 这个时候我们可以通过props来完成组件之间的通信;
- 什么是Props呢?
- Props是你可以在组件上注册一些自定义的attribute;
- 父组件给这些attribute赋值,子组件通过attribute的名称获取到对应的值;
- Props有两种常见的用法:
- 方式一:字符串数组,数组中的字符串就是attribute的名称;
- 方式二:对象类型,对象类型我们可以在指定attribute名称的同时,指定它需要传递的类型、是否是必须的、默认值等等
# 8.2.Props的数组用法
# 8.3.Props的对象用法
- 数组用法中我们只能说明传入的attribute的名称,并不能对其进行任何形式的限制,接下来我们来看一下对象的写法是如何让我们的props变得更加完善的。
- 当使用对象语法的时候,我们可以对传入的内容限制更多:
- 比如指定传入的attribute的类型;
type
- 比如指定传入的attribute是否是必传的;
required
- 比如指定没有传入时,attribute的默认值;
default
- 比如指定传入的attribute的类型;
# 8.4.细节一:那么type的类型都可以是哪些呢?
- 那么type的类型都可以是哪些呢?
- String
- Number
- Boolean
- Array
- Object
- Date
- Function
- Symbol
# 8.5.细节二:对象类型的其他写法
# 8.6.细节三:Prop 的大小写命名
- Prop 的大小写命名(camelCase vs kebab-case)
- HTML 中的 attribute 名是大小写不敏感的,所以浏览器会把所有大写字符解释为小写字符;
- 这意味着当你使用 DOM 中的模板时,camelCase (驼峰命名法) 的 prop 名需要使用其等价的 kebab-case (短横线分隔命名)命名
message-info
; 当然也可使用小驼峰进行命名messageInfo
<template>
<home message-info='hahaaha'></home>
<home messageInfo='hahaaha'></home>
</template>
1
2
3
4
2
3
4
# 8.7.非Prop的Attribute
- 什么是非Prop的Attribute呢?
- 当我们传递给一个组件某个属性,但是该属性并没有定义对应的props或者emits时,就称之为 非Prop的Attribute;
- 常见的包括class、style、id属性等;
- Attribute继承
- 当组件有单个根节点时,非Prop的Attribute将自动添加到根节点的Attribute中:
# 8.8.禁用Attribute继承和多根节点
- 如果我们不希望组件的根元素继承attribute,可以在组件中设置 inheritAttrs: false:
- 禁用attribute继承的常见情况是需要将attribute应用于根元素之外的其他元素;
- 我们可以通过 $attrs来访问所有的 非props的attribute;
- 多个根节点的attribute
- 多个根节点的attribute如果没有显示的绑定,那么会报警告,我们必须手动的指定要绑定到哪一个属性上:
- 控制台出现的警告内容:[Vue warn]:将无关的非属性(类)传递给组件,但无法自动继承,因为组件呈现片段或文本根节点
- 多个根节点的attribute如果没有显示的绑定,那么会报警告,我们必须手动的指定要绑定到哪一个属性上:
# 9、子组件传递给父组件(子传父->this.$emit)
# 9.1.子组件传递给父组件
- 什么情况下子组件需要传递内容到父组件呢?
- 当子组件有一些事件发生的时候,比如在组件中发生了点击,父组件需要切换内容;
- 子组件有一些内容想要传递给父组件的时候;
- 我们如何完成上面的操作呢?
- 首先,我们需要在子组件中定义好在某些情况下触发的事件名称;
- 其次,在父组件中以v-on的方式传入要监听的事件名称,并且绑定到对应的方法中;
- 最后,在子组件中发生某个事件的时候,根据事件名称触发对应的事件;
# 9.2.自定义事件的流程
- 我们封装一个CounterOperation.vue的组件:
- 内部其实是监听两个按钮的点击,点击之后通过 this.$emit的方式发出去事件;
# 9.3.自定义事件的参数和验证(了解)
- 自定义事件的时候,我们也可以传递一些参数给父组件:
<script>
export default {
incrementTen() {
this.$emit('addTen',10)
}
}
</script>
1
2
3
4
5
6
7
2
3
4
5
6
7
- 在vue3当中,我们可以对传递的参数进行验证:(如果子组件传给父组件的参数不为10,则会抛出警告:[Vue warn]:无效的事件参数:事件“addTen”的事件验证失败。)
# 10、非父子组件的通信
# 10.1.非父子组件的通信
- 在开发中,我们构建了组件树之后,除了父子组件之间的通信之外,还会有非父子组件之间的通信。
- 这里我们主要学习两种方式:
- 全局事件总线;
- Provide/Inject;提供/注入
# 10.2.全局事件总线mitt库
- Vue3从实例中移除了 $on、$off 和 $once 方法,所以我们如果希望继续使用全局事件总线,要通过第三方的库:
- Vue3官方有推荐一些库,例如 mitt 或 tiny-emitter;mitt库 (opens new window)
# 首先先安装这个库
npm install --save mitt
1
2
2
# 10.3.使用事件总线工具
- 我们在App.vue中监听事件;
mitt().on('functionName',(o)=> {})
- 我们在Info.vue中发出事件;
mitt().emit('functionName',{name:'kobe'})
- 在某些情况下我们可能希望取消掉之前注册的函数监听:
mitt().off('functionName')
# 10.4.Provide和Inject
- Provide/Inject用于非父子组件之间共享数据:
- 比如有一些深度嵌套的组件,子组件想要获取父组件的部分内容;
- 在这种情况下,如果我们仍然将props沿着组件链逐级传递下去,就会非常的麻烦;
- 对于这种情况下,我们可以使用 Provide 和 Inject :
- 无论层级结构有多深,父组件都可以作为其所有子组件的依赖提供者;
- 父组件有一个 provide 选项来提供数据;
- 子组件有一个 inject 选项来开始使用这些数据;
- 实际上,你可以将依赖注入看作是“long range props”,除了:
- 父组件不需要知道哪些子组件使用它 provide 的 property
- 子组件不需要知道 inject 的 property 来自哪里
# 10.5.Provide和Inject基本使用
- 我们开发一个这样的结构:
# 10.6.Provide和Inject函数的写法
- 如果Provide中提供的一些数据是来自data,那么我们可能会想要通过this来获取:
- 这个时候会报错:
- 这里给大家留一个思考题,我们的this使用的是哪里的this?
- 我们直接将provide写成方法写法,并返回一个对象;这样就能拿到data中的数据了
# 10.7.处理响应式数据
- 我们先来验证一个结果:如果我们修改了this.names的内容,那么使用length的子组件会不会是响应式的?
- 我们会发现对应的子组件中是没有反应的:
- 这是因为当我们修改了names之后,之前在provide中引入的 this.names.length 本身并不是响应式的;
- 那么怎么样可以让我们的数据变成响应式的呢?
- 非常的简单,我们可以使用响应式的一些API来完成这些功能,比如说computed函数;先进行导入
import {computed} from 'vue'
- 当然,这个computed是vue3的新特性,在后面会专门学习,这里大家可以先直接使用一下;
- 非常的简单,我们可以使用响应式的一些API来完成这些功能,比如说computed函数;先进行导入
- 注意:我们在使用length的时候需要获取其中的value
- 这是因为computed返回的是一个ref对象,需要取出其中的value来使用;