关于Vben中modal的封装代码浅析

3,354 阅读1分钟

Modal目录结构

image.png

components包含modal的内容组件,头部组件和底部组件等,

hooks中是方法具体实现的集合,

BasciModal.vue集成了components的组件文件,

props.ts为传入属性类型定义和初始化赋值,

typing.ts为弹窗对外暴露方法的声明和初始化。

BasciModal.vue 封装-template

<template>
  <Modal v-bind="getBindValue" @cancel="handleCancel">
    <template #closeIcon v-if="!$slots.closeIcon">
      <ModalClose
        :canFullscreen="getProps.canFullscreen"
        :fullScreen="fullScreenRef"
        @cancel="handleCancel"
        @fullscreen="handleFullScreen"
      />
    </template>

    <template #title v-if="!$slots.title">
      <ModalHeader
        :helpMessage="getProps.helpMessage"
        :title="getMergeProps.title"
        @dblclick="handleTitleDbClick"
      />
    </template>

    <template #footer v-if="!$slots.footer">
      <ModalFooter v-bind="getBindValue" @ok="handleOk" @cancel="handleCancel">
        <template #[item]="data" v-for="item in Object.keys($slots)">
          <slot :name="item" v-bind="data || {}"></slot>
        </template>
      </ModalFooter>
    </template>

    <ModalWrapper
      :useWrapper="getProps.useWrapper"
      :footerOffset="wrapperFooterOffset"
      :fullScreen="fullScreenRef"
      ref="modalWrapperRef"
      :loading="getProps.loading"
      :loading-tip="getProps.loadingTip"
      :minHeight="getProps.minHeight"
      :height="getWrapperHeight"
      :visible="visibleRef"
      :modalFooterHeight="footer !== undefined && !footer ? 0 : undefined"
      v-bind="omit(getProps.wrapperProps, 'visible', 'height', 'modalFooterHeight')"
      @ext-height="handleExtHeight"
      @height-change="handleHeightChange"
    >
      <slot></slot>
    </ModalWrapper>

    <template #[item]="data" v-for="item in Object.keys(omit($slots, 'default'))">
      <slot :name="item" v-bind="data || {}"></slot>
    </template>
  </Modal>
</template>

从 template可以看到,Modal 的 dom 结构,有遮罩层、标题组件、内容组件、和底部组件几部分。这几块都可以定义并接收对应 props进行不同的样式或行为配置。 看下内容组件modalWrapper这块:

    <ModalWrapper
      :useWrapper="getProps.useWrapper"
      :footerOffset="wrapperFooterOffset"
      :fullScreen="fullScreenRef"
      ref="modalWrapperRef"
      :loading="getProps.loading"
      :loading-tip="getProps.loadingTip"
      :minHeight="getProps.minHeight"
      :height="getWrapperHeight"
      :visible="visibleRef"
      :modalFooterHeight="footer !== undefined && !footer ? 0 : undefined"
      v-bind="omit(getProps.wrapperProps, 'visible', 'height', 'modalFooterHeight')"
      @ext-height="handleExtHeight"
      @height-change="handleHeightChange"
    >
      <slot></slot>
    </ModalWrapper>

该组件获取从父组件传递的参数后通过传统的调用组件方式再传回给子组件ModalWrapper

<template>
 <ScrollContainer ref="wrapperRef">
   <div ref="spinRef" :style="spinStyle" v-loading="loading" :loading-tip="loadingTip">
     <slot></slot>
   </div>
 </ScrollContainer>
</template>

通过slot可以自定义传入modal内容

组件API

属性api:

image.png

事件api:

image.png 在vben中扩展了拖拽,全屏,自适应高度等功能。 属性是根据组件之间的传值进行监听和设置的,方法也是通过组件通信进行传递的。 举例如下: 在封装的basicalModal.vue 时,已经写好了对应的「确定」「取消」事件:

image.png

      function handleOk(e: Event) {
        emit('ok', e);
      }
  // 取消事件
      as1ync function handleCancel(e: Event) {
        e?.stopPropagation();

        if (props.closeFunc && isFunction(props.closeFunc)) {
          const isClose: boolean = await props.closeFunc();
          visibleRef.value = !isClose;
          return;
        }

        visibleRef.value = false;
        emit('cancel', e);
      }

即在父组件中调用组件时进行监听,点击按钮后触发自定义取消/确实事件。

企业微信截图_16484496065291.png emit传回的Ok事件直接绑定handleSubmit进行后续的数据处理

modal组件的使用

由于弹窗内代码一般都是作为单文件组件挂载,故推荐将弹窗用单文件组件进行进一步封装 如:

// ModalExp.vue
<template>
  <BasicModal v-bind="$attrs" title="Modal Title" :helpMessage="['提示1', '提示2']">
    Modal Info.
  </BasicModal>
</template>
<script lang="ts">
  import { defineComponent } from 'vue';
  import { BasicModal } from '/@/components/Modal';
  export default defineComponent({
    components: { BasicModal },
    setup() {
      return {};
    },
  });
</script>

页面引用该ModalExp弹窗

// Page.vue
<template>
  <div class="px-10">
    <ModalExp @register="register" />
  </div>
</template>
<script lang="ts">
  import { defineComponent } from 'vue';
  import { useModal } from '/@/components/Modal';
  import Modal from './Modal.vue';
  export default defineComponent({
    components: { Modal },
    setup() {
      const [register, { openModal }] = useModal();
      return {
        register,
        openModal,
      };
    },
  });
</script>

注意:独立封装的组件需要将 attrs绑定到BasicModal组件。

解析useModal及其相关方法

调用组件必须通过外部调用useModal方法用于注册和调用事件操作组件。 image.png

const [register, { openModal, setModalProps }] = useModal();

register

register 用于注册 useModal,如果需要使用 useModal 提供的 api,必须将 register 传入组件的 onRegister进行注册。

  <ModalExp @register="register" />

实现这一步依赖于vue的子传父组件通信,通过emit('register', modalInstance)实现;

  const register = (modalInstance: ModalMethods, uuid: string) => 
    isProdMode() &&
      tryOnUnmounted(() => {
        modalInstanceRef.value = null;
      });
    uidRef.value = uuid;
    modalInstanceRef.value = modalInstance;
    currentInstance?.emit('register', modalInstance, uuid);
  };

openModal/closeModal

    openModal: <T = any>(visible = true, data?: T, openOnSet = true): void => {
      getInstance()?.setModalProps({
        visible: visible,
      });

      if (!data) return;
      const id = unref(uid);
      if (openOnSet) {
        dataTransfer[id] = null;
        dataTransfer[id] = toRaw(data);
        return;
      }
      const equal = isEqual(toRaw(dataTransfer[id]), toRaw(data));
      if (!equal) {
        dataTransfer[id] = toRaw(data);
      }
    },

该方法传入true或false打开/关闭弹窗

image.png

image.png

几乎所有api属性均是如此传值,通过setModalProps进行更新。 也可直接用colseModal关闭弹窗

// true/false: 打开关闭弹窗
// data: 传递到子组件的数据
openModal(true, data);
closeModal();