vue 组件函数式调用实战:以身份验证弹窗为例

通常我们在 Vue 中使用组件,是像这样在模板中写标签:

<MyComponent :prop="value" @event="handleEvent" />

而函数式调用,则是让我们像调用一个普通 JavaScript 函数一样来使用这个组件,例如:

MyComponentFunction({ prop: value }).then(result => { /* ... */ })

接下来我们就用一个实际的例子来看看这种函数式调用的写法是怎么写的。

我们来实现一个非常通用的功能,在系统中,如果某些操作需要进行身份验证才能进行下一步,我们就需要实现一个身份验证的弹框,只有验证了用户的账号密码的情况下,才能执行接下来的逻辑。

以下是这个AuthBox组件的部分,这里无需多言。

// src/components/AuthBox/src/AuthBox.vue
<template>
  <el-dialog
    title="身份验证"
    v-model="state.dialogVisible"
    width="360px"
    :custom-class="customClass"
    center
    align-center
    destroy-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-input
          ref="usernameRef"
          v-model="state.formData.username"
          placeholder="请输入账号"
          @keyup.enter="handlePasswordFocus"
        />
      </el-form-item>
      <el-form-item label="密码" prop="password">
        <el-input
          ref="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.ts

import { 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 做了什么:

  1. 导入依赖
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 组件相关的各种类型,这里就不放出来了,根据自己实际业务来写就行。
  1. 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 控制函数的。
  1. getAppendToElement:获取挂载目标
const getAppendToElement = (props: AuthBoxOptions): HTMLElement => {
  return document.body;
};
  • 作用:决定 AuthBox 组件的 DOM 元素最终应该被插入到页面的哪个位置。
  • 实现:这里简化了处理,固定返回 document.bodyAuthBox 会被挂载到 <body> 元素的末尾。
  • 扩展性:在实际应用中,可以根据需要来自定义挂载位置。
  1. genContainer:创建临时容器
const genContainer = (): HTMLDivElement => {
  return document.createElement("div");
};
  • 作用:创建一个临时的 <div> 元素。
  • 为什么需要临时容器? Vue 的 render 函数需要一个容器元素来渲染虚拟节点。我们先将组件渲染到这个临时容器中,然后再将容器内的实际 DOM 元素(即 AuthBox 的根元素)移动到由 getAppendToElement 指定的最终挂载点。
  1. 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 中。
  • 步骤
    1. 创建虚拟节点 (VNode):使用 createVNode(AuthBoxConstructor, props)AuthBoxConstructor 是导入的 .vue 文件,props 是传递给组件的属性。
    2. 设置应用上下文 (appContext):如果调用时传入了 appContext,则将其设置到 vnode.appContext。它能确保动态创建的组件实例可以访问到主 Vue 应用实例中注册的全局组件、指令、插件以及 provide/inject 等。
    3. 渲染到临时容器:调用 render(vnode, container),将虚拟节点转换成真实的 DOM 元素,并插入到 container(由 genContainer 创建的 div)中。
    4. 移动到最终挂载点getAppendToElement(props).appendChild(container.firstElementChild!)。这一步是将 container 中的第一个子元素 (即 AuthBox 组件的根 DOM 元素) 移动到 document.body (或 getAppendToElement 返回的其他元素) 中。
    5. 返回组件实例vnode.component 是实际的 Vue 组件实例对象,我们可以通过它访问组件的属性和方法。
  1. 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 Error
    const currentInstance = instanceMap.get(vm)!;
    const resolveData: AuthBoxData = { /* ... */ action: "confirm" };
    currentInstance.resolve(resolveData);
  };

  // 4. 设置用户点击取消按钮的回调
  options.onCancel = () => { // Linter Error
    const currentInstance = instanceMap.get(vm)!;
    const resolveData: AuthBoxData = { /* ... */ action: "cancel" };
    currentInstance.resolve(resolveData);
  };

  // 5. 设置对话框关闭的回调 (例如点击遮罩层或右上角关闭按钮)
  options.onClose = () => { // Linter Error
    const 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 显示逻辑和事件回调的核心函数。
  • 步骤与解释
    1. 创建容器:调用 genContainer()
    2. 设置 options.onVanish
      • 这个回调函数会在 AuthBox 组件从界面上完全消失时被调用。
      • 内部逻辑:
        • render(null, container): 这是 Vue 中卸载组件并从 DOM 中移除其内容的方法。
        • instanceMap.delete(vm): 从 instanceMap 中删除对此实例的引用,防止内存泄漏。
    3. 设置 options.onConfirm
      • 当用户在 AuthBox 组件内部点击“确认”按钮时,AuthBox 组件会触发这个回调,并传入用户数据 (userData)。
      • 内部逻辑:
        • instanceMap.get(vm)!: 通过组件实例 vminstanceMap 中获取之前存储的 Promise resolve 函数。
        • 构造 resolveData:包含用户名、密码和操作类型 (action: "confirm")。
        • currentInstance.resolve(resolveData): 调用 Promise 的 resolve 函数,将 resolveData 作为结果传递出去。这将使得等待此 Promise 的 await AuthBox(...) 调用得到结果。
    4. 设置 options.onCancel
      • 当用户点击“取消”按钮时触发。
      • 逻辑与 onConfirm 类似,但 action"cancel",并且通常不包含用户输入数据。
    5. 设置 options.onClose
      • 当对话框因其他方式关闭(如点击遮罩层、按下 Esc 键,或组件内部调用关闭逻辑)时触发。
      • 逻辑与 onCancel 类似,action"close"
    6. 初始化实例:调用 initInstance(options, container, appContext) 创建并挂载 AuthBox 组件。注意,这里的 options 对象已经被我们动态添加了 onVanish, onConfirm, onCancel, onClose 这些回调处理函数。AuthBox.vue 组件内部在合适的时机(如用户点击、组件卸载)就会调用这些通过 props 传递进来的函数。
    7. 获取组件代理 vminstance.proxy 是组件实例的代理对象,通过它来调用组件的方法或访问其数据 (如果组件暴露了 doClose 方法和 AuthBoxState 中的状态)。
    8. 返回代理对象 vm:将 vm 返回。
  1. 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({ }); 这样使用。
  • 实现逻辑
    1. 返回 Promise:核心在于 return new Promise((resolve, reject) => { ... });。这使得 AuthBox 函数的调用结果可以被 await 或者通过 .then() 方法处理。
    2. 确定应用上下文const finalAppContext = appContext ?? (AuthBox as IAuthBox)._context;
      • 优先使用调用时直接传入的 appContext
      • 如果没传,则尝试使用 (AuthBox as IAuthBox)._context。这是一个预设的或全局设置的 appContext (见后续解释)。
    3. 显示 AuthBoxconst vm = showAuthBox(options, finalAppContext); 调用我们之前定义的 showAuthBox 函数,传入用户配置和确定的应用上下文。这将创建、挂载组件,并返回组件实例代理 vm
    4. 存储到 instanceMapinstanceMap.set(vm, { options, callback: undefined, resolve, reject });
      • 它将当前创建的组件实例 vm 与新创建的 Promise 的 resolvereject 函数关联起来,并存储到 instanceMap 中。
      • 这样,当 showAuthBox 中设置的 onConfirm, onCancel, onClose 等回调被 AuthBox.vue 组件内部触发时,它们可以通过 vminstanceMap 中找到对应的 resolve (或 reject) 函数,从而完成 Promise,将数据传递给 AuthBox 函数的调用者。
  1. AuthBox.close:关闭所有实例
AuthBox.close = () => {
  instanceMap.forEach((_, vm) => {
    vm.doClose(); // 调用每个实例的doClose方法
  });
  instanceMap.clear(); // 清空映射表
};
  • 作用:提供一个静态方法,用于关闭所有当前通过 AuthBox 函数打开的对话框实例。
  • 实现
    1. 遍历 instanceMap 中的所有组件实例代理 (vm)。
    2. 调用每个 vm 上的 doClose() 方法。这要求 AuthBox.vue 组件通过 defineExpose 暴露了一个名为 doClose 的方法,用于执行关闭自身的逻辑(比如设置一个内部状态让组件隐藏,然后触发过渡动画,最终触发 onVanish)。
    3. instanceMap.clear(): 清空映射表。

以上就是这个 index.ts 的详细解释,相信你能明白vue中的组件是如何通过函数式调用的了,这样的调用方式非常的方便。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值