当前位置: 首页 > news >正文

h 函数的运用场景=== 函数式封装组件 (弹窗调用)

目录

表格中动态渲染内容

函数式封装组件 (弹窗调用)

什么是函数调用组件?

常见场景:

核心特点:

Modal 封装设计思路

动态渲染弹框 (初始版)

关于组件组册问题

处理动画与卸载

对外导出弹窗销毁方法

处理事件 (表单提交与校验)

封装拓展

完整代码

如何共享 Vue app 实例上下文 ?


上节课我们讲了 h 函数到底是什么,以及它的基本用法。我们乘热打铁,继续深入探索 h 函数在实际开发中的几种典型运用场景。

  • h函数组件的二次封装
  • 函数式组件封装 (弹窗调用)
  • 表格中动态渲染内容
  • 封装 HOC 组件

表格中动态渲染内容

在使用 ant-design-vue 渲染表格时,基础代码如下:

<script setup lang="ts">
const columns = [{ title: "Name",dataIndex: "name" },{ title: "Address",dataIndex: "address" }
]const data = [{ name: "John Brown", address: "New York No. 1 Lake Park" },{ name: "Jim Green", address: "London No. 1 Lake Park" },{ name: "Joe Black", address: "Sidney No. 1 Lake Park" }
]
</script><template><a-table :columns="columns" :data-source="data" bordered></a-table>
</template>

如果我们希望为“名字”加上超链接,可以通过插槽实现:

<script lang="ts" setup>
// columns 和 data 同上
</script><template><a-table :columns="columns" :data-source="data" bordered><template #bodyCell="{ column, text }"><template v-if="column.dataIndex === 'name'"><a href="#">{{ text }}</a></template></template></a-table>
</template>
 

我们还可以通过 customRender 配合 h 函数实现更灵活的渲染逻辑:

<template><a-table :columns="columns" :data-source="data" bordered></a-table>
</template><script setup lang="ts">
import { h } from "vue"const columns = [{title: "Name",dataIndex: "name",customRender: ({ text }: { text: string }) => {return h("a", { href: "#" }, text)}},{title: "Address",dataIndex: "address",}
]// data 同上
</script>

函数式封装组件 (弹窗调用)

在实际开发时,有时我们需要动态创建和挂载组件,例如实现一个弹窗、通知或某些特定的交互组件。以窗组件为例,如果按照传统方式使用弹框组件,我们需在组件内声明 ref、属性及回调函数,若存在多个弹框,会导致代码冗余、命名混乱。

例如:

  • 需为每个弹框声明独立 ref 控制显示状态
  • 点击事件需手动修改 ref 值
  • 表单提交需通过 ref 调用内部方法
什么是函数调用组件?
常见场景:
  • 弹窗(如 DialogModal
  • 通知组件(如 Toast、MessageBox
  • 特定交互(如动态表单、选择器)
核心特点:
  • 动态创建:不需要预定义在模板中;
  • 灵活传参:支持动态传递 props 和事件处理;
  • 销毁机制:组件可以在合适的时机被清理;
Modal 封装设计思路

通过封装函数实现弹框的动态渲染,核心逻辑如下:

使用函数调用方式

openDialog(Component, { message: '登录提示' }, { title: '用户登录' })
  • 第一个参数:弹窗中需要渲染的自定义组件;
  • 第二个参数:表单组件的 props 数据;
  • 第三个参数:弹框自身的属性(如 title );

渲染实现步骤

  • 使用 h 函数创建弹框组件实例;
  • 将自定义组件通过默认插槽插入弹框组件中;
  • 通过 createApp 动态创建应用实例并挂载到 DOM
动态渲染弹框 (初始版)
import { h, resolveComponent, createApp } from "vue";
import ElementPlus from "element-plus";
export const renderDialog = (component, componentProps, modalProps) => {const Dialog = () => {return h(resolveComponent("el-dialog"),{...modalProps,modelValue: true,},{default: () => h(component, componentProps),});};// 创建挂载节点const div = document.createElement("div");document.body.appendChild(div);// 动态创建应用并挂载const app = createApp(Dialog);app.use(ElementPlus);app.mount(div);
};

示例表单组件

<script setup>
import { reactive, ref } from "vue";const props = defineProps({msg: {type: String,default: "登录",},
});const formRef = ref();
const formState = reactive({username: "",password: "",
});async function submit() {// validate() 返回 boolean 或抛出异常(取决于 Element Plus 版本)const valid = await formRef.value?.validate();if (!valid) return;// 模拟异步提交return new Promise((resolve) => {setTimeout(() => resolve({ ...formState }), 2000);});
}defineExpose({ submit });
</script><template><div><h3>{{ props.msg }}</h3><el-form ref="formRef" :model="formState" label-width="80px"><el-form-itemlabel="用户名"prop="username":rules="[{ required: true, message: '请输入用户名!', trigger: 'blur' },]"><el-input v-model="formState.username" /></el-form-item><el-form-itemlabel="密码"prop="password":rules="[{ required: true, message: '请输入密码!', trigger: 'blur' }]"><el-input v-model="formState.password" type="password" /></el-form-item></el-form></div>
</template>

在页面使用

<template><el-button type="primary" @click="openDialog">打开弹窗</el-button>
</template>
<script setup>
import { renderDialog } from "./test_demo/Dialog";import form from "./test_demo/form.vue";
const openDialog = () => {renderDialog(form, { msg: "晚上好,请登录 👋" }, { title: "登录" });
};
</script>
关于组件组册问题

如果你在 mian.ts 是使用全局注册的方式使用组件库,那么上面的代码应该无法熏染 ,因为动态创建的 app 实例需重新注册组件, 如果是使用自动导入插件不需要重新注册

import ElementPlus from "element-plus";const renderDialog = (component, componentProps, modalProps) => {// 在动态应用中注册全局组件app.use(ElementPlus);
}
处理动画与卸载

直接卸载会导致动画丢失,需通过 onClosed 事件或定时器控制卸载时机:

import { h, resolveComponent, createApp, ref } from "vue";
import ElementPlus from "element-plus";
export const renderDialog = (component, componentProps, modalProps) => {// 声明一个 ref 响应式数据,用于控制弹窗的显示与隐藏 (为了保留 Modal 组件关闭动画)const open = ref(true);const Dialog = () => {return h(resolveComponent("el-dialog"),{...modalProps,modelValue: open.value, // 这里不是模板语法!需要 .value"onUpdate:modelValue": (val) => {open.value = val;},// PS: 如果组件库没有 onClosed 钩子,可以使用 setTimeout 处理onClosed() {// 关闭动画结束后,卸载组件app.unmount();document.body.removeChild(div);},},{default: () => h(component, componentProps),});};// 创建挂载节点const div = document.createElement("div");document.body.appendChild(div);// 动态创建应用并挂载const app = createApp(Dialog);app.use(ElementPlus);app.mount(div);
};

注意: 上面的代码中,Dialog 返回的必须是一个函数,因为响应式数据只有依赖 effect 才能正常工作, 在 Vue 中函数式组件能监听到 ref 响应式数据的变化,所以这里使用函数式组件 。

注意注意再注意!:使用 h 函数必须写成 函数式组件 才能触发响应式

const dialog = h() ❌
const dialog = () => h() ✅// ❌ 函数触发不了响应式
const NewModal1 = h(Modal, { modelValue: open.value, onClosed: () => open.value = false 
}, () => h('div', 'Hello World'))// ✅ 函数式组件可以触发响应式
const NewModal2 = () => h(Modal, {modelValue: open.value, onClosed: () => open.value = false 
}, () => h('div', 'Hello World 2'))

响应式触发的必要条件(下面两者缺一不可)!

  1. 数据是响应式的
  2. 在 effect 函数下面执行了 get 方法(建立了关联关系)
对外导出弹窗销毁方法

有时候我们想在某些逻辑执行后手动销毁弹窗,那我们可以在 openDialog 对外导出一个销毁方法:

import { h, resolveComponent, createApp, ref } from "vue";
import ElementPlus from "element-plus";
export const renderDialog = (component, componentProps, modalProps) => {// 声明一个 ref 响应式数据,用于控制弹窗的显示与隐藏 (为了保留 Modal 组件关闭动画)const open = ref(true);const Dialog = () => {return h(resolveComponent("el-dialog"),{...modalProps,modelValue: open.value, // 这里不是模板语法!需要 .value"onUpdate:modelValue": (val) => {open.value = val;},// PS: 如果组件库没有 onClosed 钩子,可以使用 setTimeout 处理onClosed() {// 关闭动画结束后,卸载组件app.unmount();document.body.removeChild(div);},},{default: () => h(component, componentProps),});};// 创建挂载节点const div = document.createElement("div");document.body.appendChild(div);// 动态创建应用并挂载const app = createApp(Dialog);app.use(ElementPlus);app.mount(div);// 导出一个对外关闭弹窗的方法,支持外部调用关闭弹窗const unmount = (delay = 900) => {if (!open.value) return;open.value = false;setTimeout(() => {app.unmount();document.body.removeChild(div);}, delay);};return { unmount };
};
处理事件 (表单提交与校验)

添加一个 ref 用于接收 弹窗传入的表单组件 实例,在确认按钮点击时手动触发表单组件对外导出的 submit 方法:

import { h, resolveComponent, createApp, ref } from "vue";
import ElementPlus from "element-plus";
export const renderDialog = (component, componentProps, modalProps) => {// 声明一个 ref 响应式数据,用于控制弹窗的显示与隐藏 (为了保留 Modal 组件关闭动画)const open = ref(true);const instance = ref(null);const loading = ref(false);const Dialog = () => {return h(resolveComponent("el-dialog"),{...modalProps,modelValue: open.value, // 这里不是模板语法!需要 .value"onUpdate:modelValue": (val) => {open.value = val;},// PS: 如果组件库没有 onClosed 钩子,可以使用 setTimeout 处理onClosed() {// 关闭动画结束后,卸载组件app.unmount();document.body.removeChild(div);},},{default: () => h(component, { ref: instance, ...componentProps }),footer: () =>h("div", { class: "dialog-footer" }, [h(resolveComponent("el-button"),{ onClick: () => (open.value = false) },() => "取 消"),h(resolveComponent("el-button"),{ type: "primary", onClick: submit, loading: loading.value },() => "确 认"),]),});};// 创建挂载节点const div = document.createElement("div");document.body.appendChild(div);// 动态创建应用并挂载const app = createApp(Dialog);app.use(ElementPlus);app.mount(div);// 导出一个对外关闭弹窗的方法,支持外部调用关闭弹窗const unmount = (delay = 0) => {if (!open.value) return;open.value = false;setTimeout(() => {app.unmount();document.body.removeChild(div);}, delay);};async function submit() {loading.value = true;try {await instance.value?.submit?.();open.value = false;} finally {loading.value = false;}}return { unmount, instance };
};
封装拓展

如果考虑自定义的提交事件怎么办 ?

const {methodKey = 'submit' }  = propsif(instace.value?.[methodKey]){try {await instace.value?.[methodKey]?.()}finally {loading.value = false}
}
 

完整代码

import { h, resolveComponent, createApp, ref } from "vue";
import ElementPlus from "element-plus";
export const renderDialog = (component, componentProps, modalProps) => {// 声明一个 ref 响应式数据,用于控制弹窗的显示与隐藏 (为了保留 Modal 组件关闭动画)let { confirmButtonText = "确 认", cancelButtonText = "取 消" } = modalProps;let { methodKey = "submit" } = componentProps;const open = ref(true);const instance = ref(null);const loading = ref(false);const Dialog = () => {return h(resolveComponent("el-dialog"),{...modalProps,modelValue: open.value, // 这里不是模板语法!需要 .value"onUpdate:modelValue": (val) => {open.value = val;},// PS: 如果组件库没有 onClosed 钩子,可以使用 setTimeout 处理onClosed() {// 关闭动画结束后,卸载组件app.unmount();document.body.removeChild(div);},},{default: () => h(component, { ref: instance, ...componentProps }),footer: () =>h("div", { class: "dialog-footer" }, [h(resolveComponent("el-button"),{ onClick: () => (open.value = false) },() => cancelButtonText),h(resolveComponent("el-button"),{ type: "primary", onClick: submit, loading: loading.value },() => confirmButtonText),]),});};// 创建挂载节点const div = document.createElement("div");document.body.appendChild(div);// 动态创建应用并挂载const app = createApp(Dialog);app.use(ElementPlus);app.mount(div);// 导出一个对外关闭弹窗的方法,支持外部调用关闭弹窗const unmount = (delay = 0) => {if (!open.value) return;open.value = false;setTimeout(() => {app.unmount();document.body.removeChild(div);}, delay);};async function submit() {loading.value = true;try {await instance.value?.[methodKey]?.();open.value = false;} finally {loading.value = false;}}return { unmount, instance };
};

如何共享 Vue app 实例上下文 ?

1. 暴力模式

在 main.js 中

const app = createApp(App)
window._APP_CONTEXT = app._contextapp.mount('#app')

使用

const app = createApp(Dialog)
app._context = window._APP_CONTEXT
// app._context.provides = window._APP_CONTEXT.provides

2. 重写 createApp

我们还可以重写一下 createApp 方法,在需要支持共享 App 的实例方法的时候,直接使用我们自己重写的 createApp 方法:

import { createApp as _createApp } from "vue";
import router from "./router/index";
import ElementPlus from "element-plus";
import "element-plus/dist/index.css";
import * as ElementPlusIconsVue from "@element-plus/icons-vue";
function loadPlugins(app) {for (const [key, component] of Object.entries(ElementPlusIconsVue)) {app.component(key, component);}app.use(ElementPlus);app.use(router);
}export const createApp = (...args) => {const app = _createApp(...args);loadPlugins(app);return app;
};

使用

// mainimport App from "./App.vue";
import { createApp } from "./index";
const app = createApp(App);
app.mount("#app");

http://www.dtcms.com/a/446311.html

相关文章:

  • 数据结构——排序算法全解析(入门到精通)
  • 建设装饰网站创客贴做网站吗
  • 爆炸特效-Unity-04-shader粒子系统
  • 公司做网站一般用什么域名网店设计师是干什么的
  • 【Redis】RedLock算法讲解
  • 网站专题页功能河北省住宅和城乡建设厅网站
  • stp root secondary 概念及题目
  • 马尔可夫链蒙特卡洛(MCMC):高维迷宫里的 “智能导航仪”—— 从商场找店到 AI 参数模拟
  • 无穿戴动捕大空间交互:如何靠摄像头实现全感官沉浸体验?
  • 求个没封的w站2022高端网站建设的要求
  • 网站经常修改好不好拼多多网店注册
  • 题解:洛谷P14127 [SCCPC 2021] K-skip Permutation
  • FreeBSD14.1 安装中文输入法fcitx
  • C++STL反向迭代器设计
  • 一文学会《C++》进阶系列之C++11
  • 腊肉网站的建设前景网页版微信可以发朋友圈吗
  • 大连凯杰建设有限公司网站wordpress 文章链接失效
  • 百度网站优化升上去国外网站入口
  • BIT*算法
  • Python常用三方模块——psutil
  • 网站开发的优势建设京东物流网站的目标是什么
  • 制作网站详细步骤爱客crm系统登录
  • Linux事件循环——高效处理多任务(高并发)
  • 【Linux】POSIX信号量、环形队列、基于环形队列实现生产者消费者模型
  • SELinux系列专题(一):SELinux是什么?
  • 三角函数公式全归纳
  • 热 动漫-网站正在建设中-手机版wordpress活动报名
  • 建设银行扬中网站织梦网站仿站
  • 网站建设公司伟置鄂尔多斯 网站制作
  • Hi3516DV500/HI3519DV500开发笔记之例程编译和测试