H1. 面向函数封装
H1.1. 需求
目前有这么两个组件:
- webxue-drawer.vue
<script setup lang="ts">
/**
* 抽屉组件
* 应用场景:新增、编辑表单内容
* 组件属性继承于a-drawer
* 额外添加了一个事件
* ok:当点击右下角确认按钮时触发
* */
const emits = defineEmits(["update:visible","ok"]);
const getContainer = () => document.querySelector(".webxue-drawer");
const onClose = () => {
emits("update:visible",false)
};
const onOk = () => emits("ok");
</script>
<template>
<div class="webxue-drawer">
<a-drawer width="max-content" @close="onClose" :closable="false" :get-container="getContainer" v-bind="$attrs">
<!-- 自定义header -->
<template #title>
<div class="webxue-drawer-title">
<slot name="headerLeft"></slot>
</div>
</template>
<!-- 右上角扩展图标 -->
<template #extra>
<div class="extra-btn" @click="onClose"><CloseOutlined/></div>
</template>
<!-- 自定义内容区域 -->
<div class="webxue-drawer-content">
<slot></slot>
</div>
<!-- 自定义footer -->
<template #footer>
<div class="glc-drawer-footer">
<a-button @click="onClose"><CloseOutlined />取消</a-button>
<a-button type="primary" @click="onOk"><CheckOutlined />确认</a-button>
</div>
</template>
</a-drawer>
</div>
</template>
<style lang="less" scoped>
.webxue-drawer{
background-color: #fff;
.extra-btn{
cursor:pointer;
}
.webxue-drawer-content{
min-width: 30vw;
}
.webxue-drawer-footer{
display: flex;
justify-content: flex-end;
.ant-btn + .ant-btn{
margin-left: 8px;
}
}
}
</style>
- webxue-view.vue
<script setup lang="ts">
const submit = () => console.log("我被点击了");
</script>
<template>
<div>
<a-button type="primary" @click="submit">点击</a-button>
</div>
</template>
这是两个 SFC
组件,需求是:
在自定义页面中,通过函数调用的方式挂载并渲染
webxue-drawer
组件,并且可以为webxue-drawer
组件传递参数
和插槽
,且能监听
到组件emit的事件
,并且点击webxue-drawer
组件的确认按钮可以调用默认插槽
的方法。
H1.2. 实现
H1.2.1 编写方法函数
在 utils
目录下新建一个 Render.ts
文件:
// 引入vue的h函数、createApp函数、ref响应式变量
import { h,createApp,ref } from "vue";
// 引入函数式调用的SFC组件
import WebDrawer from "@/components/demo-comp/webxue-drawer/index.vue";
// 引入SFC组件所依赖的组件
import { Drawer,Button } from "ant-design-vue";
import { CloseOutlined,CheckOutlined } from "@ant-design/icons-vue";
/**
* 函数式打开抽屉的方法
* @param {WebxueDrawerProp} data
* @param {HTMLElement} data.el - 组件挂载元素
* @param {any} data.headerLeftSlot- headerLeft插槽
* @param {any} data.defaultSlot - default插槽
* @param {(self:any) => any} data.onConfirm - 确认按钮的回调
* */
export const WebxueDrawer = ({ el,headerLeftSlot,defaultSlot,onConfirm }:WebxueDrawerProp) => {
// 创建一个组件
const app = createApp({
setup(){
// 打开状态
const showModal = ref(true);
return {
showModal
}
},
// 渲染函数
render(){
return h(WebDrawer,{
// 组件打开状态
visible:this.showModal,
// 组件内部触发update:visible事件
'onUpdate:visible':(nval) => {
this.showModal = nval;
},
// 抽屉关闭动画结束卸载组件
onAfterVisibleChange(visible){
if(!visible) app.unmount();
},
// ok时执行回调
onOk:() => {
onConfirm && onConfirm(this);
},
},{
// 默认插槽
default:() => this.renderSlot(defaultSlot),
// headerLeft插槽
headerLeft:() => this.renderSlot(headerLeftSlot)
})
},
methods:{
// 渲染插槽方法
renderSlot(slotBlock){
if(!slotBlock) return null;
if (typeof slotBlock === 'string') {
return h('div', { innerHTML: slotBlock });
} else {
return slotBlock();
}
}
}
})
// 所依赖的组件及插件注册
app.component("CloseOutlined",CloseOutlined);
app.component("CheckOutlined",CheckOutlined);
app.use(Drawer).use(Button);
// 组件挂载
app.mount(el);
}
export interface WebxueDrawerProp {
// 挂载元素
el:HTMLElement,
// headerLeft插槽
headerLeftSlot?:any,
// default插槽
defaultSlot?:any,
// 确定按钮的回调
onConfirm?:(self:any) => any
}
H1.2.2 调用方法
接下来在我们页面中通过调用这个方法来挂载并渲染组件,给组件传参,传递插槽,调用插槽的方法:
<script setup lang="ts">
// 引入h方法是为了给组件的default插槽传入一个组件
import { h } from "vue";
// 引入函数式调用组件的方法
import { WebxueDrawer } from "@/utils/Render";
// default插槽组件
import WebxueDemo from "@/components/demo-comp/webxue-demo/index.vue";
// 点击事件执行
const onOpenShowDrawer = () => {
// 调用这个方法
WebxueDrawer({
// 传入挂载到的元素
el:document.querySelector(".demo-drawer"),
// 默认插槽传入一个组件,并为其设置ref属性
defaultSlot:() => h(WebxueDemo,{
ref:"webxueDemoRef",
}),
// headerLeft插槽,传入html字符串将使用innerHTML渲染
headerLeftSlot:"<h1 class='h1-class'>哈哈哈</h1>",
// 确定按钮的回调
onConfirm(self){
// self就是组件对象,组件对象下由上面组件定义的ref
const { $refs } = self;
// 可以通过ref调用插槽组件的方法,但需要defineExpose,详情看第三步
console.log($refs.webxueDemoRef.submit())
// 可以通过设置组件的showModal为false来关闭抽屉
self.showModal = false;
}
})
}
</script>
<template>
<div class="demo-render">
<!-- 点击这个按钮函数式打开抽屉 -->
<a-button type="primary" @click="onOpenShowDrawer">打开</a-button>
<!-- 定义这个div是为了挂载抽屉 -->
<div class="demo-drawer"></div>
</div>
</template>
<style lang="less" scoped>
/* 要给webxue-drawer的插槽设置样式,需要被穿透 */
:deep(.h1-class){
color: #f00;
}
</style>
H1.2.3 default插槽组件方法可被外部通过ref调用
我们需要在 webxue-view.vue
组件中加入一行代码:
// 将submit方法暴露出去,然后在父组件中就可以通过ref.submit调用
defineExpose({ submit })
H2. 面向对象封装
H2.1 需求
有一个 loading
组件,现需要通过函数式调用的方式来打开和关闭 loading
<script setup lang="ts">
/**
* 全局loading组件
* */
defineProps({
// loading状态
loading:{ type:Boolean,default:false },
// loading文字
loadingText:{ type:String,default:"加载中..." }
})
</script>
<template>
<div class="webxue-loading" v-if="loading">
<div class="webxue-loading-mask"></div>
<div class="webxue-loading-wrap">
<div class="loading-box">
<img class="loading-bg" src="@/assets/images/loading-bg.png" alt="">
<img class="loading-icon" src="@/assets/images/loading-icon.png" alt="">
</div>
<div class="loading-text-box">
<div class="loading-text">{{ loadingText }}</div>
</div>
</div>
</div>
</template>
<style lang="less" scoped>
@keyframes rotateFin {
from{ transform: rotate(0deg); }
to{ transform: rotate(360deg); }
}
.webxue-loading{ position: fixed;top: 0;right: 0;bottom: 0;left: 0;z-index: 1000;
.webxue-loading-mask{ position: absolute;left: 0;right: 0;top: 0;bottom: 0;background-color: rgba(0,0,0,.73); }
.webxue-loading-wrap{ display: flex;flex-direction: column;align-items: center;height: 100%;padding-top: 15vh;
.loading-box{ display: flex;align-items: center;position: relative;margin-bottom: 20px;
.loading-bg{ width: 120px;animation:rotateFin 1.5s linear infinite; }
.loading-icon{ position: absolute;top: 50%;left: 50%;transform: translate(-50%,-50%); }
}
.loading-text-box{ font-size: 16px;color: #fff;z-index: 1; }
}
}
</style>
H2.2 与示例一对比
- 传入挂载节点el,示例二直接主动生成挂载节点,支持传参挂载节点
- 示例二使用面向对象的方式,更容易阅读和维护
- ref直接定义为class的属性,他本身就是个响应式变量的包裹器,所以可以不用放在setup
H2.3 实现
H2.3.1 定义方法
在组件同级目录下,定义一个 render.ts
文件
/**
* 函数式调用loading
* */
import webxueLoading from "./index.vue";
import { createApp, h, ref } from "vue";
export class WebxueLoading {
// loading组件对象
private loadingInstance = null;
// 挂载节点
private el = null;
// loading状态
private showLoading = ref(false);
constructor(el?:HTMLElement) {
if(el) this.el = el;
this.init();
}
init(){
const _this = this;
this.initEl();
this.loadingInstance = createApp({
render(){
return h(WebxueLoading ,{
loading:_this.showLoading.value
})
}
})
this.loadingInstance.mount(this.el);
}
// 挂载根
initEl(){
if(this.el) return;
let rootDiv = document.querySelector(".webxue-loading-root");
if(rootDiv) return;
rootDiv = document.createElement("div");
rootDiv.className = "webxue-loading-root";
document.body.append(rootDiv);
this.el = rootDiv;
}
open(){
this.showLoading.value = true;
}
close(){
this.showLoading.value = false;
this.el.remove();
this.loadingInstance.unmount();
}
}
H2.3.2 调用方法
假设有一个按钮,这个按钮点击之后会 打开loading
,然后请求数据,数据请求完成,关闭loading
,这里我就只写点击事件
,请求数据我使用定时器
来延时
const onOpenLoading = () => {
const loading = new WebxueLoading();
loading.open();
setTimeout(() => {
loading.close();
},1500)
}