用脚本来写函数式弹窗,更快更爽

前言

在业务开发中,弹窗是我们常常能够遇到的开发需求,通常我们使用组件都是通过show变量来控制弹窗的开启或者隐藏。这样面对简单的业务需求的确可以满足了,但是面对深层嵌套,不同地方打开同一个弹窗便会让我们头疼。于是我想通过函数的方式来打开弹窗,通过函数参数的方式来向弹窗内更新props和处理弹窗的emit事件。

iShot_2023-08-15_10.13.24.gif

<template>
  <n-button @click="open">open</n-button>
</template>

<script setup lang="ts">
import { injectTestModal } from "./test-modal/use-test-modal"
const model = injectTestModal()
function open() {
  model?.open({
    testValue: "传给弹窗组件的值",
    onConfirm({ formData }, close, endLoading) {
      console.log({ ...formData })
      endLoading()
      close()
    },
  })
}
</script>

传统vue编写弹窗

通过变量来直接控制弹窗的开启和关闭。

<template>
  <n-button @click="showModal = true">
    来吧
  </n-button>
  <n-modal v-model:show="showModal" preset="dialog" title="Dialog">
    <template #header>
      <div>标题</div>
    </template>
    <div>内容</div>
    <template #action>
      <div>操作</div>
    </template>
  </n-modal>
</template>

<script lang="ts">
import { defineComponent, ref } from 'vue'

export default defineComponent({
  setup () {
    return {
      showModal: ref(false)
    }
  }
})
</script>

痛点

  • 深层次的传props让人有很大的心理负担,污染组件props
  • 要关注弹窗show变量的true,false

函数式弹窗

在主页面用Provider包裹一下

// RootPage.vue
<ModalProvider>
  <ChildPage></ChildPage>
</ModalProvider>

<script setup lang="ts">
import ModalProvider from "./ModalProvider.vue"
import ChildPage from "./ChildPage.vue"
</script>

在页面内的某个子组件中,直接通过oepn方法打开弹窗

// ChidPage.vue
<template>
	<n-button @click="open">open</n-button>
</template>

<script setup lang="ts">
import { injectTestModal } from "./test-modal/use-test-modal"
const model = injectTestModal()
function open() {
	model?.open({
		testValue: "传给弹窗组件的值",
		onConfirm({ formData }, close, endLoading) {
			console.log({ ...formData })
			endLoading()
			close()
		},
	})
}
</script>

优势

  • 对于使用者来说简单,没有控制show的心理负担
  • 弹窗内容和其他业务代码分离,不会污染其他组件props和结构
  • 能够充分复用同一个弹窗,通过函数参数的方式来更新弹窗内容,在不同地方打开同一个弹窗

劣势

  • 对于一些简单需求的弹窗,用这种函数式的弹窗会有些臃肿
  • 在使用函数式弹窗前需要做一些操作(Provider和Inject),会在后面提到以及解决方案。

如何使用这种函数式的弹窗

原理

通过在根页面将我们的Modal组件挂载上去,然后通过使用hook去管理这个modal组件的状态值(props,ref),然后通过provide将props和event和我们的modal组件联系起来,再在我们需要使用弹窗的地方使用inject更新props来启动弹窗。

步骤1(❌):编写Modal

这里我使用的是Naive Ui的Modal组件,按喜好选择就行。按照你的需求编写弹窗的内容。定义好props和emits,写好他们的类型申明。

// TestModal.vue
<template>
  <n-modal
    v-model:show="isShowModal"
    preset="dialog"
    @after-leave="handleClose"
  >
    ...你的弹窗内容
  </n-modal>
</template>

<script setup lang="ts">
import { ref, reactive, computed, watch } from "vue"
import { useVModel } from "@vueuse/core"
import { FormRules } from "naive-ui"
interface ModalProps {
  show: boolean
  testValue: string
}

// 中间区域不要修改
const props = defineProps<ModalProps>()
const formRef = ref()
const loading = ref(false)
const isShowModal = useVModel(props, "show")
// 中间区域不要修改

const rules: FormRules = []

const formData = reactive({
  testValue: props.testValue,
})

const callBackData = computed(() => {
  return {
    formData,
  }
})

watch(
  () => props.show,
  () => {
    if (props.show) {
      formData.testValue = props.testValue
    } else {
      formData.testValue = ""
    }
  }
)

const emits = defineEmits<{
  (e: "update:show", value: boolean): void
  (e: "close", param: typeof callBackData.value): void
  (
    e: "confirm",
    param: typeof callBackData.value,
    close: () => void,
    endLoading: () => void
  ): void
  (e: "cancel", param: typeof callBackData.value): void
}>()
function handleCancel() {
  // 中间区域不要修改
  emits("cancel", callBackData.value)
  isShowModal.value = false
  // 中间区域不要修改
}

function handleClose() {
  // 中间区域不要修改
  emits("close", callBackData.value)
  isShowModal.value = false
  // 中间区域不要修改
}

function handleConfirm() {
  // 中间区域不要修改
  loading.value = true
  emits(
    "confirm",
    callBackData.value,
    () => {
      loading.value = false
      isShowModal.value = false
    },
    () => {
      loading.value = false
    }
  )
  // 中间区域不要修改
}
</script>

步骤2(❌):编写hook来管理弹窗的状态

在这个文件里面,使用hook管理 TestModal 弹窗需要的props和event事件,然后我们通过ts类型体操来获取TestModal的props类型和event类型,我们向外inject一个 open 函数,这个函数可以更新 TestModal 的props和event,同时打开弹窗,他的参数有完整的类型提示,可以让使用者更加明确的使用我们的弹窗。

// use-test-modal.ts
import {
  ref,
  provide,
  InjectionKey,
  inject,
  VNodeProps,
  AllowedComponentProps,
  reactive,
} from "vue";
import Modal from "./TestModal.vue";

/**
 * 通过引入弹窗组件来获取组件的除show,update:show以外的props和emits来作为open函数的
 */
type ModalInstance = InstanceType<
  typeof Modal extends abstract new (...args: any) => any ? typeof Modal : any
>["$props"];
type OpenParam = Omit<
  {
    readonly [K in keyof Omit<
      ModalInstance,
      keyof VNodeProps | keyof AllowedComponentProps
    >]: ModalInstance[K];
  },
  "show" | "onUpdate:show"
>;

interface AnyFileChangeModal {
  open: (param?: OpenParam) => Promise<void>;
}

/**
 * 通过弹窗实例来获取弹窗组件内需要哪些props
 */
type AllProps = Omit<
  OpenParam,
  "onClose" | "onCancel" | "onConfirm" | "onUpdate:show"
> & { show: boolean };
const anyModalKey: InjectionKey<AnyFileChangeModal> = Symbol("ModalKey");
export function provideTestModal() {
  const allProps: AllProps = reactive({
    show: false,
  } as AllProps);
  const closeCallback = ref();
  const cancelCallback = ref();
  const confirmCallback = ref();
  const handleUpdateShow = (value: boolean) => {
    allProps.show = value;
  };

  /**
   * @param param 通过函数来更新props
   */
  function updateAllProps(param: OpenParam) {
    const excludeKey = ["show", "onClose", "onConfirm", "onCancel"];
    for (const [key, value] of Object.entries(param)) {
      if (!excludeKey.includes(key)) {
        allProps[key] = value;
      }
    }
  }
  function clearAllProps() {
    for (const [key] of Object.entries(allProps)) {
      allProps[key] = undefined;
    }
  }

  async function open(param: OpenParam) {
    clearAllProps();
    updateAllProps(param);
    allProps.show = true;
    param.onClose && (closeCallback.value = param.onClose);
    param.onConfirm && (confirmCallback.value = param.onConfirm);
    param.onCancel && (cancelCallback.value = param.onCancel);
  }
  provide(anyModalKey, { open });
  return {
    allProps,
    closeCallback,
    confirmCallback,
    cancelCallback,
    handleUpdateShow,
  };
}

export function injectTestModal() {
  return inject(anyModalKey)
}
Ï

步骤3(❌):提供Provider

在这个文件里,我将TestModal放在了根页面,然后将hook返回的props和event绑定给TestModal

// ModalProvider.vue
<template>
  <slot />

  <TestModal
    v-bind="allTestModalProps"
    @update:show="handleTestModalUpdateShow"
    @close="closeTestModalCallback"
    @confirm="confirmTestModalCallback"
    @cancel="cancelTestModalCallback"
  />
  <!-- 新增Modal -->
</template>

<script setup lang="ts">
import TestModal from "./test-modal/TestModal.vue";
import { provideTestModal } from "./test-modal/use-test-modal";
/** 新增import */

const {
  allProps: allTestModalProps,
  handleUpdateShow: handleTestModalUpdateShow,
  closeCallback: closeTestModalCallback,
  confirmCallback: confirmTestModalCallback,
  cancelCallback: cancelTestModalCallback,
} = provideTestModal();
/** 新增provide */
</script>

步骤4(❌):通过函数打开弹窗

<template>
  <n-button @click="open">open</n-button>
</template>

<script setup lang="ts">
import { injectTestModal } from "./test-modal/use-test-modal"
const model = injectTestModal()
function open() {
  model?.open({
    testValue: "传给弹窗组件的值",
    onConfirm({ formData }, close, endLoading) {
      console.log({ ...formData })
      endLoading()
      close()
    },
  })
}
</script>

到这里也就结束了。如果这样写一个弹窗,确实可以达到函数式的打开弹窗,但是他实在是太繁琐了,要写这么一堆东西,不如直接修改弹窗的show来的快。如果看过上面的use-test-modal.ts的话,会发现里面有AllProps,这是为了减少方便使用脚本工具来写通用化的代码,可以大大减少人工的重复代码编写,接下来我们使用一个工具来减少这些繁琐的操作。

步骤1(✅):初始化Provider

通过使用工具生成根页面ModalProvder组件。 具体步骤:在你想放置当前业务版本的弹窗文件夹路径下使用终端执行脚本,选择initProvider

iShot_2023-08-15_15.16.14.gif

步骤2(✅):生成弹窗模板

通过继续使用脚本工具,生成弹窗组件以及hook文件。 具体步骤:在你想放该弹窗的文件夹路径下使用终端,使用脚本工具,选择genModal,然后跟着指令操作就行。比如例子中,我们生产名为test的弹窗,然后告诉脚本我们的ModalProvider组件的绝对路径,脚本帮我生产test-modal的文件夹,里面放着testModal.vueuse-test-modal.ts,这里的use-test-modal.ts文件已经是成品了,不需要你去修改,ModalProvider.vue也不需要你去修改,里面的路径关系也帮你处理好了。

iShot_2023-08-15_15.17.33.gif

步骤3(✅):修改弹窗内容

上一步操作中,脚本帮我写好了TestModal.vue组件,我们可以在他的基础上完善我们的业务需求。

步骤4(✅):调用弹窗

我们找到生成的use-test-modal.ts,里面有一个injectXXXModal的方法,我们在哪个地方用到弹窗就引用执行他,返回的对象中有open方法,通过open方法去开启弹窗并且更新弹窗props。

Demo

Demo地址 里面有完整的demo代码

iShot_2023-08-15_18.32.39.gif

脚本工具

仓库地址 这个是我写的脚本工具,用来减少一些重复代码的工作。用法可看readme

总结

本文先通过比较函数式的弹窗和直接传统编写弹窗的优劣,然后引出本文的主旨(如何编写函数式弹窗),再考虑我这一版函数式弹窗的问题,最后通过脚本工具来解决这一版函数式弹窗的劣势,完整的配合脚本工具,可以极大的加快我们的工作效率。 大家不要看这么写要好几个文件夹,实际使用的时候其实就是脚本生成代码后,我们只需要去修改弹窗主题的内容。有什么疑惑或者建议评论区聊。

全部评论

相关推荐

评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务