likes
comments
collection
share

🤔 优化使用体验 Vue Hook 改造 Dialog 对话框组件实现 API 调用

作者站长头像
站长
· 阅读数 96

前言

在上一篇文章中,我们使用 Vue+Zag+PandaCSS 实现一个超丝滑的对话框组件,这篇文章基于对话框组件进行补充,新增一种通过 Vue Hook 去控制对话框的使用方式:

传统组件用法:

<script setup lang="ts">
import { ref } from "vue";

import Button from "./components/button/index.tsx";
import Dialog from "./components/dialog/index.tsx";

const isOpen = ref(false);
</script>

<template>
  <div>
    <Button @click="isOpen = !isOpen">show dialog</Button>
    <Dialog
      v-model="isOpen"
      title="Edit Profile"
      content="Make changes to your profile here. Click save when you are done."
    >
    </Dialog>
  </div>
</template>

hook 用法:

<template>
  <div>
    <ZButton @click="open">show dialog</ZButton>
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";

import { ZButton } from "./components/button/index.ts";
import { useDialog } from "./components/dialog/index.ts";

const { open, close } = useDialog({
  title: "Edit Profile",
  content: "Make changes to your profile here. Click save when you are done.",
});
</script>

相比于直接在页面中引入再手动写在模板中,使用 Hook 调用更加简单,而且大部分的对话框场景并不复杂,不需要去进行大量的自定义元素样式,因此 Hook 往往是更加高频的使用方式。

如果你想学习有关组件实现 Vue TSX 的前置知识,也可以参考以往的文章,前文回顾:

改造

在改造前,我们先确认一下这个 hook 需要实现的功能:

  1. 调用 hook 的时候 自动将 Dialog 组件挂载 在页面中,不需要在模板中手动写入
  2. 不需要手动管理 Dialog 打开或关闭的状态,只需要调用打开或关闭的方法即可使用

自动挂载组件

首先实现调用 hook 时自动挂载组件

import { createApp, onMounted, onUnmounted } from "vue";

import { ZDialog, ZDialogProps } from ".";

const useDialog = (props: ZDialogProps) => {
  const app = createApp(ZDialog, { ...props, modelValue: true });

  let DialogDom: HTMLDivElement;

  onMounted(() => {
    DialogDom = document.createElement("div");
    app.mount(DialogDom);
    document.body.appendChild(DialogDom);
  });

  onUnmounted(() => {
    app.unmount();
    document.body.removeChild(DialogDom);
  });
};

export default useDialog;
  1. createApp 函数用于创建一个Vue实例,该实例的根组件为 ZDialog 组件,并将 props 对象与modelValue 属性合并作为 rootProps 传入, modelValue 暂时默认为 true
  2. 定义了一个名为 DialogDom 的变量,类型为 HTMLDivElement,用于存储即将创建的对话框组件的 DOM 节点。
  3. onMounted 钩子函数在组件挂载前调用创建了一个新的 div 元素作为组件的挂载点,并使用 app.mount 方法将新的 Vue 实例挂载到该元素上。
  4. 接着,DialogDom 被添加到 document.body 中,使对话框组件显示在页面上。
  5. onUnmounted钩子函数在组件卸载前调用 app.unmount() 方法将 Vue 实例卸载,然后使用document.body.removeChild(DialogDom) 移除对话框组件的 DOM 节点。

这样就实现了调用 hook 时自动挂载元素,由于 modelValue 默认为 true 所以已进入页面就会弹出对话框:

🤔 优化使用体验 Vue Hook 改造 Dialog 对话框组件实现 API 调用

在 hook 内维护状态

接下来在 hook 中补充打开和关闭的方法,并且维护一个弹窗是否打开的状态:

import { createApp, onMounted, onUnmounted, ref } from "vue";

import { ZDialog, ZDialogProps } from ".";

const useDialog = (props: ZDialogProps) => {
+  const isOpen = ref(false);
+  const app = createApp(ZDialog, { ...props, modelValue: isOpen.value });

  let DialogDom: HTMLDivElement;

  onMounted(() => {
    DialogDom = document.createElement("div");
    app.mount(DialogDom);
    document.body.appendChild(DialogDom);
  });

  onUnmounted(() => {
    app.unmount();
    document.body.removeChild(DialogDom);
  });

+  const open = () => {
+    isOpen.value = true;
+  };

+  const close = () => {
+    isOpen.value = false;
+  };

+  return { open, close };
};

export default useDialog;

然后修改页面调用 hook 的方法,在点击按钮时执行 open 函数:

<template>
  <div>
    <ZButton @click="open">show dialog</ZButton>
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";

import { ZButton } from "./components/button/index.ts";
import { useDialog } from "./components/dialog/index.ts";

const { open, close } = useDialog({
  title: "Edit Profile",
  content: "Make changes to your profile here. Click save when you are done.",
});
</script>

在这样修改后,你会发现点击按钮并不能弹出弹窗:

🤔 优化使用体验 Vue Hook 改造 Dialog 对话框组件实现 API 调用

这是为什么呢,明明我已经将 isOpen 传入组件中了,为什么将 isOpen 修改为 true 却没有弹出对话框呢?

这是因为 createApp 函数的第二个参数 rootProps 并不是响应式的,并不会随着你的值改变了而重新渲染,具体的上下文也可以参考这个 issue github.com/vuejs/core/…

因此我们要调整一种写法:

// old
const app = createApp(ZDialog, { ...props, modelValue: isOpen.value });

// new
const app = createApp(() => <ZDialog {...props} v-model={isOpen.value} />);

直接将 props 传入 Dialog 组件,而不经过 rootProps, 通过函数式组件直接返回一个 ZDialog 组件的实例,这么写还有一个好处,就是可以直接使用 v-model 双向绑定,不需要传递值的同时再传递一个修改值的方法。修改后再看看效果:

🤔 优化使用体验 Vue Hook 改造 Dialog 对话框组件实现 API 调用

可以看到我们已经顺利的实现了对话框的打开和关闭功能。

响应式参数

想要实现对话框内的元素随着 props 的变化进行响应式更改,只需要将传入的 props 使用 reactive 函数包装一下:

<template>
  <div>
    <ZButton @click="open">show dialog</ZButton>
  </div>
</template>

<script setup lang="ts">
import { ref, reactive } from "vue";

import { ZButton } from "./components/button/index.ts";
import { useDialog } from "./components/dialog/index.ts";

const dialogProps = reactive({
  title: "Edit Profile",
  content: "Make changes to your profile here. Click save when you are done.",
  size: "sm",
});

setTimeout(() => {
  dialogProps.size = "xl";
}, 5000);

const { open, close } = useDialog(dialogProps);
</script>

这里使用 reactive 函数包装了 props 再传入 hook,设置了一个定时器在 5 秒后修改弹窗的大小,效果如下图:

🤔 优化使用体验 Vue Hook 改造 Dialog 对话框组件实现 API 调用

可以看到对话框随着 props 的改变顺利的重新渲染改变了尺寸。

最后修改一下 hook 的参数,将 modelValue 从参数中忽略掉,避免使用 hook 传值时类型提示错误,重复传入了 modelValue

// old
const useDialog = (props:ZDialogProps) => {

// new
const useDialog = (props: Omit<ZDialogProps, "modelValue">) => {

传入自定义插槽

在 vue 中,由于组件得通过插槽的形式传入自定义元素,而不是像 react 那样 props 一把梭,因此我们除了 props 之外,我们还得单独处理 slots 属性:

import { createApp, onMounted, onUnmounted, ref } from "vue";

import { ZDialog, ZDialogProps } from ".";

const useDialog = (
  props: Omit<ZDialogProps, "modelValue">,
+  slots: InstanceType<typeof ZDialog>["$slots"],
) => {
  const isOpen = ref(false);
  const app = createApp(() => (
+    <ZDialog {...props} v-model={isOpen.value} v-slots={slots} />
  ));

  let DialogDom: HTMLDivElement;

  onMounted(() => {
    DialogDom = document.createElement("div");
    app.mount(DialogDom);
    document.body.appendChild(DialogDom);
  });

  onUnmounted(() => {
    app.unmount();
    document.body.removeChild(DialogDom);
  });

  const open = () => {
    isOpen.value = true;
  };

  const close = () => {
    isOpen.value = false;
  };

  return { open, close };
};

export default useDialog;

这里为 hook 增加第二个参数 slots,类型通过 InstanceType<typeof ZDialog>["$slots"] 的方式获取,然后在组件中通过 v-slots 的方式传入,这里的类型需要你在组件中提前定义好,在我的 Dialog 组件中我是这么定义的:

const ZDialog = defineComponent({
  slots: Object as SlotsType<{
    default?: any;
    title?: any;
    footer?: any;
  }>,
  // ...
});

export default ZDialog;

测试一下效果,在调用 hook 时第二个参数传入 title 插槽一个自定义的文本内容,h() 函数用于在 vue 文件中创建 vnodes,如果你使用的是 JSX 则直接传入一个返回 Element 的函数即可:

// vue
const { open, close } = useDialog(dialogProps, {
  title: h("h1", { style: { color: "red" }, innerHTML: "Hello" }),
});

// jsx
const { open, close } = useDialog(dialogProps, {
  title: <h1 style={{color: "red"}}>"Hello</h1>,
});

我们看下效果,标题确实变为自定义传入的红色 Hello 了:

🤔 优化使用体验 Vue Hook 改造 Dialog 对话框组件实现 API 调用

总结

将对话框组件改造为 hook 的调用方式,其中有一些小的细节还是很容易踩坑的。例如 rootProps 没有响应式,如果传递 slots 等等。本文对应的源码可以在这里看到 github.com/oil-oil/zax…, 后续的更新也都在这个仓库中, 快去点个 star 🌟

如果文章对你有帮助在收藏的同时也可以为我点个赞👍,respect!

转载自:https://juejin.cn/post/7287907234716254249
评论
请登录