虽然现在又很多组件库,方便了我们的开发。但是自己对于组件的封装,对组件的认识也不能少。下面我们就来介绍一些常用组件的封装。
为了方便,我们的样式都使用bootstrap,方便开发。
下拉菜单
我们知道,下拉菜单需要每一个item项组成,所以我们就可以封装一个drop-down-item
的组件,并且封装其父组件drop-down
。
drop-down
组件,他只需要提供触发下拉菜单的文字。并且提供默认插槽,为定制不同的drop-down-item
。
<div class="dropdown" ref="refDom">
<a
class="btn btn-outline-light dropdown-toggle"
href="javascript:;"
@click="openMenu"
>
hi, {{ name }}
</a>
<!-- bootstrap中默认dropdown为display: none -->
<div class="dropdown-menu" style="display: block" v-if="isOpen">
<slot></slot>
</div>
</div>
复制代码
这里需要注意的是,我们点击drop-down
组件外部范围内,该下拉菜单才会关闭。否则不会关闭。这时候,我们就需要js的一个API了。contains
这部分逻辑我们可以给它抽出,作为一个hooks。他的主要实现,就是传入下拉菜单根组件对象,即drop-down
组件的根组件,然后加以判断,返回一个boolean值。
import { ref, onMounted, onUnmounted, Ref } from "vue";
const useClickOutside = (refDom: Ref<null | HTMLElement>) => {
const isClickOutside = ref(false);
const handler = (e: MouseEvent) => {
// 防止节点为获取到
if (refDom.value) {
// 这个函数是判断点击区域是否是下拉菜单。
if (refDom.value.contains(e.target as HTMLElement)) {
isClickOutside.value = false;
} else {
isClickOutside.value = true;
}
}
};
onMounted(() => {
window.addEventListener("click", handler);
});
onUnmounted(() => {
window.removeEventListener("click", handler);
});
return {
isClickOutside
};
};
export default useClickOutside;
复制代码
下面就来实现drop-down
组件的逻辑部分
<script lang="ts">
import { defineComponent, ref, watch } from 'vue'
import useClickOutside from '../hooks/useClickOutside'
export default defineComponent({
name: 'DropDown',
props: {
name: {
type: String,
required: true,
},
},
setup() {
const isOpen = ref(false)
const openMenu = () => {
isOpen.value = !isOpen.value
}
const refDom = ref<null | HTMLElement>(null)
const { isClickOutside } = useClickOutside(refDom)
watch(isClickOutside, () => {
// 当点击是下拉菜单的外部并且下拉菜单处于展开状态。
if (isOpen.value && isClickOutside.value) {
isOpen.value = false
}
})
return {
isOpen,
openMenu,
refDom,
}
},
})
</script>
复制代码
drop-down-item
组件,他就只需要提供默认插槽。并且根据外部传入的跳转url,来定制。
<template>
<div class="drop-down-item">
<a class="dropdown-item" :href="path">
<slot></slot>
</a>
</div>
</template>
<script>
import { defineComponent } from "vue";
export default defineComponent({
name: 'DropDownItem',
props: {
path: {
type: String,
required: true
}
}
})
</script>
<style scoped>
.drop-down-item {
cursor: pointer;
}
</style>
复制代码
使用
<drop-down :name="user.username">
<drop-down-item path="/create">新建文章</drop-down-item>
<drop-down-item :path="`/column/${user.column}`">我的专栏</drop-down-item>
<drop-down-item path="/edit">编辑资料</drop-down-item>
<drop-down-item path="/" @click="logout">退出登录</drop-down-item>
</drop-down>
复制代码
表单组件
我们知道表单组件,使用非常频繁。而且,通常情况下,我们都回去使用第三方的组件库,来完成这部分的展示。所以下面我们自己来封装一下表单组件吧。包括表单验证。
validate-form
组件 :

<template>
<div class="validate-input pb-2">
<!--
:value="inputVal.val"
@input="updateValue"
他两就相当于v-model="inputVal.val"
-->
<!-- 如果没有设置inheritAttribute为false的话,子组件中不是prop的属性将直接挂载到直接父元素上,这里将挂载到div.validate-input pb-2上 -->
<div class="mb-3">
<label class="form-label">{{ inputLabel }}</label>
<input
v-if="tag === 'input'"
class="form-control"
:class="{ 'is-invalid': inputVal.error }"
@blur="validate"
v-bind="$attrs"
v-model="inputVal.val"
/>
<textarea
v-else-if="tag !== 'textarea'"
class="form-control"
:class="{ 'is-invalid': inputVal.error }"
@blur="validate"
v-bind="$attrs"
v-model="inputVal.val"
placeholder="请输入文章内容, 支持markdown语法"
></textarea>
<small
id="emailHelp"
class="form-text text-muted invalid-feedback"
v-if="inputVal.error"
>{{ inputVal.message }}</small
>
</div>
</div>
</template>
复制代码
对于单个表单元素它具有以下属性。
// 输入框中的约束
interface InputProps {
// 表单绑定的值
val: string
// 是否验证全部错误
error: boolean
// 错误提示
message: string
}
复制代码
并且还需要具有一下表单验证规则的属性
//验证规则的约束
interface RuleProps {
// 可以根据自己的需要,传入表单验证类型
type: 'required' | 'email' | 'password' | 'custom'
// 表单验证错误信息
message: string
// 当type类型为custom时,传入他,自定义验证函数。
valdator?: () => boolean
}
复制代码
这里我们就传入了两个表单类型,'input' | 'textarea'
。如果想要扩展,继续添加即可,然后再template中判断即可。
type Tag = 'input' | 'textarea';
复制代码
validate-input
组件需要传入以下props。
props: {
// 表单验证需要的rules数组
rules: Array as PropType<RulesProps>,
//v-model实现的value
modelValue: String,
// 表单类型
tag: {
type: String as PropType<Tag>,
default: 'input',
},
// 表示输入框的label值。
inputLabel: {
type: String,
required: true,
},
}
复制代码
实现表单值的双向绑定。
const inputVal: InputProps = reactive({
val: computed({
get: () => props.modelValue || '',
set: val => {
emit('update:modelValue', val)
}
}),
error: false,
message: ''
})
复制代码
实现表单验证函数。
const validate = () => {
if (props.rules) {
const allPassed = props.rules.every(rule => {
let passed = true
inputVal.message = rule.message
switch (rule.type) {
case 'required':
passed = (inputVal.val.trim() !== '')
break
case 'email':
passed = /^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/.test(inputVal.val)
break
case 'password':
passed = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[^]{8,16}$/.test(inputVal.val)
break
// 如果传入的是自定义的验证函数,我们直接执行即可。
case 'custom':
passed = rule.validator ? rule.validator() : true
break
default:
break
}
return passed
})
// 将全部验证通过后,将error设置为false
inputVal.error = !allPassed
return allPassed
}
return true
}
复制代码
其实我们也可以自定义触发验证的事件,这里默认指定是失去焦点(blur
)的时候,所以就不修改了。
接下来我们还需要将全部验证函数保存,发送给validate-form
组件,当点击按钮,我们将要判断时候验证都通过了,然后来进行请求的阻止或者发送。所以这些我们需要使用emitt
库来为我们服务。因为这是触发父组件中的按钮,然后将事件传到子组件中。
onMounted(() => {
emitter.emit('all-true', validate)
})
复制代码
接下来我们就来看看validate-form
组件如何实现吧。 这里我们需要定义一个默认插槽,来放置若干个表单。还有一个表单提交的具名插槽。
<template>
<div class="validate-form">
<form>
<slot name="default"></slot>
<div class="submit-area" @click.prevent="FormSubmit">
<slot name="submit">
<button type="submit" class="btn btn-primary">登录</button>
</slot>
</div>
</form>
</div>
</template>
复制代码
下面给出validate-input
和validate-form
组件的完整代码
// validate-form
<template>
<div class="validate-form">
<form>
<slot name="default"></slot>
<div class="submit-area" @click.prevent="FormSubmit">
<slot name="submit">
<button type="submit" class="btn btn-primary">登录</button>
</slot>
</div>
</form>
</div>
</template>
<script lang="ts">
import { defineComponent, onUnmounted } from 'vue'
import emitter from '../mitt'
type Func = () => boolean
export default defineComponent({
name: 'ValidateForm',
emits: ['form-submit'],
setup(props, context) {
let funcArr: Func[] = []
const FormSubmit = () => {
// 调用数组中每一项,然后判断是否有false
const val = funcArr.map((item) => item()).every((element) => element)
context.emit('form-submit', val)
}
// 这里就是将全部验证函数保存在数组中。
const callback = (func?: Func) => {
if (func) {
funcArr.push(func)
}
}
emitter.on('all-true', callback)
onUnmounted(() => {
emitter.off('all-true', callback)
// 清空数组
funcArr = []
})
return {
FormSubmit,
}
},
})
</script>
<style scoped>
.submit-area {
margin-top: 30px;
margin-bottom: 20px;
}
</style>
复制代码
<template>
<div class="validate-input pb-2">
<div class="mb-3">
<label class="form-label">{{ inputLabel }}</label>
<input
v-if="tag === 'input'"
class="form-control"
:class="{ 'is-invalid': inputVal.error }"
@blur="validate"
v-bind="$attrs"
v-model="inputVal.val"
/>
<textarea
v-else-if="tag !== 'textarea'"
class="form-control"
:class="{ 'is-invalid': inputVal.error }"
@blur="validate"
v-bind="$attrs"
v-model="inputVal.val"
></textarea>
<small
id="emailHelp"
class="form-text text-muted invalid-feedback"
v-if="inputVal.error"
>{{ inputVal.message }}</small
>
</div>
</div>
</template>
<script lang="ts">
import {
defineComponent,
PropType,
reactive,
onMounted,
ref,
watch,
computed,
} from 'vue'
import emitter from '../mitt'
// 输入框中的约束
interface InputProps {
val: string
error: boolean
message: string
}
//验证规则的约束
interface RuleProps {
type: 'required' | 'email' | 'password' | 'custom'
message: string
valdator?: () => boolean
}
export type RulesProps = RuleProps[]
// 判断输入框是普通输入框,还是多行输入框
type Tag = 'input' | 'textarea'
export default defineComponent({
name: 'ValidateInput',
// 将非props中的属性不要挂载到根组件上
inheritAttrs: false,
props: {
rules: Array as PropType<RulesProps>,
//v-model实现的value
modelValue: String,
tag: {
type: String as PropType<Tag>,
default: 'input',
},
// 表示输入框的label值。
inputLabel: {
type: String,
required: true,
},
},
setup(props, context) {
const inputVal: InputProps = reactive({
val: computed({
get() {
return props.modelValue || ''
},
set(val: string) {
context.emit('update:modelValue', val)
},
}),
error: false,
message: '',
})
const validate = () => {
if (props.rules) {
const allPassed = props.rules.every(rule => {
let passed = true
inputVal.message = rule.message
switch (rule.type) {
case 'required':
passed = (inputVal.val.trim() !== '')
break
case 'email':
passed = /^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/.test(inputVal.val)
break
case 'password':
passed = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[^]{8,16}$/.test(inputVal.val)
break
// 如果传入的是自定义的验证函数,我们直接执行即可。
case 'custom':
passed = rule.validator ? rule.validator() : true
break
default:
break
}
return passed
})
// 将全部验证通过后,将error设置为false
inputVal.error = !allPassed
return allPassed
}
return true
}
onMounted(() => {
emitter.emit('all-true', validate)
})
return {
inputVal,
validate
}
},
})
</script>
<style scoped>
.form-text {
color: #dc3545 !important;
}
</style>
复制代码
loading组件
当我们发送请求的时候,我们需要控制loading的展示,提高用户体验。
下面来看看它的模板吧。
<template>
<teleport to="#loader">
<div class="loader">
<div class="loader-mask"></div>
<div class="container">
<div class="spinner-border text-primary" role="status">
<span class="sr-only"></span>
</div>
</div>
</div>
</teleport>
</template>
复制代码
由于loading组件是独立于各个组件之外的,所以我们应该将它挂在到body标签中,作为直接子元素。这时就需要用到vue3内置的teleport
组件了。
<script>
import { defineComponent, onUnmounted } from "vue";
export default defineComponent({
name: 'Loader',
setup() {
const oLoader = document.createElement('div');
oLoader.id = 'loader'
document.body.appendChild(oLoader)
onUnmounted(() => {
document.body.removeChild(oLoader)
})
}
})
</script>
复制代码
下面来看看它的样式。
<style scoped>
.loader {
width: 100%;
height: 100%;
}
.loader-mask {
position: fixed;
z-index: 9;
left: 0;
right: 0;
top: 0;
background: #000000;
opacity: .4;
width: 100%;
height: 100%;
}
.spinner-border {
position: absolute;
top: 50%;
left: 50%;
}
</style>
复制代码
message组件
这个组件也是比较常见的,当用户输入错误信息,或者做了一些错误操作,我们就可以使用这个组件来提示用户。
他只需要传入提示信息和提示类型,来定制message组件。这个组件非常容易封装,下面直接给出代码。
<template>
<teleport to="#message">
<div class=" message alert message-info fixed-top mx-auto d-flex justify-content-between mt-2">
<div class="alert" :class="`alert-${type}`" role="alert">
{{message}}
</div>
</div>
</teleport>
</template>
<script lang="ts">
import { defineComponent, onUnmounted, PropType } from "vue";
export type MessageType = 'success' | 'error' | 'default'
export default defineComponent({
name: 'Message',
props: {
type: {
type: String as PropType<MessageType>,
required: true
},
message: String
},
setup() {
const oDiv = document.createElement('div');
oDiv.id = "message"
document.body.appendChild(oDiv)
onUnmounted(() => {
document.body.removeChild(oDiv)
})
}
})
</script>
<style scoped>
.message {
margin: 0 auto;
}
.alert {
width:500px;
text-align: center;
}
</style>
复制代码
但是请思考一下,我们的提示信息,一般想要通过函数来调用。方便操作。因为当我们出现错误时,都是在逻辑代码中的,直接调动函数就可以创建一个message组件。
这时候,你就需要了解一下createApp
API了。请访问
- 该函数接收一个根组件选项对象作为第一个参数
- 使用第二个参数,我们可以将根 prop 传递给应用程序
下面就来看看createMessage
函数组件怎么实现吧。
import { createApp } from 'vue'
import Message from './Message.vue'
export type MessageType = 'success' | 'error' | 'default'
const createMessage = (
message: string,
type: MessageType,
timeout = 2000
) => {
const messageInstance = createApp(Message, {
message,
type
})
const mountNode = document.createElement('div')
document.body.appendChild(mountNode)
messageInstance.mount(mountNode)
// 指定时间内,移除message组件
setTimeout(() => {
messageInstance.unmount(mountNode)
document.body.removeChild(mountNode)
}, timeout)
}
export default createMessage
复制代码
未完待续...