求救!前端弹窗组件,到底该怎么用?

想象如下场景:

父组件里有三个弹窗,弹窗中是一个form+table的页面。

<template>
  <div>
    <a-modal>
      <a-form></a-form>
      <a-table></a-table>
    </a-modal>

    <a-modal>
      <a-form></a-form>
      <a-table></a-table>
    </a-modal>

    <a-modal>
      <a-form></a-form>
      <a-table></a-table>
    </a-modal>
  </div>
</template>

那父组件就需要三个变量控制弹窗开关。

const show1 = ref(false)
const show2 = ref(false)
const show3 = ref(false)

弹窗打开时需要调接口获取数据,弹窗关闭时需要调接口保存。

const data1 = {}
const data2 = {}
const data3 = {}

const onOk1 = () => {}
const onOk2 = () => {}
const onOk3 = () => {}

const onCancel1 = () => {}
const onCancel2 = () => {}
const onCancel3 = () => {}

这样写肯定不行。父组件里状态、方法、dom太多

把弹窗里的内容变成组件?

    <a-modal>
      <Comp1 />
    </a-modal>

    <a-modal>
      <Comp2 />
    </a-modal>
    <a-modal>
      <Comp3 />
    </a-modal>

这样一来,如果a-modal在初始状态是关闭的,就不会加载其内部的组件的代码,父组件的加载就会变快。起到了异步组件的效果

但是,弹窗onOk、onCancel的时候,还要获取其内部的组件的数据,调接口。(或者直接调组件内部expose的方法)

这样父组件里就有三个开关状态,一堆弹窗动作与组件的交互,还是很乱!

如果把modal和内部dom一起封装成一个组件

    <!-- 子组件HTML -->
    <a-modal>
      <a-form></a-form>
      <a-table></a-table>
    </a-modal>

// JS 部分
  defineExpose({
    // 暴露一个方法用来打开弹窗
    showModal,
  })

在父组件中:

    <ModalComp1
      ref="ModalCompRef"
    />

const openModal = () => {
    ModalCompRef.value?.showModal()
  }

这样很好,父组件变干净了,既不用维护控制弹窗开关的变量,也不用写弹窗开启和关闭时调接口的逻辑
只需要打开弹窗时ref调showModal方法,弹窗关闭时子组件emit事件中处理逻辑

但是父组件里就会有一个硕大的组件树,每个弹窗组件的代码都会和父组件一起加载!

我们可以把这整个组件变成一个异步组件,并加一个布尔值来控制组件初次打开

  <div>
    <ModalComp1
      v-model="isShow"
      v-if="isLoaded"
    />
  </div>

  const ModalComp1 = defineAsyncComponent(() => import('./ModalComp1.vue'))

  // 用isLoaded控制父组件刚打开时不需要加载子组件
  const isLoaded = ref(false)

  const isShow = ref(false)

  const openModal = () => {
    // 初次打开弹窗时,isLoaded变为true,开始加载子组件的代码
    if (!isLoaded.value) {
      isLoaded.value = true
    }
    isShow.value = true
  }

父组件加载时由于isLoaded是false,弹窗组件不会加载。
调openModal后,组件开始加载,加载完成后,由于isShow是true,弹窗会展示。
(为什么这里不用 组件ref.showModal 来打开?因为isLoaded变为true后,弹窗组件开始异步加载,不能确切的知道何时加载成功,此时无法用ref调用组件方法)

但是这样写,父组件里,每个弹窗都需要两个布尔值

其实isLoaded不是必须的,我们只需要用一个watch,在isShow初次变成true的时候,把isLoaded变成true就可以了。

加入一个hooks。

export default function useModalControl() {
  const open = ref(false)
  const loaded = ref(false)

  const stopWatch = watch(open, (newVal, oldVal) => {
    if (!oldVal && newVal) {
      loaded.value = true
      stopWatch()
    }
  })

  return [open, readonly(loaded)] as const
}

在父组件中:

  <div>
    <ModalComp1
      v-model="isShow"
      v-if="isLoaded"
    />
  </div>

  const ModalComp1 = defineAsyncComponent(() => import('./ModalComp1.vue'))

  import useModalControl from '@/composables/useModalControl'

  const [isShow, isLoaded] = useModalControl()

  const openModal = () => {
    isShow.value = true
  }

这下好了,既有了异步组件的能力,父组件也只需要维护一个isShow,但是这样又引入了一个hooks,组件上也多了一个v-if, 给后面看代码的人造成了疑惑

所以到底要怎样使用弹窗组件,才能做到:
1,弹窗内的数据和dom延迟加载
2,尽量减少父组件里的状态
3,弹窗内的数据可以直接emit到父组件

阅读 499
3 个回答

我自己倾向于,把状态和组件关联起来,决定 Modal 开关的状态,存在 Modal 组件内,通过作用域插槽来开关或者 defineExpose 开关都可以。
然后是 showModal ,你是打算用 dialog 标签吗?dialog 标签初始就是加载出来的,我有一个方法就是,用 v-if 来控制 dialog 的加载,使用 Transition 组件的钩子来触发 showModal。比如说状态是 isOpen

isOpen -> true -> dialog 渲染 -> 触发 onEnter 钩子 -> 执行 showModal()
isOpen -> false -> dialog 移出 -> 触发 afterLeave 钩子 -> 执行 close()

同时还要在 cancel 事件里令 isOpen.value = false 就可以达到你说的效果了。

补充:

<template>
  <!-- 换成作用域插槽 -->
  <!-- <slot name='...' :='{ open: ..., close: ... }'></slot> -->
  <button @click='isOpen = true'>OPEN</button>

  <Transition @enter='el => el.showModal()' @afterLeave='el => el.close()'>
    <dialog v-if='isOpen' @cancel='isOpen = false'>
       <!-- 换成插槽 -->
       <button @click='isOpen = false'>CLOSE</button>
    </dialog>
  </Transition>
</template>

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

const isOpen = ref(false)

/* 
 * 也可以这里暴露方法修改 isOpen
 * defineExpose({ open: ...,close: ... })
 */
</script>

大体是这样子的,如果用的是 showModal 可以用 ESC 关闭,然后在加上自己想要的样式就行了。

先封装子组件
ModalComp1.vue:

<template>
  <a-modal v-model:visible="visible" @ok="onOk" @cancel="onCancel">
    <a-form></a-form>
    <a-table></a-table>
  </a-modal>
</template>

<script setup>
import { ref, defineExpose } from 'vue';

const visible = ref(false);
const onOk = () => {
  console.log('Form submitted');
};
const onCancel = () => {
  console.log('Modal canceled');
};

defineExpose({
  showModal: () => {
    visible.value = true;
  },
  hideModal: () => {
    visible.value = false;
  }
});
</script>

父组件里面用:

<template>
  <div>
    <ModalComp1
      v-model="isShow"
      v-if="isLoaded"
      ref="modal1"
    />
    <a-button @click="openModal">打开弹窗</a-button>
  </div>
</template>

<script setup>
import { ref, defineAsyncComponent, watchEffect } from 'vue';
import useModalControl from '@/composables/useModalControl';

const ModalComp1 = defineAsyncComponent(() => import('./ModalComp1.vue'));

const [isShow, isLoaded] = useModalControl();

const openModal = () => {
  isShow.value = true;
};
</script>

再自定义一个hook:useModalControl hook

import { ref, watchEffect } from 'vue';

export default function useModalControl() {
  const open = ref(false);
  const loaded = ref(false);

  watchEffect(() => {
    if (open.value) {
      loaded.value = true;  // 弹窗打开时开始加载组件
    }
  });

  return [open, loaded];
}

最后:

!-- 子组件 ModalComp1.vue -->
<template>
  <a-modal v-model:visible="visible" @ok="onOk" @cancel="onCancel">
    <a-form :model="formData"></a-form>
    <a-table :dataSource="tableData"></a-table>
  </a-modal>
</template>

<script setup>
import { ref, defineExpose, defineProps } from 'vue';

const visible = ref(false);
const formData = ref({}); 
const tableData = ref([]);  

const onOk = () => {
  console.log('Form submitted');
  emit('update-data', formData.value, tableData.value);
};

const onCancel = () => {
  console.log('Modal canceled');
  emit('cancel');
};

defineExpose({
  showModal: () => {
    visible.value = true;
  },
  hideModal: () => {
    visible.value = false;
  }
});
</script>

在 Vue.js 项目中实现弹窗内的数据和 DOM 延迟加载、减少父组件里的状态以及弹窗内的数据直接 emit 到父组件的功能。

1. 弹窗内的数据和 DOM 延迟加载

可以使用 v-if 指令来控制弹窗的显示,从而实现延迟加载:

<template>
  <div>
    <button @click="showModal = true">打开弹窗</button>
    <Modal v-if="showModal" @close="showModal = false" />
  </div>
</template>

<script>
import Modal from './Modal.vue';

export default {
  data() {
    return {
      showModal: false,
    };
  },
};
</script>

2. 尽量减少父组件里的状态

可以将弹窗的状态和逻辑尽量放在弹窗组件内部,只在必要时通过事件与父组件通信:

<template>
  <div>
    <button @click="openModal">打开弹窗</button>
    <Modal v-if="isModalOpen" @close="isModalOpen = false" />
  </div>
</template>

<script>
import Modal from './Modal.vue';

export default {
  data() {
    return {
      isModalOpen: false,
    };
  },
  methods: {
    openModal() {
      this.isModalOpen = true;
    },
  },
};
</script>

3. 弹窗内的数据可以直接 emit 到父组件

可以在弹窗组件内使用 $emit 方法将数据传递给父组件:

ParentComponent.vue

<template>
  <div>
    <button @click="showModal = true">打开弹窗</button>
    <Modal v-if="showModal" @close="showModal = false" @submit="handleSubmit" />
  </div>
</template>

<script>
import Modal from './Modal.vue';

export default {
  data() {
    return {
      showModal: false,
    };
  },
  methods: {
    handleSubmit(data) {
      console.log('Received data from modal:', data);
      this.showModal = false;
    },
  },
};
</script>

Modal.vue

<template>
  <div class="modal">
    <form @submit.prevent="submitForm">
      <input v-model="formData" placeholder="Enter some data" />
      <button type="submit">Submit</button>
      <button type="button" @click="$emit('close')">Close</button>
    </form>
  </div>
</template>

<script>
export default {
  data() {
    return {
      formData: '',
    };
  },
  methods: {
    submitForm() {
      this.$emit('submit', this.formData);
    },
  },
};
</script>
推荐问题
宣传栏