通用回调命令式 Vue|React 模态框解决方案(Unified Overlays)

486 阅读5分钟

在前几篇文章中,介绍了在 Vue 中使用 render 来定义模态框、弹出层的方法,以及使用 unoverlay-vue(已废弃) 来简化操作,但实际上,Vue 和 React 中的模态框非常类似,为了在各种框架中更好的开发体验,我创建了 unoverlays

以下是我创建这个项目的初衷:

在如今日益繁琐的业务场景中,我们经常被重复的 Model 类定义工作所困扰,这意味着一旦遇到了 Model 类组件,我们需要不断重复定义 cancelconfirmvisible 等通用字段, 在当保存组件状态时,经常的需要对 Model 流程进行控制(open model -> edit data -> @confirm -> save data -> clear data) 这极大的加剧了工作量,并在组件重复使用时产生大量的冗余代码。

Unoverlays 是构建弹出层的统一插件,创建回调(命令式)方法、以及在 Vue Template 或 React Jsx 中(声明式)使用,并具有以下特点。

  • 制作类似于 element-plus/antd... 的 Message 或 Dialog
  • 同时支持回调(命令式)与Template/JSX(声明式)
  • 使用现有组件库(如 element-plus、antd)集成和定制化功能
  • 支持组件继承全局应用上下文
  • Unoverlays 支持 vue2|3react 等前端渐进式框架。
  • 更稳定!单元测试覆盖率 99.54% (Vue)

这篇文章主要以 Vue 为主,使用 react 的小伙伴可以在 @unoverlays/react 文档中查看使用方法。

Devtools

由 Unified Overlays 创建的组件,均支持对应框架的 Devtools(React、Vue)

Supported
React Developer ToolsVue.js Devtools
✅(holder)✅(holder、child-app)
  • holder 在对应的组件中插入持有者,使其在虚拟 DOM 当中。
  • child-app 创建独立的应用,由 devtools 识别新应用。

超快速开始

使用 unoverlays 提供的 useOverlayMeta Hook 创建弹出层组件(Vue、React) ts

import { useOverlayMeta } from '@unoverlays/vue'
// or
import { useOverlayMeta } from '@unoverlays/react'
// 在你的 Vue、React 弹出层组件中,使用 useOverlayMeta 获取弹出层元信息
const { visible, resolve, reject } = useOverlayMeta({
  // 弹出层动画的持续时间, 可以避免组件过早被销毁
  animation: 1000
})

// 成功并关闭弹出层
resolve('ok')
// 失败并关闭弹出层
reject('nook')
// 用于控制显示隐藏
visible

使用 createOverlay|renderOverlay 转换为命令式回调(callback)

const callback = createOverlay(OverlayComponent)
const result = await callback(props)

const result = renderOverlay(OverlayComponent, {
  props: { msg: 'ok' }
})

定制化(使用 element-plus dialog)

@unoverlays/vue 还可以使用第三方的组件库进行二次封装,下面是一个使用 element-plus(dialog)的例子。

element-plus(dialog)为例(其他组件库同理)

<!-- overlay.vue -->
<script setup>
import { defineEmits, defineProps } from 'vue-demi'
import { useOverlayMeta } from '@unoverlays/vue'
const props = defineProps({
  title: String,
})

const { visible, resolve, reject } = useOverlayMeta({
  animation: 1000
})
</script>

<template>
  <el-dialog v-model="visible" :title="title" @close="reject()">
    <!-- 你的定制化内容 -->
    <button @click="resolve(`${title}:confirmed`)" />
  </el-dialog>
</template>
import { createOverlay } from '@unoverlays/vue'
import OverlayComponent from './overlay.vue'

const callback = createOverlay(OverlayComponent)
const value = await callback({ title: 'myElDialog' })
// value === "myElDialog:confirmed"

组件调试

在使用 createOverlay 时会创建一个 Vue 子应用,这个应用区别与主应用,可在 Vue devtools 中查看与调试组件。

你也可以使用 useInjectHolder 在组件内部创建弹出层,并继承应用的当前上下文。

image.png

image.png

生成根元素的 ID 与应用名称使用同一 ID,在创建组件时,会自动对 ID 进行 PascalCase 处理。

可以通过渲染选项,更改生成 ID 名称。

const callback = createOverlay(OverlayComponent)
callback({}, {
  id: 'custom-overlay',
  // 关闭 ID 后续自增长
  autoIncrement: false
})

image.png

image.png

继承上下文

如果你全局注册了 Unoverlays,它会自动继承你的应用上下文,你也可以通过更细致的控制来传入上下文。

import { getCurrentInstance } from 'vue-demi'
import Component from './overlay.vue'

// 在你的 setup 中
const { appContext } = getCurrentInstance()!
renderOverlay(Component, {
  props: {},
  appContext
})

外部控制

如果把控制权都交给 Component,在一些使用场景时会受到到限制,Unoverlays 转换的组件允许用户在外部控制组件的流程

Model 的返回值的功能不仅仅包括 Promise 在此基础还有 confirm 和 cancel

const Model = createOverlay(MyComponent)
const instance = Model({/* you props */})

function close() {
  instance.cancel('no')
  instance.catch((value) => {
    // log: no
    console.log(value)
  })
}
function yes() {
  instance.confirm('yes')
  instance.then((value) => {
    // log: yes
    console.log(value)
  })
}

模版支持

使用 @unoverlays/vue 创建的组件,除了支持使用命令式(Imperative)方法调用外,还支持在 <template> 中使用。

支持了 <template> 中使用的组件,同样也支持使用 callback 调用,并不会影响彼此功能,这是一项可选项。

步骤.1: Define Component #

在 <template> 中使用,需要显式定义 modal 与 event

<!-- overlay.vue -->
<script setup>
import { defineEmits, defineProps } from 'vue-demi'
import { useOverlayMeta } from '@unoverlays/vue'
const props = defineProps({
  title: String,
  // 在 Template 中使用,需要定义 v-modal 所使用的字段(默认对应 visible)
  visible: Boolean
})

// 定义组件中使用的事件类型(默认:cancel、confirm)
defineEmits(['cancel', 'confirm'])

const { visible, resolve, reject } = useOverlayMeta({
  // 如果使用 template 渲染,animation 则可以不需要定义
  // animation: 1000,
})
</script>

如果您想替换为其他的字段与事件名,可以通过 useOverlayMeta 传入对应的配置实现。

<!-- overlay.vue -->
<script setup>
import { defineEmits, defineProps } from 'vue-demi'
import { useOverlayMeta } from '@unoverlays/vue'
const props = defineProps({
  title: String,
  modalValue: Boolean
})

defineEmits(['nook', 'ok'])

const { visible, resolve, reject } = useOverlayMeta({
  event: { confirm: 'ok', cancel: 'nook' },
  modal: 'modalValue',
})
</script>

<template>
  ...
</template>

步骤.2: In Template #

定义 modal 与 event 后,即可在 <template> 中使用弹出层组件。

<!-- overlay.vue -->
<script setup>
import OverlayComponent from './overlay.vue'
const visible = ref(false)

const confirm = () => {
  // ...
}
const cancel = () => {
  // ...
}
</script>

<template>
  <OverlayComponent
    v-model:visible="visible"
    @resolve="confirm"
    @reject="cancel"
  />
</template>

template-promise

vue-template-promise 是 Anthony Fu 创建的一个项目,它一样可以解决在 template 中使用模态框打断编程流程的问题,它可以很好的与 @unoverlays/vue 配合使用。

<script setup lang="ts">
import OverlayComponent from './overlay.vue'
const TemplatePromise = useTemplatePromise<ReturnType>({
  transition: {
    name: 'fade',
    appear: true,
  },
})
</script>

<template>
  <TemplatePromise v-slot="{ resolve, reject }">
    <overlay-component visible @confirm="resolve" @cancel="reject">
     <!-- 组件中的自定义插槽 -->
    </overlay-component>
  </TemplatePromise>
</template>

<style scoped>
.fade-enter-active, .fade-leave-active {
  transition: opacity .5s;
}
.fade-enter, .fade-leave-to {
  opacity: 0;
}
</style>

Injection Holder

除了使用 createOverlay 与 renderOverlay 创建使用弹出层组件外,还支持使用 useInjectHolder 创建在组件内部的弹出层组件,并继承应用的当前上下文。

<!-- App.vue -->
<script setup>
import { useInjectHolder } from '@unoverlays/vue'
import OverlayComponent from './overlay.vue'
// 通过 useInjectHolder(Component) 创建支持当前 context 的组件持有者
const [overlayApi, holder] = useInjectHolder(OverlayComponent)

function open() {
  // 打开弹出层
  overlayApi()
    .then((result) => {})
}
</script>

<template>
  <div @click="overlayApi">
    open
  </div>
  <!-- 使用 <component :is="holder" /> 挂载 -->
  <component :is="holder" />
</template>

插槽与 VNode 渲染

如果您想支持 template 模式下渲染插槽,以及 props 中传入的某个字段,只需要定义插槽后传入默认内容。

<script setup>
import { useOverlayMeta } from '@unoverlays/vue'
defineProps({ title: String })

const { visible, /* ... */ } = useOverlayMeta()
</script>

<template>
  <div v-if="visible">
    <slot name="title">
      <!-- 传入默认内容 -->
      {{ title }}
    </slot>
  </div>
</template>

如果您想在回调(命令式)下,支持某个字段渲染 VNode,建议您使用内置组件 FieldRender

FieldRender 会帮您处理 VNode、Component、String 的渲染,它可以在回调和 template 下同时使用。

以下是同时支持 Slots、String、VNode、Component 的完整例子:

<script lang="ts" setup>
import { Component, VNode } from 'vue'
import { FieldRender, useOverlayMeta } from '@unoverlays/vue'
defineProps<{
  title?: String | VNode | Component
}>()

const { visible, /* ... */ } = useOverlayMeta()
</script>

<template>
  <div v-if="visible">
    <slot name="title">
      <!-- 传入字符串、虚拟节点或组件 -->
      <FieldRender :value="title" />
    </slot>
  </div>
</template>
const result = await renderOverlay(Component, {
  props: {
    title: h('div', 'You Content')
  }
})