探索Nuxt UI弹窗系统
本文简单介绍了对Nuxt UI弹窗系统探索的心路历程
长久以来,对「状态驱动」和「命令式」来展示弹窗始终都有争议,在知乎上,Vue作者尤雨溪有对此做过回答,他的选择是:状态驱动;而我个人更偏向于命令式,但始终没有找到一个我满意的方案。
最近在使用Nuxt UI
,它提供了useOverlay
,我认为它所提供的API非常合我的口味。于是好奇地想看看它是如何管理弹窗的,便以useOverlay
的源码为入口开始阅读。
在Usage介绍了基础的使用方法:
<script setup lang="ts">
import { LazyModalExample } from '#components'
const overlay = useOverlay()
const modal = overlay.create(LazyModalExample)
async function openModal() {
modal.open()
}
</script>
所以我从create
方法开始:
const overlays = shallowReactive<Overlay[]>([])
const create = <T extends Component>(component: T, _options?: OverlayOptions<ComponentProps<T>>): OverlayInstance<T> => {
const { props, defaultOpen, destroyOnClose } = _options || {}
const options = reactive<Overlay>({
id: Symbol(import.meta.dev ? 'useOverlay' : ''),
isOpen: !!defaultOpen,
component: markRaw(component!),
isMounted: !!defaultOpen,
destroyOnClose: !!destroyOnClose,
originalProps: props || {},
props: { ...props }
})
overlays.push(options)
return {
...options,
open: <T extends Component>(props?: ComponentProps<T>) => open(options.id, props),
close: value => close(options.id, value),
patch: <T extends Component>(props: Partial<ComponentProps<T>>) => patch(options.id, props)
}
}
它将组件和OverlayOptions
合并后 push 到了overlays
里,一个用于管理的数组,同时返回了open
、close
、patch
方法等,仅此而已。
那么再看open
方法:
const open = <T extends Component>(id: symbol, props?: ComponentProps<T>): OpenedOverlay<T> => {
const overlay = getOverlay(id)
// If props are provided, merge them with the original props, otherwise use the original props
if (props) {
overlay.props = { ...overlay.originalProps, ...props }
} else {
overlay.props = { ...overlay.originalProps }
}
overlay.isOpen = true overlay.isMounted = true const result = new Promise<any>(resolve => overlay.resolvePromise = resolve)
return Object.assign(result, {
id,
isMounted: overlay.isMounted,
isOpen: overlay.isOpen,
result
})
}
它也仅仅是通过getOverlay
找到这个弹窗后将isOpen
和isMounted
设为true
。
在getOverlay
中也仅仅是通过id
找到了对应的Overlay
:
const getOverlay = (id: symbol): Overlay => {
const overlay = overlays.find(overlay => overlay.id === id)
if (!overlay) {
throw new Error('Overlay not found')
}
return overlay
}
但是以上的这些操作,也仅仅是在操作弹窗组件的实例,并没有哪一步是将其实际渲染出来的,于是我又注意到刚刚的overlays
。
最终我找到了OverlayProvider
:
<script setup lang="ts">
import { computed } from 'vue'
import { useOverlay } from '../composables/useOverlay'
import type { Overlay } from '../composables/useOverlay'
const { overlays, unmount, close } = useOverlay()
const mountedOverlays = computed(() => overlays.filter((overlay: Overlay) => overlay.isMounted))
const onAfterLeave = (id: symbol) => {
close(id)
unmount(id)
}
const onClose = (id: symbol, value: any) => {
close(id, value)
}
</script>
<template>
<component
:is="overlay.component"
v-for="overlay in mountedOverlays"
:key="overlay.id"
v-bind="overlay.props"
v-model:open="overlay.isOpen"
@close="(value:any) => onClose(overlay.id, value)"
@after:leave="onAfterLeave(overlay.id)"
/>
</template>
这个组件使用v-for
将overlays
中的所有弹窗都进行了渲染,并且由于被push
进数组的先后顺序,似乎也很好地解决了弹窗的层级问题。
现在组件是被渲染了,但是如何控制显示和隐藏呢?显然v-model:open
一定是关键。
于是我寻找了官方示例中可以用于useOverlay
的Modal
组件
抛开所有无关的逻辑,实际与显示相关的代码是以下这些:
const rootProps = useForwardPropsEmits(reactivePick(props, 'open', 'defaultOpen', 'modal'), emits)
<template>
<DialogRoot v-slot="{ open, close }" v-bind="rootProps">
...
</DialogRoot>
</template>
根结点的DialogRoot
来自reka-ui
,实际只是将open
属性provide
了出去:
<script setup lang="ts">
provideDialogRootContext({
open,
modal,
openModal: () => {
open.value = true
},
onOpenChange: (value) => {
open.value = value
},
onOpenToggle: () => {
open.value = !open.value
},
contentId: '',
titleId: '',
descriptionId: '',
triggerElement,
contentElement,
})
</script>
对应地,寻找inject
方法(injectDialogRootContext
),最终在DialogOverly
中找到了传入open
的组件:Presence
,实际与显示相关的最简化代码为:
export default defineComponent({
name: 'Presence',
props: {
present: {
type: Boolean,
required: true,
},
},
slots: {} as SlotsType<{
default: (opts: { present: boolean }) => any
}>,
setup(props, { slots, expose }) {
const { present } = toRefs(props)
return () => {
if (present.value) {
return h(slots.default()[0] as VNode)
}
else { return null }
}
},
})
至此,我对弹窗系统的探索就结束了,其实实现非常简单,但也收获颇丰。
另外,在探索的过程中可以发现,Nuxt UI的类型声明也非常优秀,值得学习,但在此不再过多赘述。