组件化开发
1.组件化开发的意义
⼀、本节⼤纲
- ⼈类更擅⻓先拆分问题,再解决问题
- 什么是组件化开发
- 组件化的好处
⼆、⼈类更擅⻓先拆分问题,再解决问题
复杂问题拆分,复杂⻚⾯拆分,这是⼈类最擅⻓的事情。
如果有开发⼈员接到项⽬经理的⼀个任务:写⼀个阅读类的 app ,没有做过的话,可能当时就蒙了不知从何⼊⼿。但如果项⽬经理说,我们要做⼀个阅读类的 app ,你先把注册功能的⻚⾯做好,是不是就容易些。
如果注册功能还是不会做,那就再细化为:先参考某某 app 把静态⻚⾯⽤户密码输⼊框做出来!是不是就更加容易些。
三、什么是组件化开发
组件化开发实际上就是⼀种典型的先拆分问题,再解决问题的思路。对于 vue ⽽⾔,组件就是把⼀组相 关度极⾼的视图模板、样式、数据、操作抽取出来,形成可复⽤的独⽴功能代码。
四、组件化的好处
- 代码复⽤及功能复⽤,避免重复造轮⼦,降低程序员的⼯作量
- ⽅便代码的组织及管理
- 降低代码之间的耦合度,提⾼可扩展性
- Vue 组件不仅在单功能模块可以多次复⽤,甚⾄可以跨模块、跨项⽬复⽤
2.全局组件与局部组件
⼀、本节⼤纲
- 全局组件的定义、注册、使⽤
- 全局组件定义注册合⼆为⼀(简写,常⽤)
- 局部私有组件
- 模板视图与实例定义分离(结构清晰)
- ⽗⼦组件的嵌套
⼆、全局组件的定义、注册、使⽤
上⼀节我们已经学习了使⽤组件的意义,现在就正式来使⽤组件,使⽤全局组件有三个步骤:
下图为代码实现:
- 全局的组件遵循先定义( vue.extend() )、再注册( vue.component() )、后使⽤的原则。
- template ⽤于定义组件的视图内容,即:html 代码、vue 指令等。
- 如果⽤ new VUE() 定义多个 vue 实例,全局组件可以跨多个实例使⽤。
- 通常组件定义的名称为⾸字⺟⼤写,驼峰标志,如:MyComponent。
- 在 dom 中使⽤组件,通常遵循使⽤“-”分隔单词,⼩写规范。如:my-component。
上图代码最终实现效果如下:
三、全局组件定义注册合⼆为⼀(简写,常⽤)
可以使⽤ Vue.component() ⽅法 + template 组件视图层模板⼀步完成全局组件的定义与注册。
这种⽅法是第⼆⼩节中代码的简写⽅式,使⽤更加⼴泛。将 Vue.extend() 定义和 Vue.component() 注册 合并为⽤⼀个 Vue.component(template) 完成,实际上的内部实现原理是⼀致的。
四、局部私有组件
局部私有组件和全局组件的区别在于,私有组件只能在定义它的实例或者⽗组件⾥⾯使⽤。
定义的组件 MyConponent2 只能在 <div id="app">
⾥⾯使⽤,其他 Vue 实例⽆法使⽤该组件。
五、模板视图与实例定义分离
在上⾯的代码中,我们将组件定义的 html 代码写在了⼀个模板字符串⾥⾯,这样书写很不⽅便,没有 IDE 的提示,⽽且看上去也⽐较乱。
我们下⾯就来学⼀种更加正规的写法:(注意和上⽂中的代码形成对⽐)
六、⽗⼦组件的嵌套
⼀个⽗组件 Parent ⾥⾯包含两个⼦组件 Child,并将⽗组件放在 app2 实例⻚⾯渲染范围内。
下⽂中是使⽤私有局部组件的⽅式定义的,也就是 Parent 组件是 app2 实例的私有局部组件,Child 是 Parent 组件的私有局部组件。
代码实现的效果截图如下:
可以想⼀想如何通过全局组件,实现嵌套组件?
3.⽗⼦组件的数据定义及访问
⼀、本节⼤纲
- 组件⽆法直接访问 VUE 实例的 data 数据
- 组件有⾃⼰的数据和⽅法
- 组件数据为什么要定义在函数⾥⾯
- ⽗⼦组件的数据访问
⼆、组件⽆法直接访问 VUE 实例的 data 数据
我们发现:
- ⽆法在组件内直接通过插值表达式访问 vue 实例的数据
- ⼦组件也⽆法直接使⽤⽗组件中的数据
三、组件有⾃⼰的数据和⽅法
从上⼀⼩节的内容中,我们知道,组件视图内⽆法直接通过插值表达式访问 Vue 实例的数据。
那么组件的数据定义在哪⾥呢?组件有⾃⼰的数据和⽅法定义。如下图所示:
- MyComponent 组件有⾃⼰的视图模板定义 template#MyComponent。
- MyComponent 组件有⾃⼰的数据定义,data() 函数。注意这⾥定义的是⼀个函数,⽽不是对象。通过函数返回对象数据。
- MyComponent 组件有⾃⼰的操作⽅法,定义在 methods 代码块⾥⾯。
四、组件数据为什么要定义在函数⾥⾯
- 我们知道 Vue 实例的数据定义在 data 对象⾥⾯。但是组件的数据定义在函数⾥⾯,相当于使⽤了⼯⼚函数创建对象,每⼀次创建的对象在内存⾥⾯都是新的空间和指针。
- 如果不定义在对象⾥⾯,⾸先⼀定会报错,其次如果每次返回同⼀个对象。当⻚⾯出现多次使⽤同 ⼀个组件,⼀定会彼此影响,从⽽产⽣我们不期望的结果。
五、⽗⼦组件的数据访问
⽗组件⽆法直接使⽤⼦组件的数据,⼦组件也⽆法直接使⽤⽗组件定义的数据。
那么有没有间接的使⽤⽅式呢?我们可以使⽤ $parent、**.$children、$ref** 引⽤的⽅式:
- 在⼦组件代码中调⽤ this.$parent 可以获取到⽗组件对象
- 在⽗组件代码中调⽤ this.$children[0] 可以获取到⽗组件引⽤的第⼀个⼦组件对象。 ( this.$children 得到的是⼀个⼦组件的数组)
如果我们希望在⼦组件中使⽤ this.$parent 打印⽗组件信息,在⽗组件中调⽤ this.$children[0] 打印⼦组件信息。
上⾯的⽅法中,⽗组件使⽤ this.$children[0] 来获取多个⼦组件的数据。
如果我们希望快速从⽗组件获取⼦组件的数据,还可以为⼦组件加上⼀个属性 ref 。相当于为⼦组件起了⼀个别名,便于查找。
然后⽗组件通过如下代码即可打印⼦组件的属性数据:
console.log(this.$refs.childRef.childMessage);
但是通常使⽤ $parent、.$children、$ref 会增加组件之间的耦合性,所以不建议使⽤。除⾮你定义的组件只在单模块⾥⾯使⽤,不考虑以后的复⽤问题。
4.如何实现组件的切换
⼀、本节⼤纲
- v-if、v-else 指令切换组件
- 通过标签切换组件
⼆、v-if、v-else 指令切换组件
我们来通过 v-if、v-else 指令实现⼀个简单的需求,通过点击切换按钮。切换登录和注册⻚⾯,⼆者只能同时显示其中⼀个组件。视图定义如下:
- 通过 template 定义组件视图的内容
- 通过 isLogin 布尔型变量,切换登录与注册内容的显示
模型定义如下:
- 使⽤ Vue.component() 实现全局组件的注册
- isLogin 初始值为 true,即默认显示登录组件
最终实现效果如下:
三、通过标签切换组件
除了可以使⽤ v-if 和 v-else 指令,我们还可以使⽤标签来实现组件的切换,这种⽅式使⽤上更加灵活。
视图定义如下:
component 标签有⼀个 is 属性绑定,⽤于指定当前位置显示的组件的名称
模型定义如下:
实现的效果与之前相同。
5.⽗组件向⼦组件传递数据
⼀、本节⼤纲
- 使⽤ props ⽗组件向⼦组件传递数据
- 组件 props 属性传递最佳命名实践
- props 数据校验
⼆、使⽤ props ⽗组件向⼦组件传递数据
- ⾸先,⽗实例将⾃⼰的数据 player,传递给⼦组件 child-cpn。(图中⻩⾊箭头)
- ⼦组件通过绑定属性 child-player 绑定⽗组件数据 player
- ⼦组件标签属性 child-player 对应⼦组件模型属性定义 props:childPlayer
- childPlayer 属性可以使⽤插值表达式,显示在 template 模板⾥⾯
这样,我们通过改变⽗组件⾥⾯的 player 对象,就能动态的改变⼦组件的 childPlayer 属性值
要⾮常注意⼀点:在 Vue2.0 中,props 数据流只能从⽗组件向⼦组件传递。并且在组件内,不能修改 由外层传来的 props 数据。这是为了防⽌⼦组件⽆意修改了⽗组件的状态——这会让应⽤的数据流难以理解。
⼦组件如何将⾃⼰的数据改变传递给⽗组件呢?是下⼀节的内容。
三、组件 props 属性传递最佳命名实践
对于 props 声明的属性来说,在⽗级 HTML 模板中,属性名需要使⽤中划线写法。
⼦级 props 属性声明时,使⽤⼩驼峰或者中划线写法都可以;⼩写字⺟开头。
但是⽆论如何,在插值表达式⾥⾯必须是驼峰写法,如:{{childMessage}}
- 在 html 代码⾥⾯标签属性命名时,使⽤中划线属性写法。
- 在 js 代码⾥⾯的变量或属性使⽤驼峰的写法,组件名⾸字⺟⼤写,变量属性名⾸字⺟⼩写。
- 插值表达式
{{}}`是代码?还是 html 标签属性?html 规范⾥⾯有`{{}}
这种⽤法么?所以它当然是代码。
和我们平时写代码的规范是⼀致的。html 标签属性都是中划线,代码都是驼峰标志。
上⾯的规范中遗留⼀个问题,就是组件的名称是⾸字⺟⼤写,驼峰标识。在 html ⾥⾯我们通常希望也遵循这样的规则。如: <ChildCpn></ChildCpn>
,这样可以避免 html 标签和组件标签混淆。
但是⽬前是不可以这么⽤的,在 Vue 单⽂件⾥⾯可以这样写。
四、props 数据校验
通常我们定义⼀个组件是应该可以提供给其他模块或其他⼈使⽤的。使⽤者可能对该组件的⽤法并不熟悉,可能会导致错误。所以有必要在⼦组件内,对⽗组件传递过来数据进⾏校验。
Vue.component("example", {
props: {
// 基础类型检测 (`null` 意思是任何类型都可以)
propA: Number,
// 可以是多种类型
propB: [String, Number],
// 必传属性且必须是字符串类型
propC: {
type: String,
required: true,
},
// 数字,不传就默认值100
propD: {
type: Number,
default: 100,
},
// 数组/对象的默认值应当由⼀个⼯⼚函数返回
propE: {
type: Object,
default: function () {
return { message: "hello" };
},
},
// ⾃定义验证函数
propF: {
validator: function (value) {
return value > 10;
},
},
},
});
type 的类型可以是如下:
String;
Number;
Boolean;
Function;
Object;
Array;
Symbol;
6.⼦组件向⽗组件传播事件
⼀、本节⼤纲
- 想⼀想这个需求怎么实现
- ⼦组件向⽗组件事件传播核⼼代码解读
- 整体需求的实现
⼆、想⼀想这个需求怎么实现
其中加⼀和减⼀的按钮是属于⼦组件,pCounter 显示是在⽗组件⾥⾯( Vue 实例⾥⾯)。
我们该如何把⼦组件的点击事件传递给⽗组件,并传递参数改变⽗组件的值?
三、⼦组件向⽗组件事件传播核⼼代码解读
前⾯《⽗⼦组件的数据定义与访问》中我们曾学过,⽗组件可以通过 $children 和 $ref 的⽅式获取⼦组件的数据。
但是在实际开发过程中,还有另外⼀种需求,即:⼦组件中发⽣了某些动作,从⽽改变了⼦组件的数据。
那么⽗组件如何实时的监听到⼦组件数据的变化呢?这就是本节的核⼼内容。
- 在⼦组件使⽤ $emit (‘事件名称’,参数),触发并发送事件,并且可以通过参数传值。
- 在⽗组件中嵌⼊⼦组件,并使⽤ v-on 指令(简写为@)监听事件,从⽽触发回调函数,回调函数接受⼦组件发送的参数。图中 changePcounter 就是针对 increment 事件和 decrment 事件监听的回调函数。
- ⽗组件定义回调函数接收实践触发源传递的参数,即 $emit 的第⼆个参数
四、整体需求的实现
⼦组件与视图的定义。
⼦组件点击加⼀按钮触发 incr 函数,incr 函数触发 increment 事件,并将当前 cCounter 作为事件参数传递出去。
⽗组件范围监听到 increment 事件,从⽽触发 changePCounter ⽅法,该⽅法接受 cCounter 作为参数。
⽗组件的定义
7.插槽的使⽤场景与⽅法
⼀、本节⼤纲
- 插槽的使⽤场景
- 插槽的使⽤⽅法
- 组件如何给插槽传递数据
- 改变插槽数据的显示样式
⼆、插槽的使⽤场景
Windows 操作系统⽂件管理的⻚⾯布局,头部导航栏,左侧⽂件夹菜单,右侧⽂件内容区。对 于“Windows ⽂件管理”这个组件,划分三个区域,头部导航栏,左侧⽂件夹菜单,右侧⽂件内容区。这 三个区域就是三个插槽。
我们可以先归纳⼀下应⽤插槽的场景的⼏个特点:
- 插槽是组件的插槽,并不脱离于组件⽽存在。⽐如:Windows ⽂件管理功能是⼀个组件,右侧⽂件内容区是该组件的⼀个插槽。
- 插槽相当于是⻚⾯占位符,占⽤位置展示数据,插槽通常可以较明确的做出区域划分。如: windows 的右侧⽂件内容区就可以划分为⼀个插槽。
- 插槽⾥⾯数据及其显示的样式是可以变化的,但是数据内容属性最好是有⼀定的相同点,才定义为⼀个插槽。如:Windows 的右侧⽂件内容区展示的都是⽂件或⽂件夹,不是菜单也不是按钮。
- 抽取共性,保留不同。将共性定义在组件⾥⾯,将差异暴露为插槽。
三、插槽的使⽤⽅法
第⼀步:抽取定义插槽
我们先看⼀个左右布局的视图,共性内容抽取定义为组件 MyLayout,差异内容定义为插槽,对外暴露。
注意上⾯的插槽定义,第⼀个插槽命名为 left,第⼆个插槽没有命名,没有命名的插槽默认名称为 default 。
第⼆步:插槽占位替换
对组件 MyLayout(my-layout) 的两个插槽进⾏占位替换。
v-slot:[slotname] 指令通过指明插槽名称,⽤ template 模板中的内容替换指定的插槽。
v-slot 可以简写为 #[slotname]
<template #left>
<h1>这⾥是左侧区域</h1>
</template>
四、组件如何给插槽传递数据
上⼀节在右侧内容区展示的数据时静态的,我们能不能动态地为右侧内容区传递数据呢?也是可以的。 思考:谁给插槽传数据呢?在哪个组件⾥⾯定义的插槽,就由哪个组件提供数据。
- players 和 bestPlayers 是组件 MyComponent ⾥⾯定义的数据。数据定义如下:
players: ["aaa", "bbb", "ccc", "ddd"],
bestPlayer: "aaa"
- 通过 v-bind 指令(简写为“:”)为插槽绑定变量,将变量绑定到 slotProps 上⾯,slotProps 相当于主⼲,我们通过 v-bind 为它添加分⽀。
- slotProps 只是⼀个绑定对象变量名,可以随意起名。
要注意⼀个问题:我们绑定的变量名是 bestPlayer(P ⼤写),到了 slot 模板⾥⾯,使⽤的时候⽤的是 bestplayer(⼩写)。这是因为 Vue 遵循⼀个规则:html 标签属性不应包含⼤写字⺟,所以尽量不要⽤驼峰标识。可以⽤ best-player 或 bestplayer 。
通过上⾯的实现,我们就可以通过改变⼦组件的数据,进⽽影响 slot 的显示内容。
五、改变插槽数据的显示样式
插槽的数据我们从组件中获取到了,那么数据展示的样式呢?
展示的样式是⼀个差异化的问题,所以应该在插槽的定义⾥⾯解决。
我们⽤ ui-li 标签展示⼀下 slot 的数据。
完整代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initialscale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>插槽-具名插槽-作⽤域插槽</title>
<style>
.wrapper {
height: 200px;
border: 1px solid #ccc;
display: flex;
}
.left {
width: 200px;
height: 100%;
background: #9fa8da;
}
.center {
height: 100%;
background: #80cbc4;
flex: 1;
}
</style>
</head>
<body>
<div id="app">
<my-layout>
<!-- 插槽占位替换 -->
<template v-slot:left>
<h1>左侧区域</h1>
<h1>左侧区域</h1>
</template>
<template v-slot:default="slotProps">
<ul>
<li v-for="player in slotProps.players">{{player}}</li>
</ul>
<h2>{{slotProps.bestplayer}}</h2>
</template>
</my-layout>
</div>
<template id="MyLayout">
<div class="wrapper">
<div class="left">
<slot name="left"></slot>
</div>
<div class="center">
<slot :players="players" :bestPlayer="bestPlayer"></slot>
</div>
</div>
</template>
<script src="https://cdn.jsdelivr.net/npm/vue@2"></script>
<script>
const app = new Vue({
el: "#app",
data() {
return {
message: "Vue",
};
},
components: {
MyLayout: {
template: "#MyLayout",
data() {
return {
players: ["aaa", "bbb", "ccc", "ddd"],
bestPlayer: "aaa",
};
},
},
},
});
</script>
</body>
<html></html>
</html>
运⾏效果: