기술 공유 | 팝업 창 개발에서 Hook를 사용하여 el-Dialog를 캡슐화하는 방법은 무엇입니까?

팝업 창은 프런트 엔드 개발의 일반적인 요구 사항입니다. Element UI 프레임워크의 구성 요소 el-dialog는 팝업 창과 관련된 기본 기능을 제공하지만 실제 개발에서는 프로젝트의 스타일과 동작을 균일하게 관리하기 위한 팝업 창의 보조 캡슐화와 같은 몇 가지 맞춤화된 요구 사항에 필연적으로 직면하게 됩니다.

useDialog이 기사에서는 후크 캡슐화를 사용하여 el-dialog보다 유연하고 사용하기 쉬운 팝업 구성 요소를 만드는 방법을 공유합니다 .

1. 문제의 명확화

"여러 페이지에 공통 구성 요소 적용"은 매우 일반적인 실제 시나리오입니다.

예: 애플리케이션 구매를 예로 들어 보겠습니다. 사용자는 결제 페이지에서 구매할 수도 있고, 다른 페이지를 검색하는 동안 구매 요청을 실행할 수도 있습니다. 이 경우 사용자를 안내하는 대화 상자가 팝업되어야 합니다. 구매 행동을 완료합니다.

이 기능을 달성하기 위해 과거에는 일반적으로 다음 단계를 수행했습니다.

  1. 구매 구성 요소 캡슐화 : 먼저 다양한 페이지와 시나리오에서 재사용할 수 있도록 일반 구매 구성 요소를 만듭니다.
  2. 결제 페이지에서 구매 구성요소 렌더링 : 구매 구성요소를 결제 페이지에 직접 삽입합니다.
  3. el-dialog다른 페이지에서 구매 표시 구성 요소를 사용합니다 . 다른 페이지에서 el-dialog구성 요소의 표시를 제어하고 visible상태 변수(일반적으로 ref반응형 변수)를 사용하여 대화 상자의 팝업 및 닫기를 동적으로 제어합니다.

이 방법은 기능적 요구 사항을 충족할 수 있지만 구성 요소가 점점 더 많은 페이지와 기능에서 사용됨에 따라 유지 관리가 더욱 복잡하고 번거로워집니다. 추가 페이지를 사용할 때마다 표시/숨기기를 제어하는 ​​논리를 반복적으로 코드로 작성해야 합니다.

그렇다면 이 프로세스를 단순화하는 더 좋은 방법이 있습니까? 별도의 기능을 사용하여 구매 구성 요소의 열기 및 닫기를 어떤 방식으로든 전역적으로 제어함으로써 코드 복제 및 유지 관리 비용을 줄일 수 있습니까?

2. useDialog Hook 정보

Vue에서 Hooks를 사용하면 Vue 기능을 기능적 구성 요소나 API에 "연결"할 수 있습니다. 이는 일반적으로 Vue에서 제공하는 반응형 및 재사용 가능한 논리 함수 세트인 Composition API에서 사용됩니다.

이 기사에서 언급된 Hook은 구성 요소의 기본 기능을 useDialog캡슐화하는 사용자 정의 Hook el-dialog이며 프로젝트에서 팝업 창을 관리하고 표시하는 추가 기능을 제공할 수도 있습니다.

3. useDialog Hook 구현

useDialogHook은 다음 목표를 달성해야 합니다.

  1. 기본 사용법, el-dialog전달된 기본 속성, 기본 슬롯, 내보내기 openDialogcloseDialog기능으로 표시되는 콘텐츠를 만나보세요.
  2. 지원되는 el-dialog이벤트 구성;
  3. 기본 slot구성요소 속성 구성을 지원합니다.
  4. 등과 el-dialog같은 다른 슬롯 구성을 지원합니다 .headerfooter
  5. 콘텐츠 구성 요소에 특정 이벤트를 발생시키면 대화 상자 닫기가 지원됩니다.
  6. 지원되는 표시 내용은 jsx, 普通文本, Vue Component;
  7. 표시된 콘텐츠를 닫을 수 있는지 여부를 제어하는 ​​콜백 함수를 지원합니다 beforeClose.
  8. 예를 들어 후크 이전 표시를 지원합니다 onBeforeOpen.
  9. 정의 및 팝업 중에 구성 속성 수정을 지원합니다.
  10. vue-i18n루트 뷰의 프로토타입 상속을 지원하며 다음과 같은 $t함수를 사용할 수 있습니다 .
  11. 지원 ts매개변수 프롬프트;

(1) useDialog.ts파일 구현 유형 정의 준비

import type { Ref } from 'vue'
import { h, render } from 'vue'
import { ElDialog } from 'element-plus'
import type {
  ComponentInternalInstance,
} from '@vue/runtime-core'

type Content = Parameters<typeof h>[0] | string | JSX.Element
// 使用 InstanceType 获取 ElDialog 组件实例的类型
type ElDialogInstance = InstanceType<typeof ElDialog>

// 从组件实例中提取 Props 类型
type DialogProps = ElDialogInstance['$props'] & {
}
interface ElDialogSlots {
  header?: (...args: any[]) => Content
  footer?: (...args: any[]) => Content
}
interface Options<P> {
  dialogProps?: DialogProps
  dialogSlots?: ElDialogSlots
  contentProps?: P
}

(2) 일반적인 useDialog기능 구현

다음 함수는 목표 1, 2, 3, 4, 6, 11을 포함한 기본 사용법을 구현합니다.

el-dialog목표 1: 기본 사용법 충족, 기본 속성 및 기본 슬롯 표시 콘텐츠 전달 , 내보내기 openDialogcloseDialog기능
목표 2: 이벤트 el-dialog구성 지원 목표 4: 기타 슬롯 구성 지원 등; 목표 6: , , 의 표시 내용 지원 목표 11: 매개변수 프롬프트 지원;
slot
el-dialogheaderfooter
jsx普通文本Vue Component
ts

export function useDialog<P = any>(content: Content, options?: Ref<Options<P>> | Options<P>) {
  let dialogInstance: ComponentInternalInstance | null = null
  let fragment: Element | null = null

  // 关闭并卸载组件
  const closeAfter = () => {
    if (fragment) {
      render(null, fragment as unknown as Element) // 卸载组件
      fragment.textContent = '' // 清空文档片段
      fragment = null
    }
    dialogInstance = null
  }
  function closeDialog() {
    if (dialogInstance)
      dialogInstance.props.modelValue = false
  }

  // 创建并挂载组件
  function openDialog() {
    if (dialogInstance) {
      closeDialog()
      closeAfter()
    }
    
    const { dialogProps, contentProps } = options
    fragment = document.createDocumentFragment() as unknown as Element

    const vNode = h(ElDialog, {
      ...dialogProps,
      modelValue: true,
      onClosed: () => {
        dialogProps?.onClosed?.()
        closeAfter()
      },
    }, {
      default: () => [typeof content === 'string'
        ? content
        : h(content as any, {
          ...contentProps,
        })],
      ...options.dialogSlots,
    })
    render(vNode, fragment)
    dialogInstance = vNode.component
    document.body.appendChild(fragment)
  }

  onUnmounted(() => {
    closeDialog()
  })

  return { openDialog, closeDialog }
}

(3) 목표 5 달성

목표 5: 대화 상자 닫기를 지원하기 위해 콘텐츠 구성 요소에 특정 이벤트를 발생시킵니다.

  1. 정의에서 지원됨 closeEventName;
interface Options<P> {
  // ...
  closeEventName?: string // 新增的属性
}
  1. 대화 상자를 닫는 이벤트를 useDialog수신하도록 함수를 수정합니다 .closeEventName
export function useDialog<P = any>(content: Content, options?: Ref<Options<P>> | Options<P>) {
  // ...
  // 创建并挂载组件
  function openDialog() {
    // ...
    fragment = document.createDocumentFragment() as unknown as Element
    // 转换closeEventName事件
    const closeEventName = `on${upperFirst(_options?.closeEventName || 'closeDialog')}`

    const vNode = h(ElDialog, {
      // ...
    }, {
      default: () => [typeof content === 'string'
        ? content
        : h(content as any, {
          ...contentProps,
          [closeEventName]: closeDialog, // 监听自定义关闭事件,并执行关闭
        })],
      ...options.dialogSlots,
    })
    render(vNode, fragment)
    dialogInstance = vNode.component
    document.body.appendChild(fragment)
  }

  onUnmounted(() => {
    closeDialog()
  })

  return { openDialog, closeDialog }
}

(4) 목표 7, 8 달성

목표 7: 표시된 콘텐츠에서 닫힐 수 있는지 여부를 제어하는 ​​콜백 기능을 지원합니다. beforeClose
를 들어 목표 8: 표시하기 전에 후크를 지원합니다 onBeforeOpen.

  1. 이는 정의에서 지원되며 onBeforeOpen기본적 beforeCloseDialog으로 구성 요소 호출 설정과 함께 콘텐츠 구성 요소에 전달됩니다.
type DialogProps = ElDialogInstance['$props'] & {
  onBeforeOpen?: () => boolean | void
}
  1. 이벤트를 수신하고 전달 useDialog하도록 함수를 수정합니다 .onBeforeOpenbeforeCloseDialog
export function useDialog<P = any>(content: Content, options?: Ref<Options<P>> | Options<P>) {
  // ...
  // 创建并挂载组件
  function openDialog() {
    // ...
    const { dialogProps, contentProps } = options
    // 调用before钩子,如果为false则不打开
    if (dialogProps?.onBeforeOpen?.() === false) {
      return
    }
    // ...
    // 定义当前块关闭前钩子变量
    let onBeforeClose: (() => Promise<boolean | void> | boolean | void) | null

    const vNode = h(ElDialog, {
      // ...
      beforeClose: async (done) => {
        // 配置`el-dialog`的关闭回调钩子函数
        const result = await onBeforeClose?.()
        if (result === false) {
          return
        }
        done()
      },
      onClosed: () => {
        dialogProps?.onClosed?.()
        closeAfter()
        // 关闭后回收当前变量
        onBeforeClose = null
      },
    }, {
      default: () => [typeof content === 'string'
        ? content
        : h(content as any, {
          // ...
          beforeCloseDialog: (fn: (() => boolean | void)) => {
            // 把`beforeCloseDialog`传递给`content`,当组件内部使用`props.beforeCloseDialog(fn)`时,会把fn传递给`onBeforeClose`
            onBeforeClose = fn
          },
        })],
      ...options.dialogSlots,
    })
    render(vNode, fragment)
    dialogInstance = vNode.component
    document.body.appendChild(fragment)
  }

  onUnmounted(() => {
    closeDialog()
  })

  return { openDialog, closeDialog }
}

(5) 목표 9, 10 달성

목표 9: 정의 및 팝업 시 구성 속성 수정 지원 목표 10: 루트 vue의 프로토타입 상속 지원, 다음과 같은 기능을
사용할 수 있습니다 .vue-i18n$t

// 定义工具函数,获取计算属性的option
function getOptions<P>(options?: Ref<Options<P>> | Options<P>) {
  if (!options)
    return {}
  return isRef(options) ? options.value : options
}

export function useDialog<P = any>(content: Content, options?: Ref<Options<P>> | Options<P>) {
  // ...
  // 获取当前组件实例,用于设置当前dialog的上下文,继承prototype
  const instance = getCurrentInstance()
  // 创建并挂载组件,新增`modifyOptions`参数
  function openDialog(modifyOptions?: Partial<Options<P>>) {
    // ...
    const _options = getOptions(options)
    // 如果有修改,则合并options。替换之前的options变量为 _options
    if (modifyOptions)
      merge(_options, modifyOptions)
    
    // ...

    const vNode = h(ElDialog, {
      // ...
    }, {
      // ...
    })
    // 设置当前的上下文为使用者的上下文
    vNode.appContext = instance?.appContext || null
    render(vNode, fragment)
    dialogInstance = vNode.component
    document.body.appendChild(fragment)
  }

  onUnmounted(() => {
    closeDialog()
  })

  return { openDialog, closeDialog }
}

위 패키지를 통해 Hook을 사용한 후 useDialog, 윈도우를 띄워야 할 때 Hook을 도입하고 openDialog메소드를 호출하기만 하면 되는데, 매우 편리하고 간결합니다. 또한 이러한 캡슐화를 사용하면 나중에 팝업 창 로직을 수정하는 것이 더 편리해집니다. useDialog하나하나 편집할 필요 없이 Hook에서만 수정하면 됩니다.

4. UseDialog Hook 사례 실습

useDialog다음으로, 앞서 언급한 애플리케이션 구매 문제를 해결하기 위해 Hook을 사용합니다 .

(1) components/buy.vue구매 컴포넌트 생성

<script lang="ts" setup>
  const props = defineProps({
    from: {
      type: String,
      default: '',
    },
  })
</script>
<template>
  我是购买组件
</template>

(2) 페이지 내 구매 컴포넌트를 pages/subscription.vue이용하세요buy.vue

<script lang="ts" setup>
  import Buy from '@/components/buy.vue'
</script>
<template>

  <Buy from="subscription" />

</template>

(3) 기타 기능 페이지의 팝업 buy.vue구매 구성요소

<script lang="ts" setup>
  import { useDialog } from '@/hooks/useDialog'
  const Buy = defineAsyncComponent(() => import('@/components/buy.vue'))

  const { openDialog } = useDialog(Buy, {
    dialogProps: {
      // ...
      title: '购买'
    },
    contentProps: {
      from: 'function',
    },
  })
  
  const onSomeClick = () => {
    openDialog()
  }
</script>

확장: useDialog Hook의 다른 응용 프로그램

beforeClose& closeEventName예: buy.vue구성품 구매

<script lang="ts" setup>
  const props = defineProps({
    from: {
      type: String,
      default: '',
    },
    beforeCloseDialog: {
      type: Function,
      default: () => true,
    },
  })
  
  const emit = defineEmits(['closeDialog'])

  props.beforeCloseDialog(() => {
    // 假如from 为 空字符串不能关闭
    if (!props.from) {
      return false
    }
    return true
  })
  
  // 关闭dialog
  const onBuySuccess = () => emit('closeDialog')
</script>
<script lang="ts" setup>
  import { useDialog } from '@/hooks/useDialog'
  const Buy = defineAsyncComponent(() => import('@/components/buy.vue'))

  const { openDialog } = useDialog(Buy, {
    dialogProps: {
      // ...
      title: '购买'
    },
    contentProps: {
      from: '',
    },
  })
  
  const onSomeClick = () => {
    openDialog()
  }
</script>

요약하다

useDialogHook 캡슐화를 사용하면 el-dialog프런트엔드 기술을 더욱 흥미롭고 간결하게 만들 수 있습니다. 저자는 또한 모든 사람이 이러한 종류의 캡슐화 방법을 시도하여 프런트 엔드 코드를 더욱 우아하고 유지 관리하기 쉽게 만들 수 있기를 바랍니다.

훌륭한 엔지니어는 훌륭한 요리사와 같으며 절묘한 요리와 양념 기술을 마스터하여 모든 요리를 맛있게 만듭니다!


LigaAI는 개발자 문화의 유지와 구축을 매우 중요하게 생각하며 앞으로도 더 많은 기술 공유와 흥미로운 기술 사례를 공유할 것입니다.

LigaAI 계정 팔로우를 환영합니다. 차세대 지능형 R&D 협업 플랫폼을 클릭하여 우리와 더 많은 교류를 하길 기대합니다.

개발자의 항해를 돕기 위해 LigaAI는 여러분과 함께 여행하기를 기대합니다!

1990년대에 태어난 프로그래머가 비디오 포팅 소프트웨어를 개발하여 1년도 안 되어 700만 개 이상의 수익을 올렸습니다. 결말은 매우 처참했습니다! 고등학생들이 성인식으로 자신만의 오픈소스 프로그래밍 언어 만든다 - 네티즌 날카로운 지적: 만연한 사기로 러스트데스크 의존, 가사 서비스 타오바오(taobao.com)가 가사 서비스를 중단하고 웹 버전 최적화 작업 재개 자바 17은 가장 일반적으로 사용되는 Java LTS 버전입니다. Windows 10 시장 점유율 70%에 도달, Windows 11은 계속해서 Open Source Daily를 지원합니다. Google은 Docker가 지원하는 오픈 소스 Rabbit R1을 지원합니다. Electric, 개방형 플랫폼 종료 Apple, M4 칩 출시 Google, Android 범용 커널(ACK) 삭제 RISC-V 아키텍처 지원 Yunfeng은 Alibaba에서 사임하고 향후 Windows 플랫폼에서 독립 게임을 제작할 계획
{{o.이름}}
{{이름}}

추천

출처my.oschina.net/u/5057806/blog/11091317