vue 组件函数式调用实战:以身份验证弹窗为例
通常我们在 Vue 中使用组件,是像这样在模板中写标签:
<MyComponent :prop="value" @event="handleEvent" />
而函数式调用,则是让我们像调用一个普通 JavaScript 函数一样来使用这个组件,例如:
MyComponentFunction({ prop: value }).then(result => { /* ... */ })。
接下来我们就用一个实际的例子来看看这种函数式调用的写法是怎么写的。
我们来实现一个非常通用的功能,在系统中,如果某些操作需要进行身份验证才能进行下一步,我们就需要实现一个身份验证的弹框,只有验证了用户的账号密码的情况下,才能执行接下来的逻辑。
以下是这个AuthBox
组件的部分,这里无需多言。
// src/components/AuthBox/src/AuthBox.vue
<template><el-dialogtitle="身份验证"v-model="state.dialogVisible"width="360px":custom-class="customClass"centeralign-centerdestroy-on-close:show-close="false":close-on-click-modal="false"@opened="handleOpened"@closed="handleClosed"><el-form ref="formRef" :model="state.formData" :rules="formRules" label-width="70px" :validate-on-rule-change="false"><el-form-item label="账号" prop="username"><el-inputref="usernameRef"v-model="state.formData.username"placeholder="请输入账号"@keyup.enter="handlePasswordFocus"/></el-form-item><el-form-item label="密码" prop="password"><el-inputref="passwordRef"v-model="state.formData.password"type="password"placeholder="请输入密码"@keyup.enter="handleConfirm"/></el-form-item></el-form><template #footer><div class="text-center"><el-button @click="handleCancel">取消</el-button><el-button type="primary" @click="handleConfirm">确认</el-button></div></template></el-dialog>
</template><script lang="ts" setup>
import { ref, reactive, nextTick, onMounted } from "vue";
import { ElDialog, ElForm, ElFormItem, ElInput, ElButton, ElMessage } from "element-plus";
import { AuthBoxState } from "./type";// 定义组件属性
const props = defineProps({// 自定义类名customClass: {type: String,default: ""},// 提示文本message: {type: String,default: ""}
});// 定义事件
const emits = defineEmits(["confirm", "cancel", "close", "vanish"]);// 组件状态
const state = reactive<AuthBoxState>({dialogVisible: false,formData: {username: "",password: ""}
});// 表单校验规则
const formRules = {username: [{ required: true, message: "请输入账号", trigger: "blur" }],password: [{ required: true, message: "请输入密码", trigger: "blur" }]
};// 表单引用
const formRef = ref();
const usernameRef = ref();
const passwordRef = ref();// 聚焦密码输入框
const handlePasswordFocus = () => {passwordRef.value.focus();
};// 确认按钮处理
const handleConfirm = () => {formRef.value.validate((valid: boolean) => {if (valid) {// 在实际应用中,这里可能会调用API进行验证// 这里我们简化为直接模拟验证通过// 触发confirm事件,传递表单数据emits("confirm", {username: state.formData.username,password: state.formData.password});// 关闭对话框state.dialogVisible = false;}});
};// 取消按钮处理
const handleCancel = () => {emits("cancel");state.dialogVisible = false;
};// 对话框打开后处理
const handleOpened = () => {// 对话框完全打开后才设置焦点,确保元素已渲染完成并可见usernameRef.value?.focus();
};// 对话框关闭处理
const handleClosed = () => {emits("close");// 组件消失,用于清理资源nextTick(() => {emits("vanish");});
};// 公开给外部的关闭方法
const doClose = () => {state.dialogVisible = false;
};// 初始化
const init = () => {// 重置表单state.formData.username = "";state.formData.password = "";// 显示对话框state.dialogVisible = true;
};// 组件挂载时初始化
onMounted(() => {init();
});// 暴露方法给父组件调用
defineExpose({doClose,state
});
</script>
接下来重点看看关于函数式调用的部分:
// src/components/AuthBox/index.tsimport { AppContext, ComponentPublicInstance, createVNode, render } from "vue";
import AuthBoxConstructor from "./src/AuthBox.vue";
import { AuthBoxData, AuthBoxOptions, AuthBoxState, Callback, IAuthBox } from "./src/type";/*** 实例映射表 - 存储所有通过函数式调用创建的AuthBox实例** Key: 组件实例的代理对象(vm),包含doClose方法* Value: 包含options、callback、Promise的resolve和reject函数** 作用: 让我们能够在异步事件(如用户点击确认)发生时,找到对应的Promise并解析它*/
const instanceMap = new Map<ComponentPublicInstance<{ doClose: () => void }>,{options: AuthBoxOptions;callback: Callback | undefined;resolve: (res: any) => void;reject: (reason?: any) => void;}
>();/*** 获取组件应该挂载到的DOM元素** @param props - 组件的props* @returns 挂载目标DOM元素,默认为document.body*/
const getAppendToElement = (props: AuthBoxOptions): HTMLElement => {// 这里简化处理,始终返回document.body// 在实际应用中,可以根据props.appendTo来自定义挂载位置return document.body;
};/*** 创建临时容器元素** @returns 新创建的div元素*/
const genContainer = (): HTMLDivElement => {return document.createElement("div");
};/*** 初始化组件实例** @param props - 传递给组件的属性* @param container - 临时容器元素* @param appContext - Vue应用上下文(可选)* @returns 创建的组件实例*/
const initInstance = (props: AuthBoxOptions, container: HTMLElement, appContext: AppContext | null = null) => {// 1. 使用组件构造函数和props创建虚拟节点const vnode = createVNode(AuthBoxConstructor, props);// 2. 如果提供了应用上下文,则设置到vnode上// (这确保组件能访问到应用的全局组件、插件等)if (appContext) {vnode.appContext = appContext;}// 3. 将虚拟节点渲染到临时容器中render(vnode, container);// 4. 将容器中渲染好的DOM元素移动到目标挂载点(通常是body)getAppendToElement(props).appendChild(container.firstElementChild!);// 5. 返回组件实例return vnode.component;
};/*** 显示AuthBox对话框** @param options - 配置选项* @param appContext - 应用上下文(可选)* @returns 创建的组件实例代理对象*/
const showAuthBox = (options: AuthBoxOptions, appContext?: AppContext | null) => {// 1. 创建临时容器const container = genContainer();// 2. 设置组件销毁时的回调// 当组件通过transition动画完全消失后触发options.onVanish = () => {// 2.1 从DOM中彻底移除组件// (将null渲染到container会清除其中的内容)render(null, container);// 2.2 从实例映射表中移除组件实例// (防止内存泄漏)instanceMap.delete(vm);};// 3. 设置用户点击确认按钮的回调options.onConfirm = (userData: { username: string; password: string }) => {// 3.1 获取该组件实例对应的Promise解析函数const currentInstance = instanceMap.get(vm)!;// 3.2 创建返回数据const resolveData: AuthBoxData = {username: userData.username,password: userData.password,action: "confirm"};// 3.3 解析Promise,传递结果数据// (这会使得await AuthBox()或.then()收到结果)currentInstance.resolve(resolveData);};// 4. 设置用户点击取消按钮的回调options.onCancel = () => {const currentInstance = instanceMap.get(vm)!;const resolveData: AuthBoxData = {username: "",password: "",action: "cancel"};currentInstance.resolve(resolveData);};// 5. 设置对话框关闭的回调options.onClose = () => {const currentInstance = instanceMap.get(vm)!;const resolveData: AuthBoxData = {username: "",password: "",action: "close"};currentInstance.resolve(resolveData);};// 6. 初始化并创建组件实例const instance = initInstance(options, container, appContext)!;// 7. 获取组件实例的代理对象// (这是我们与组件交互的接口)const vm = instance.proxy as ComponentPublicInstance<{doClose: () => void;} & AuthBoxState>;// 8. 返回代理对象return vm;
};/*** AuthBox函数 - 用于函数式调用AuthBox组件** 用法:* const result = await AuthBox({ title: '登录', message: '请输入您的账号和密码' });* if (result.action === 'confirm') {* console.log('用户名:', result.username);* console.log('密码:', result.password);* }** @param options - AuthBox配置选项* @param appContext - Vue应用上下文(可选)* @returns Promise,解析为AuthBoxData*/
async function AuthBox(options: AuthBoxOptions, appContext?: AppContext | null): Promise<AuthBoxData>;
function AuthBox(options: AuthBoxOptions, appContext: AppContext | null = null): Promise<AuthBoxData> {// 1. 创建并返回一个新的Promise// (这是函数式调用的核心,让我们可以使用await或.then()获取结果)return new Promise((resolve, reject) => {// 2. 获取应用上下文(优先使用传入的,否则使用预设的)const finalAppContext = appContext ?? (AuthBox as IAuthBox)._context;// 3. 显示AuthBox对话框,获取组件实例代理const vm = showAuthBox(options, finalAppContext);// 4. 将组件实例与Promise的resolve/reject函数关联起来// (这样在事件回调中就能找到对应的Promise进行解析)instanceMap.set(vm, {options,callback: undefined, // 保留字段,便于扩展resolve,reject});});
}/*** 关闭所有通过AuthBox函数创建的对话框*/
AuthBox.close = () => {// 1. 遍历实例映射表中的所有组件实例instanceMap.forEach((_, vm) => {// 2. 调用每个实例的doClose方法关闭对话框vm.doClose();});// 3. 清空实例映射表// (作为安全措施,确保不留下任何引用)instanceMap.clear();
};// 初始化应用上下文为null
(AuthBox as IAuthBox)._context = null;// 导出函数
export default AuthBox as IAuthBox;
下面我们就重点分析看一下,这个 indes.ts
做了什么:
- 导入依赖
import { AppContext, ComponentPublicInstance, createVNode, render } from "vue";
import AuthBoxConstructor from "./src/AuthBox.vue";
import { AuthBoxData, AuthBoxOptions, AuthBoxState, Callback, IAuthBox } from "./src/type";
vue
:从 Vue 中导入了核心的AppContext
(应用上下文)、ComponentPublicInstance
(组件公共实例类型)、createVNode
(创建虚拟DOM节点) 和render
(渲染虚拟DOM) 函数。AuthBoxConstructor
:这是实际的AuthBox.vue
组件。我们将其作为构造函数来创建组件实例。./src/type
:从类型定义文件中导入了与AuthBox
组件相关的各种类型,这里就不放出来了,根据自己实际业务来写就行。
instanceMap
:实例映射表
const instanceMap = new Map<ComponentPublicInstance<{ doClose: () => void }>,{options: AuthBoxOptions;callback: Callback | undefined;resolve: (res: any) => void;reject: (reason?: any) => void;}
>();
- 作用:这是一个关键的数据结构。由于我们可以通过函数调用创建多个
AuthBox
实例,instanceMap
用于存储每一个动态创建的AuthBox
组件实例 (vm
) 以及与之关联的配置项 (options
) 和 Promise 的resolve
/reject
函数。 - 键 (Key):
ComponentPublicInstance<{ doClose: () => void }>
,表示AuthBox
组件的实例代理对象。这个代理对象上预期有一个doClose
方法,用于关闭对话框。 - 值 (Value):一个对象,包含:
options
: 调用AuthBox
时传入的配置。callback
: 一个可选的回调函数 (这里标记为undefined
,保留了扩展性)。resolve
: Promise 的resolve
函数。当用户在AuthBox
中完成操作 (如点击确认) 时,我们会调用这个函数来解决 (fulfill) Promise,并传递结果。reject
: Promise 的reject
函数。如果发生错误或需要中断操作,会调用此函数。
- 为什么需要它? 当
AuthBox
组件内部发生事件 (如用户点击按钮) 时,我们需要一种方式找到当初调用它时创建的那个 Promise,以便能将结果传递回去。instanceMap
就是通过组件实例这个桥梁来找到对应的 Promise 控制函数的。
getAppendToElement
:获取挂载目标
const getAppendToElement = (props: AuthBoxOptions): HTMLElement => {return document.body;
};
- 作用:决定
AuthBox
组件的 DOM 元素最终应该被插入到页面的哪个位置。 - 实现:这里简化了处理,固定返回
document.body
,AuthBox
会被挂载到<body>
元素的末尾。 - 扩展性:在实际应用中,可以根据需要来自定义挂载位置。
genContainer
:创建临时容器
const genContainer = (): HTMLDivElement => {return document.createElement("div");
};
- 作用:创建一个临时的
<div>
元素。 - 为什么需要临时容器? Vue 的
render
函数需要一个容器元素来渲染虚拟节点。我们先将组件渲染到这个临时容器中,然后再将容器内的实际 DOM 元素(即AuthBox
的根元素)移动到由getAppendToElement
指定的最终挂载点。
initInstance
:初始化组件实例
const initInstance = (props: AuthBoxOptions, container: HTMLElement, appContext: AppContext | null = null) => {// 1. 使用组件构造函数和props创建虚拟节点const vnode = createVNode(AuthBoxConstructor, props);// 2. 如果提供了应用上下文,则设置到vnode上if (appContext) {vnode.appContext = appContext;}// 3. 将虚拟节点渲染到临时容器中render(vnode, container);// 4. 将容器中渲染好的DOM元素移动到目标挂载点(通常是body)getAppendToElement(props).appendChild(container.firstElementChild!);// 5. 返回组件实例return vnode.component;
};
- 作用:这个函数负责创建
AuthBox
组件的 Vue 实例并将其渲染到 DOM 中。 - 步骤:
- 创建虚拟节点 (VNode):使用
createVNode(AuthBoxConstructor, props)
。AuthBoxConstructor
是导入的.vue
文件,props
是传递给组件的属性。 - 设置应用上下文 (
appContext
):如果调用时传入了appContext
,则将其设置到vnode.appContext
。它能确保动态创建的组件实例可以访问到主 Vue 应用实例中注册的全局组件、指令、插件以及 provide/inject 等。 - 渲染到临时容器:调用
render(vnode, container)
,将虚拟节点转换成真实的 DOM 元素,并插入到container
(由genContainer
创建的div
)中。 - 移动到最终挂载点:
getAppendToElement(props).appendChild(container.firstElementChild!)
。这一步是将container
中的第一个子元素 (即AuthBox
组件的根 DOM 元素) 移动到document.body
(或getAppendToElement
返回的其他元素) 中。 - 返回组件实例:
vnode.component
是实际的 Vue 组件实例对象,我们可以通过它访问组件的属性和方法。
- 创建虚拟节点 (VNode):使用
showAuthBox
:显示对话框并处理回调
const showAuthBox = (options: AuthBoxOptions, appContext?: AppContext | null) => {const container = genContainer(); // 1. 创建临时容器// 2. 设置组件销毁时的回调options.onVanish = () => { // Linter Error: Property 'onVanish' does not exist on type 'AuthBoxOptions'.render(null, container); // 从DOM中彻底移除instanceMap.delete(vm); // 从实例映射表中移除};// 3. 设置用户点击确认按钮的回调options.onConfirm = (userData: { username: string; password: string }) => { // Linter Errorconst currentInstance = instanceMap.get(vm)!;const resolveData: AuthBoxData = { /* ... */ action: "confirm" };currentInstance.resolve(resolveData);};// 4. 设置用户点击取消按钮的回调options.onCancel = () => { // Linter Errorconst currentInstance = instanceMap.get(vm)!;const resolveData: AuthBoxData = { /* ... */ action: "cancel" };currentInstance.resolve(resolveData);};// 5. 设置对话框关闭的回调 (例如点击遮罩层或右上角关闭按钮)options.onClose = () => { // Linter Errorconst currentInstance = instanceMap.get(vm)!;const resolveData: AuthBoxData = { /* ... */ action: "close" };currentInstance.resolve(resolveData);};const instance = initInstance(options, container, appContext)!;const vm = instance.proxy as ComponentPublicInstance< /* ... */ >; // 7. 获取组件实例代理return vm; // 8. 返回代理对象
};
- 作用:这是实际处理
AuthBox
显示逻辑和事件回调的核心函数。 - 步骤与解释:
- 创建容器:调用
genContainer()
。 - 设置
options.onVanish
:- 这个回调函数会在
AuthBox
组件从界面上完全消失时被调用。 - 内部逻辑:
render(null, container)
: 这是 Vue 中卸载组件并从 DOM 中移除其内容的方法。instanceMap.delete(vm)
: 从instanceMap
中删除对此实例的引用,防止内存泄漏。
- 这个回调函数会在
- 设置
options.onConfirm
:- 当用户在
AuthBox
组件内部点击“确认”按钮时,AuthBox
组件会触发这个回调,并传入用户数据 (userData
)。 - 内部逻辑:
instanceMap.get(vm)!
: 通过组件实例vm
从instanceMap
中获取之前存储的 Promiseresolve
函数。- 构造
resolveData
:包含用户名、密码和操作类型 (action: "confirm"
)。 currentInstance.resolve(resolveData)
: 调用 Promise 的resolve
函数,将resolveData
作为结果传递出去。这将使得等待此 Promise 的await AuthBox(...)
调用得到结果。
- 当用户在
- 设置
options.onCancel
:- 当用户点击“取消”按钮时触发。
- 逻辑与
onConfirm
类似,但action
为"cancel"
,并且通常不包含用户输入数据。
- 设置
options.onClose
:- 当对话框因其他方式关闭(如点击遮罩层、按下 Esc 键,或组件内部调用关闭逻辑)时触发。
- 逻辑与
onCancel
类似,action
为"close"
。
- 初始化实例:调用
initInstance(options, container, appContext)
创建并挂载AuthBox
组件。注意,这里的options
对象已经被我们动态添加了onVanish
,onConfirm
,onCancel
,onClose
这些回调处理函数。AuthBox.vue
组件内部在合适的时机(如用户点击、组件卸载)就会调用这些通过props
传递进来的函数。 - 获取组件代理
vm
:instance.proxy
是组件实例的代理对象,通过它来调用组件的方法或访问其数据 (如果组件暴露了doClose
方法和AuthBoxState
中的状态)。 - 返回代理对象
vm
:将vm
返回。
- 创建容器:调用
AuthBox
主函数 (函数式调用的入口)
async function AuthBox(options: AuthBoxOptions, appContext?: AppContext | null): Promise<AuthBoxData>;
function AuthBox(options: AuthBoxOptions, appContext: AppContext | null = null): Promise<AuthBoxData> {return new Promise((resolve, reject) => {const finalAppContext = appContext ?? (AuthBox as IAuthBox)._context;const vm = showAuthBox(options, finalAppContext);instanceMap.set(vm, {options,callback: undefined,resolve,reject});});
}
- 作用:这是最终暴露给用户调用的函数。它使得我们可以像
const result = await AuthBox({ });
这样使用。 - 实现逻辑:
- 返回
Promise
:核心在于return new Promise((resolve, reject) => { ... });
。这使得AuthBox
函数的调用结果可以被await
或者通过.then()
方法处理。 - 确定应用上下文:
const finalAppContext = appContext ?? (AuthBox as IAuthBox)._context;
- 优先使用调用时直接传入的
appContext
。 - 如果没传,则尝试使用
(AuthBox as IAuthBox)._context
。这是一个预设的或全局设置的appContext
(见后续解释)。
- 优先使用调用时直接传入的
- 显示
AuthBox
:const vm = showAuthBox(options, finalAppContext);
调用我们之前定义的showAuthBox
函数,传入用户配置和确定的应用上下文。这将创建、挂载组件,并返回组件实例代理vm
。 - 存储到
instanceMap
:instanceMap.set(vm, { options, callback: undefined, resolve, reject });
- 它将当前创建的组件实例
vm
与新创建的 Promise 的resolve
和reject
函数关联起来,并存储到instanceMap
中。 - 这样,当
showAuthBox
中设置的onConfirm
,onCancel
,onClose
等回调被AuthBox.vue
组件内部触发时,它们可以通过vm
从instanceMap
中找到对应的resolve
(或reject
) 函数,从而完成 Promise,将数据传递给AuthBox
函数的调用者。
- 它将当前创建的组件实例
- 返回
AuthBox.close
:关闭所有实例
AuthBox.close = () => {instanceMap.forEach((_, vm) => {vm.doClose(); // 调用每个实例的doClose方法});instanceMap.clear(); // 清空映射表
};
- 作用:提供一个静态方法,用于关闭所有当前通过
AuthBox
函数打开的对话框实例。 - 实现:
- 遍历
instanceMap
中的所有组件实例代理 (vm
)。 - 调用每个
vm
上的doClose()
方法。这要求AuthBox.vue
组件通过defineExpose
暴露了一个名为doClose
的方法,用于执行关闭自身的逻辑(比如设置一个内部状态让组件隐藏,然后触发过渡动画,最终触发onVanish
)。 instanceMap.clear()
: 清空映射表。
- 遍历
以上就是这个 index.ts 的详细解释,相信你能明白vue中的组件是如何通过函数式调用的了,这样的调用方式非常的方便。