テクノロジーの共有 | ポップアップ ウィンドウ開発で el-dialog をカプセル化するためにフックを使用する方法

ポップアップ ウィンドウは、フロントエンド開発における一般的な要件です。 Element UI フレームワークのコンポーネントel-dialogは、ポップアップ ウィンドウに関連する基本的な機能を提供します。ただし、実際の開発では、プロジェクト内のスタイルと動作を均一に管理するためのポップアップ ウィンドウの二次カプセル化など、いくつかのカスタマイズ要件が必然的に発生します。

useDialogこの記事では、フックのカプセル化を使用してel-dialog、より柔軟で使いやすいポップアップ コンポーネントを実現する方法を紹介します。

1. 課題の明確化

「共通のコンポーネントを複数のページに適用する」というのは、非常に一般的な実際的なシナリオです。

例: アプリケーションの購入を例に挙げます。ユーザーは支払いページで購入することも、他のページを閲覧中に購入リクエストをトリガーすることもできます。この場合、ユーザーをガイドするダイアログ ボックスが表示される必要があります。購入行動を完了します。

この機能を実現するために、これまでは通常、次の手順が行われてきました。

  1. 購入コンポーネントをカプセル化する: まず、さまざまなページやシナリオで再利用できるように、一般的な購入コンポーネントを作成します。
  2. 支払いページに購入コンポーネントをレンダリングする: 購入コンポーネントを支払いページに直接埋め込みます。
  3. el-dialog他のページでの購入コンポーネントの表示を使用します。他のページでのel-dialogコンポーネントの表示を制御し、visible状態変数 (通常はref応答変数) を使用して、ダイアログ ボックスのポップアップと閉じるを動的に制御します。

この方法でも機能要件は満たせますが、コンポーネントを使用するページや機能が増えると、メンテナンスがより複雑で面倒になります。使用するページが増えるたびに、表示/非表示を制御するロジックを繰り返しコードで記述する必要があります。

では、このプロセスを簡素化するもっと良い方法はあるのでしょうか?別の関数を使用して、何らかの方法で購入コンポーネントの開閉をグローバルに制御し、それによってコードの重複とメンテナンスのコストを削減することは可能ですか?

2. useDialogフックについて

Vue では、フックを使用して Vue の機能を機能コンポーネントまたは API に「フック」できます。これらは通常、Vue によって提供される応答性が高く再利用可能なロジック関数のセットである、Composition API で使用されます。

この記事で説明するフックはuseDialog、コンポーネントの基本機能をカプセル化するカスタム フックでありel-dialog、プロジェクト内のポップアップ ウィンドウを管理および表示するための追加機能を提供することもできます。

3. useDialogフックを実装する

useDialogフックは次の目標を達成する必要があります。

  1. 基本的な使用法、渡される基本属性、およびデフォルトのスロット、エクスポート、および関数el-dialogによって表示されるコンテンツを満たします。openDialogcloseDialog
  2. サポートされてel-dialogいるイベント構成。
  3. デフォルトのslotコンポーネント属性構成をサポートします。
  4. などel-dialogの他のスロット構成をサポートしますheaderfooter
  5. コンテンツ コンポーネントで特定のイベントをスローすると、ダイアログを閉じることがサポートされます。
  6. サポートされている表示コンテンツはjsx普通文本Vue Component、です。
  7. 表示されたコンテンツを閉じることができるかどうかを制御するコールバック関数をサポートしますbeforeClose
  8. フックの前の表示をサポートします。たとえばonBeforeOpen
  9. 定義中およびポップアップ中の構成プロパティの変更をサポートします。
  10. root vue のプロトタイプの継承をサポートし、 ; などの関数を使用でき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 を含む基本的な使用法を実装します。

目標 1: 基本的な使用法を満たし、el-dialog基本的な属性とデフォルトのスロット表示コンテンツ、エクスポートopenDialogおよびcloseDialog関数を渡す。 目標 2:イベント構成をサポートする。目標
3 .: デフォルトのコンポーネントの属性構成をサポートする目標 6: 、 、 の表示コンテンツをサポートする目標11 :パラメータ プロンプトをサポートする。el-dialog
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. これは定義でサポートされておりonBeforeOpenbeforeCloseDialogコンポーネント呼び出し設定とともにデフォルトでコンテンツ コンポーネントに渡されます。
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: root 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 }
}

上記のパッケージを介してフックを使用した後useDialog、ウィンドウをポップアップする必要がある場合は、フックを導入してopenDialogメソッドを呼び出すだけで済み、非常に便利で簡潔です。さらに、このようなカプセル化により、後でポップアップ ウィンドウのロジックを変更useDialogする必要があり、1 つずつ編集する必要がなくなります。

4. UseDialog フックケースの練習

次に、useDialogHook を使用して、冒頭で述べたアプリケーション購入の問題を解決します。

(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 フックのその他のアプリケーション

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>

要約する

useDialogフックのカプセル化を使用すると、el-dialogフロントエンド テクノロジをより興味深く、簡潔にすることができます。また、作者は、フロントエンド コードをよりエレガントで保守しやすくするために、誰もがこの種のカプセル化方法を試してほしいと考えています。

優秀なエンジニアは優秀なシェフと同じで、あらゆる料理を美味しく仕上げる絶妙な調理技術と味付け技術を習得しています。


LigaAI は開発者文化の維持と構築を非常に重視しており、今後もより多くのテクノロジー共有と興味深いテクノロジー実践を共有していきます。

LigaAI アカウントのフォローへようこそ。新世代のインテリジェントな R&D コラボレーション プラットフォームをクリックして、私たちとさらに交流できることを楽しみにしています。

開発者の出発を支援するために、LigaAI は皆さんと一緒に旅をすることを楽しみにしています。

1990 年代生まれのプログラマーがビデオ移植ソフトウェアを開発し、1 年足らずで 700 万以上の利益を上げました。結末は非常に罰的でした。 高校生が成人式にオープンソースプログラミング言語を自作―ネチズンの鋭いコメント: 詐欺横行でRustDesk依存、国内サービスの タオバオ(taobao.com)は国内サービスを一時停止、ウェブ版の最適化作業を再開 Java最も一般的に使用されている Java LTS バージョンは 17 、Windows 11 は減少し続ける Open Source Daily | Google がオープンソースの Rabbit R1 を支持、Microsoft の不安と野心; Electricがオープンプラットフォームを閉鎖 AppleがM4チップをリリース GoogleがAndroidユニバーサルカーネル(ACK)を削除 RISC-Vアーキテクチャのサポート Yunfengがアリババを辞任し、将来的にはWindowsプラットフォームで独立したゲームを制作する予定
{{名前}}
{{名前}}

おすすめ

転載: my.oschina.net/u/5057806/blog/11091317