思想
一、样式的思考:组件里写哪些样式?应该注意什么?
二、template的思考:slot还是组件里写好?
三、行为的思考:父组件定义还是子组件定义?如果在父组件中定义,那么组件的可扩展性就好了,但是会写很多代码,如果在子组件中定义,那么使用组件的人很方便但是不好扩展
四、props的思考:到底哪些东西作为props哪些东西作为组件本身的data
样式编写建议
一、容器的基本样式,内部内容的基本样式
二、尽量使用低权值,即使用标签名来定义样式(标签名的权值是最低的,为1),这样做的好处是,如果父组件想修改样式,直接给标签加class即可
三、定义一些内置类
自定义searchBar组件:
<template>
<div class="search-wrap">
<slot></slot>
<button class="search-button">搜索</button>
<button class="reset-button">重置</button>
</div>
</template>
<script>
export default{
name:"search",
props:{
msg:String
},
mounted() {
},
methods: {
search(){
}
},
}
</script>
<style scoped>
.search-wrap{
width: 600px;
display: flex;
box-shadow: 2px 3px 4px #9f9b9b;
padding: 20px;
}
.search-item{
margin-right: 15px;
}
/* 父组件传进来的内容中肯定有input标签,要给input标签低权值的样式,所以使用input标签名来添加样式,此时他的样式权值是1 */
input{
}
/* 如果用下面这种方法给input标签添加样式,那么当父组件想修改样式时,就需要用important,因为类名选择器的样式权值为10,所以下面这种写法的权值为11
.search-wrap input{
}
*/
/* 预置一些类名 */
mini-wrap{
wisth: 200px;
}
button{
border: none;
color: white;
padding: 4px 15px;
cursor: pointer;
}
.search-wrap .search-button{
background-color: rgb(23,23,185);
}
</style>
父组件使用:
<searchBar>
<div class="search-item">
<label></label>
<input class="search-input"/>
</div>
<!-- 如果想使用小一点的搜索框 -->
<div class="mini-wrap">
<label>姓名</label>
<input class="search-input"/>
</div>
</searchBar>
template建议
一、基本原则:内容固定就写死不确定的部分使用slot传入,此外还要考虑数据传递的问题,如果把数据写死,之后传出去会不会比较麻烦
二、如果平衡一点,可以这样:父组件传了slot,子组件就用slot,没有传就用默认的。如果全部写死,组件的便捷性会很高,如果用slot,组件的可扩展性会很高
<template>
<div class="search-wrap">
<slot></slot>
<!-- 上面的slot是默认插槽,下面这行代码判断所有插槽中是否有默认插槽,即父组件是否传入了默认插槽,如果不存在(没传),则使用组件的内容 -->
<div v-if="!('default) in $slots)" class="search-item">
<label>姓名</label>
<!-- 如果要把用户输入的内容(input输入框的值)传给父组件,需要使用$emit -->
<input class="search-input" v-model="name" @change="change(name)"/>
</div>
<button class="search-button">搜索</button>
<button class="reset-button">重置</button>
</div>
</template>
<script>
export default{
name:"search",
props:{
msg:String
},
mounted() {
},
methods: {
search(){
},
change(val){
this.$emit('nameChange',val)
}
},
}
</script>
<style scoped>
.search-wrap{
width: 600px;
display: flex;
box-shadow: 2px 3px 4px #9f9b9b;
padding: 20px;
}
.search-item{
margin-right: 15px;
}
button{
border: none;
color: white;
padding: 4px 15px;
cursor: pointer;
}
.search-wrap .search-button{
background-color: rgb(23,23,185);
}
</style>
行为的建议
一、把某一个行为分成基本部分和业务部分,建议每一个行为都留给父组件监听。比如弹窗组件的弹出和关闭两个行为属于基本部分,可以写死在组件内,点击确定、取消这两个行为属于业务部分,需父组件传入,但是这个四个行为建议都留给父组件监听。但是搜索组件所有的行为基本都是业务部分的,因为搜索功能在不同场景下调用的接口不一样,组件可以监听搜索按钮的点击,然后发送事件告知父组件,让父组件去调用接口
二、为了更好的扩展,可以拆分某一个行为的前后中几个周期,比如,以搜索这一行为为例,在搜索前、搜索中、搜索后这三个阶段都给父组件发送事件
props的建议
一、组件前端行为需要的数据内部定义,业务相关数据父组件传入。比如说控制弹窗组件显示隐藏状态的数据放在组件内部data,弹窗内部具体展示的内容由父组件传入
二、props也是一个组件扩展的重要接口,比如有的搜索组件有重置按钮,有的没有,那么组件可以在props中接收一个参数,用以控制是否展示重置按钮
总结
组件封装其实就是扩展性和便捷性的权衡,组件写死的部分多了,便捷性提升了,但是扩展性下降了,组件内很多内容依靠父组件传入,那么扩展性增强了,但是便捷性下降了
如何在项目中思考组件封装
一、思想:在实际项目中,我们大多封装复用的整体操作,区别于编写组件库组件,项目中往往已经有基础组件库了,我们不会再去封装基础功能组件(比如input、button、table等),所以项目中我们更多的会去观察项目中常见的整体业务操作,去封装成组件以便复用,即封装可复用的整体操作。在后台管理系统的项目中经常会遇到一些table+页码的页面,如下图所示,而elementui提供的table和页码是分开的,所以可以将二者封装成一个整体,便于使用,本节就以页码列表组件为例。
二、项目中封装业务组件的两大原则:
- 我们写的组件不要去结合具体业务逻辑:
(1)组件只作为数据的容器,数据统一父组件传入
(2)只编写ui逻辑,具体的数据操作这样的业务逻辑,触发父组件的监听交给父组件处理
- 尽量提供简便:在不结合具体业务逻辑的前提下,让父组件使用组件尽可能方便,我们的原则是观察大部分页面的设计,能方便的满足大多数页面的需求,少数页面有差距,也有扩展方案
设计组件时,针对除基本功能以外的扩展功能都要考虑不用时能关上,要用时样式如何调整?该功能如何进行?(比如说搜索功能的模糊搜索功能,是父组件给组件一个url然后组件自动查询还是将模糊搜索的功能开发出去,让父组件自己定义请求哪个地址?)该功能所涉及的数据如何归属?(是归属组件还是父组件?)
重复小组件的处理经验
一、什么是重复小组件:组件不复杂但是在很多页面上都会出现,如弹窗
二、需求:
三、实现方案:
- 思路一:
- 思路二:希望能像elementui的messagebox方法一样,点击即可弹窗,假如我们自己封装的方法叫showSign:
showSign('xxx',{
confirm:()=>{
// 确认行为
},
cancel:()=>{
// 取消行为
}
})
四、具体实现思路:
- 基本思路:
- 代码:
(1)使用createVNode实现(简易版的,没写完)
import {
createVNode,render} from 'vue'
export function signPop(content){
let pop = createVNode('div',{
class:"divcover"
},[
createVNode('div',{
class:"popcontent"
},content)
])
render(pop,document.body)// 将虚拟dom挂载到document.body上
}
(2)使用jsx实现:
import {
render} from 'vue'
export function signPop(content){
let pop = <div class='divcover'>
<div class="popcontent">
<div>{
content}</div>
<div>
<button onclick={
() =>{
document.body.removeChild(pop)
}}>不同意</button>
<button>签署</button>
</div>
</div>
</div>
render(pop,document.body)// 将虚拟dom挂载到document.body上
}
报错:
第一个参数必须是真实dom节点,在vue中,真实dom放在虚拟dom的el属性中:document.body.removeChild(pop.el)
即可获取真实el
继续报错:第二次及之后点击按钮弹窗都不出来了。因为虚拟dom只能挂载一次,通过原生的document.body.removeChild(pop.el)
将真实dom移除,但是在vue看来,pop的虚拟dom已经挂载好了,没有隐藏,所以第二次及之后点击按钮让他再次挂载挂不上去了
解决:借鉴elementui的做法,根据let div = document.createElement('div')
创建一个真实的div,然后将pop弹窗(虚拟dom)渲染到这个真实的div中:render(pop,div)
,然后通过原生的加入子节点的方式将div加载进去:document.body.appendChild(div)
,然后移除时,直接移除这个真实的div:document.body.removeChild(div)
。完整代码:
import {
render} from 'vue'
export function signPop(content,handler){
let div = document.createElement('div')
let pop = <div class='divcover'>
<div class="popcontent">
<div>{
content}</div>
<div>
<button onclick={
() =>{
document.body.removeChild(div)
handler && handler.cancel && handler.cancel()
}}>不同意</button>
<button onclick={
() =>{
document.body.removeChild(div)
handler && handler.confirm && handler.confirm()
}}>签署</button>
</div>
</div>
</div>
render(pop,div)
document.body.appendChild(div)
}
- 外部使用:
利用v-model提升组件的方便性
一、v-model主要用于什么情况下:如果我们需要把组件内部的值和副组件的一个数据相绑定,v-model会是一个很好的帮手
二、v-model的本质:
<input v-model="a" />
等价于:
<input :value="a" @input="(e)=>{a=e.target.value}" />
所以,v-model不是一个vue指令而是一个语法糖,一个让我们传递值,并监听修改的简便语法糖
三、v-model作用于组件上
- vue2中和原生等价:
<son v-model="a"></son>
等价于:
<son :value="a" @input="(e)=>{a=e.target.value}"></son>
- vue3中,在原生组件上还是不变(同前述),在自定义组件上,传递的是modelValue,监听的是update:modelValue:
<son v-model="a"></son>
等价于:
<son :modelValue="a" @update:modelValue="(e)=>{a=e.target.value}"></son>
三、举例一些可以使用v-model的组件:
- 弹窗(直接v-model绑定父组件的值,这样一个v-model就能轻松的控制弹窗的显隐)
- 业务里的一些功能操作(直接把操作结果绑定到组件的data,不用付组件传值和监听)
四、弹窗原始实现:弹窗组件自己肯定不知道自己啥时候显隐,所以控制弹窗显隐的变量应该放在父组件,如果不使用v-model,那么我们应该这样做:点击父组件的按钮展示弹窗,点击弹窗的取消关闭弹窗,此时弹窗会像父组件发送一个事件,父组件监听到这个事件后隐藏弹窗,父组件代码:
<template>
<button @onclick="()=>{popShow=true}">显示弹窗</button>
<pop v-if="popShow" @hidden="()=>{popShow=false}">
<div>我是一个弹窗</div>
</pop>
</template>
<script setup>
import {
ref} from "vue"
import pop from "./components/Pop.vue"
let popShow = ref(false)
</script>
Pop.vue代码:
<template>
<div>
<div>
<slot></slot>
<div>
<button>确定</button>
<button @click="hiddenPop">取消</button>
</div>
</div>
</div>
</template>
<script setup>
let emit = defineEmits(["hidden"])
function hiddenPop(){
emit("hidden")
}
</script>
五、弹窗v-model实现:又要写v-if又要写监听,很麻烦,如果使用v-model可以一步到位实现:
在父组件App.vue中使用:
<template>
<button @onclick="()=>{popShow=true}">显示弹窗</button>
<pop v-model="popShow">
<div>我是一个弹窗</div>
</pop>
</template>
<script setup>
import {
ref} from "vue"
import pop from "./components/Pop.vue"
let popShow = ref(false)
</script>
Pop.vue:
<template>
<div :style="{display:modelValue ? 'block' : 'none'}">
<div>
<slot></slot>
<div>
<button>确定</button>
<button @click="hiddenPop">取消</button>
</div>
</div>
</div>
</template>
<script setup>
let emit = defineEmits(["hidden"])
// 相当于父组件给自组件传过来了一个modelValue,子组件先在props中接收一下
let {
modelValue} = defineProps({
modelValue:{
type:Boolean,
default:false
}
})
function hiddenPop(){
// 使用v-model,父组件会自动监听update:modelValue事件,子组件直接emit并把值传过去即可
emit("update:modelValue",false)
}
</script>
六、扩展:
- 可以不传modelValue,改写为其他值:v-model默认是传modelValue,也可以传其他的值:
v-model:popControl="popShow"
,传的就是popControl,上述pop.vue中将modelValue改为popControl即可 - 可以传多个v-model,但需要每个v-model的名字不一样:
<pop v-model="popShow" v-model:a="123" v-model:b="b">...
,相当于父组件给自组件传了modelValue、a、b三个值