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
调用内部方法
什么是函数调用组件?
常见场景:
- 弹窗(如
Dialog
、Modal
) - 通知组件(如
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'))
响应式触发的必要条件(下面两者缺一不可)!
- 数据是响应式的
- 在
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");