gitbook/玩转Vue 3全家桶/docs/435439.md
2022-09-03 22:05:03 +08:00

323 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 08 | 组件化:如何像搭积木一样开发网页?
你好,我是大圣。
在上一讲中,我给你讲解了响应式的基本原理和进阶用法。除了响应式,组件相关的知识在Vue中也非常重要,所以今天我就跟你聊一下Vue的组件化机制。
在我们的项目中,组件无处不在,通过对组件功能的封装,可以像搭积木一样开发网页。而我们现在已经很难想象,没有组件的开发状态是什么样了。你可以看下面 Vue官方的示例图,它对组件化开发做了形象化的展示。图中的左边是一个网页,可以按照功能模块抽象成很多组件,这些组件就像积木一样拼接成网页。
![图片](https://static001.geekbang.org/resource/image/0e/39/0e922d413eeeac4378233baa254dd039.png?wh=1406x544)
## 什么是组件化开发
谈组件化开发之前,我们先来看看什么是组件。举个通俗的例子,我们在页面的源码里写出的button标签,会在前端页面中显示出下面的样式:
![图片](https://static001.geekbang.org/resource/image/eb/91/eb35f15db1cd10a804aebfe140da5991.png?wh=1866x958)
这个button其实就是一个组件,这样前端页面在显示上会加上边框和鼠标悬停的样式,还可以使用click事件触发函数等。只不过这是浏览器帮我们封装好的组件,我们在编辑代码的任何地方,只需要使用下面的代码,就可以让前端页面显示一个按钮。
```xml
<button> 按钮 </button>
```
除了浏览器自带的组件外,Vue 还允许我们自定义组件,把一个功能的模板(template)封装在一个.vue文件中。例如在下图中,我们把每个组件的逻辑和样式,也就是把JavaScript 和CSS封装在一起,方便在项目中复用整个组件的代码。
![图片](https://static001.geekbang.org/resource/image/43/06/439386a5f3f2463feb8d908a752ac406.jpg?wh=1920x1081)
我们实际开发的项目中有导航、侧边栏、表格、弹窗等组件,并且也会引入Element3这个组件库进行开发。此外,我们也会定制业务相关的组件,最终通过这些组件,搭积木式地把页面搭建起来。
Vue已经把组件化的机制实现得很好了,你只需要在这个基础之上,去掌握和学习组件化在使用上的设计理念。这样做的目的是实现高效的代码复用,在后续的项目开发中,我们会把组件分成两个类型,一个是通用型组件,一个是业务型组件。
通用型组件就是各大组件库的组件风格,包括按钮、表单、弹窗等通用功能。业务型组件包含业务的交互逻辑,包括购物车、登录注册等,会和我们不同的业务强绑定。
组件的开发由于要考虑代码的复用性,会比通常的业务开发要求更高,需要有更好的可维护性和稳定性的要求。为了帮助你理解设计组件的要点,我先选择一个简单的组件展开讲解。小圣在继续开发项目的时候,有一个评级的需求,简单来说,就是在前端页面上,能够让商品显示1到5的评分。我会借此教小圣在组件开发上的入门和应用,并依次讲解组件的设计思路。
## 渲染评级分数
其实,对于简单的评级需求,我们就可以使用组件。这样,只需要一行代码就可以实现评级需求。比如下面的代码,rate是1到5的整数,通过slice方法,我们直接渲染出对应数量的星星即可。
```xml
"★★★★★☆☆☆☆☆".slice(5 - rate, 10 - rate)
```
想要查看上面这行代码的效果,你只需要传入评分值rate。这行代码的运行效果如下图所示,其中的星星代表着评价的等级,由rate的值来决定。
![图片](https://static001.geekbang.org/resource/image/ae/0f/aef45ab07edd11e6399b0029887bee0f.png?wh=501x548)
每个组件渲染的内容并不完全一样,这是我们写组件时首先要确认的内容。每个组件在项目中的不同地方,会渲染不同的内容。
我们在这里写的这个组件就是根据rate的值,来渲染出不同数量的星星。我们进入到src/components目录,新建Rate.vue,然后写出下面的代码。在下面的代码中,我们使用defineProps来规范传递数据的格式,这里规定了组件会接收外部传来的value属性,并且只能是数字,然后根据value的值计算出评分的星星。
```xml
<template>
<div>
{{rate}}
</div>
</template>
<script setup>
import { defineProps,computed } from 'vue';
let props = defineProps({
value: Number
})
let rate = computed(()=>"★★★★★☆☆☆☆☆".slice(5 - props.value, 10 - props.value))
</script>
```
使用组件的方式就是使用:value的方式,通过属性把score传递给Rate组件,就能够在页面上根据score的值,显示出三颗实心的星星。下面的代码展示了如何使用Rate组件来显示3颗星星。
```xml
<template>
<Rate :value="score"></Rate>
</template>
<script setup>
import {ref} from 'vue'
import Rate from './components/Rate1.vue'
let score = ref(3)
</script>
```
根据传递的score值显示的不同的内容,我们也可以更进一步,回到Rate.vue代码里,加入如下的代码,比如在组件中内置一些主题颜色,加入CSS的内容。如下面代码,Rate组件新接收一个属性theme,默认值是orange。我们在Rate组件中内置了几个主题颜色,根据传递的theme计算出颜色,并且使用 :style 渲染。
```xml
<template>
<div :style="fontstyle">
{{rate}}
</div>
</template>
<script setup>
import { defineProps,computed, } from 'vue';
let props = defineProps({
value: Number,
theme:{type:String,default:'orange'}
})
console.log(props)
let rate = computed(()=>"★★★★★☆☆☆☆☆".slice(5 - props.value, 10 - props.value))
const themeObj = {
'black': '#00',
'white': '#fff',
'red': '#f5222d',
'orange': '#fa541c',
'yellow': '#fadb14',
'green': '#73d13d',
'blue': '#40a9ff',
}
const fontstyle = computed(()=> {
return `color:${themeObj[props.theme]};`
})
</script>
```
在完成了上面代码所示的这一过程,也就是通过theme渲染星星颜色这一步,我们就可以使用下面的代码,传递value和theme两个属性,并且可以很方便地复用组件。
```xml
<Rate :value="3" ></Rate>
<Rate :value="4" theme="red"></Rate>
<Rate :value="1" theme="green"></Rate>
```
在下图中,也可以看到上面三个组件渲染的结果:
![](https://static001.geekbang.org/resource/image/8c/84/8cf702e0842ba6307856108c1acaa784.jpg?wh=227x170)
## 组件事件
使用这个Rate组件后,虽然前端页面显示评级的需求是完成了,但是评级组件还需要具备修改评分的功能,所以我们需要让组件的星星可点击,并且让点击后的评分值能够传递到父组件。
在Vue中,我们使用emit来对外传递事件,这样父元素就可以监听Rate组件内部的变化。现在我们对Rate组件进行改造,首先由于我们的星星都是普通的文本,没有办法单独绑定click事件。所以我们要对模板进行改造,每个星星都用span包裹,并且我们可以用width属性控制宽度,支持小数的评分显示。
我们回到Rate.vue组件,添加下面的代码,我们把★和☆用span包裹,并绑定鼠标的mouseover事件。然后通过:style,我们可以设置实心五角星★的宽度,实现一样的评级效果。
```xml
<template>
<div :style="fontstyle">
<div class='rate' @mouseout="mouseOut">
<span @mouseover="mouseOver(num)" v-for='num in 5' :key="num">☆</span>
<span class='hollow' :style="fontwidth">
<span @mouseover="mouseOver(num)" v-for='num in 5' :key="num">★</span>
</span>
</div>
</div>
</template>
<script setup>
// ...其他代码
// 评分宽度
let width = ref(props.value)
function mouseOver(i){
width.value = i
}
function mouseOut(){
width.value = props.value
}
const fontwidth = computed(()=>`width:${width.value}em;`)
</script>
<style scoped>
.rate{
position:relative;
display: inline-block;
}
.rate > span.hollow {
position:absolute;
display: inline-block;
top:0;
left:0;
width:0;
overflow:hidden;
}
</style>
```
因为现在是通过宽度显示星星,所以我们还可以支持3.5分的小数评级,并且支持鼠标滑过的时候选择不同的评分。用下面的代码,我们可以使用Rate组件。
```xml
<Rate :value="3.5" ></Rate>
```
上面代码的显示效果如下图所示:
# ![图片](https://static001.geekbang.org/resource/image/9a/16/9ab95503a0f65fc69896e30e21de8816.gif?wh=522x185)
然后我们需要做的,就是在点击五角星选择评分的时候,把当前评分传递给父组件即可。在Vue 3中,我们使用defineEmit来定义对外“发射”的数据,在点击评分的时候触发即可。下面的defineEmit代码就展示了点击评分后,向父元素“发射”评分数据num。
```xml
<template>
省略代码
<span @click="onRate(num)" @mouseover="mouseOver(num)" v-for='num in 5' :key="num">★</span>
</template>
<script setup>
import { defineProps, defineEmits,computed, ref} from 'vue';
let emits = defineEmits('update-rate')
function onRate(num){
emits('update-rate',num)
}
</script>
```
在下面的代码中,我们使用@update-rate 接收Rate组件emit的数据,并且修改score的值,这样就完成了数据修改后的更新。
```xml
<template>
<h1>你的评分是 {{score}}</h1>
<Rate :value="score" @update-rate="update"></Rate>
</template>
<script setup>
import {ref} from 'vue'
import Rate from './components/Rate1.vue'
let score = ref(3.5)
function update(num){
score.value = num
}
</script>
```
现在组件的示意图如下,我们通过defineProps定义了传递数据的格式,通过defineEmits定义了监听的函数,最终实现了组件和外部数据之间的同步。
![图片](https://static001.geekbang.org/resource/image/43/1b/43767ceb3324b4887271d0d20909a31b.jpg?wh=1904x1279)
## 组件的v-model
上面Rate组件中数据双向同步的需求在表单领域非常常见,例如[第二讲](https://time.geekbang.org/column/article/428106)中,我们在input标签上使用v-model这个属性就实现了这个需求。在自定义组件上我们也可以用v-model,对于自定义组件来说,v-model是传递属性和接收组件事件两个写法的简写。
在下面的代码中,首先我们把属性名修改成modelValue,然后如果我们想在前端页面进行点击评级的操作,我们只需要通过update:modelValue这个emit事件发出通知即可。
```xml
let props = defineProps({
modelValue: Number,
theme:{type:String,default:'orange'}
})
let emits = defineEmits(['update:modelValue'])
```
然后我们就可以按如下代码中的方式,使用Rate这个组件,也就是直接使用v-model绑定score变量。这样,就可以实现value和onRate两个属性的效果。
```xml
<template>
<h1>你的评分是 {{score}}</h1>
<Rate v-model="score"></Rate>
</template>
```
## 插槽
和HTML的标签使用类似,很多时候我们也需要给组件中传递内容。就像在下面的代码中click并不是button标签的属性,而是子元素,button标签会把子元素渲染在居中的位置。
```xml
<button> click </button>
```
我们的Rate组件也是类似的,在Vue中直接使用slot组件来显示组件的子元素,也就是所谓的插槽。在下面的代码中,我们使用slot组件渲染Rate组件的子元素。
```xml
<template>
<div :style="fontstyle">
<slot></slot>
<div class='rate' @mouseout="mouseOut">
<span @mouseover="mouseOver(num)" v-for='num in 5' :key="num">☆</span>
<span class='hollow' :style="fontwidth">
<span @click="onRate(num)" @mouseover="mouseOver(num)" v-for='num in 5' :key="num">★</span>
</span>
</div>
</div>
</template>
```
我们现在使用Rate组件的时候,组件的子元素都会放在评级组件之前。除了文本,也可以传递其他组件或者html标签 。下面代码显示的结果,第一个Rate组件显示课程评分 ,第二个Rate组件前面显示一个图片。
```xml
<Rate v-model="score">课程评分</Rate>
<Rate v-model="score">
<img width="14" src="/favicon.ico">
</Rate>
```
## 总结
至此,我们设计了一个非常迷你的评级组件,我带你回顾一下今天都做了什么吧。
首先,我们用props属性传递的方式,通过传递的value属性去决定显示的星星数量,这也是设计组件的时候首先就要考虑的。一个组件库首先要实现的,就是通过props属性渲染内容,而在Rate组件里,我们可以根据value属性去渲染显示了几颗星星。
然后为了让用户可以点击评分,我们优化了显示的方式,每个★包裹一个span标签,并且绑定了mouseover和mouseout事件显示鼠标悬停的样式,最后在★标签的click事件上,对外通知事件,告知父组件数据的变化。
**对于这个通知机制,我们使用defineEmits定义的方式来实现,这也是Vue组件中重要的数据交换机制**。emits配合props,这样我们在使用一个组件的时候,就实现了给组件传递数据,并且我们也能够监听组件内部数据的变化。最后我们通过规范props和emit的名字,实现了直接在自定义的组件之上使用v-model。
当然,这个组件开发到这里,依然是比较精简,但实际上我们要考虑的问题更多。比如在下一讲,我会给你讲解如何在Vue中加上一些简单的动画。字符★显示的效果可能也不符合你所在公司的设计规范,所以你可能就需要使用图片替换五角星样式等。
更加完备的Rate组件,你可以去看[Element3评级组件的实现](https://github.com/hug-sun/element3/blob/master/packages/element3/packages/rate/Rate.vue),在这里我们先初步感受一个组件的完整设计过程,它包含着交互的小细节,比如键盘事件、自定义icon等等。
Rate组件写完,Vue的组件化你就算入门了,在课程第三部分中,我们会对更多场景的组件化进行实战开发,在那里我会给你详细说说更多组件进阶和扩展的用法。
## 思考题
今天我带你从0设计了一个组件,相信你对Vue 3的组件设计也有了新的思考,那么关于这个Rate组件,你觉得还有哪些功能扩展的需求呢?
欢迎在评论区分享你的思考,也欢迎你把这一讲分享给其他人,我们下一讲见!