目录
- 功能目标
- 快速使用
- 多处调用
- 同一弹窗,多实例 add
- 移除 remove
- 替换 replace
- 更新 update
- 预注册 componentsCache
- 依赖注入
- usePop 工具函数
- core 实现
- core utils
- 工具
当UI库弹窗无法满足自定义需求时,需要我们自己开发简单的弹窗组件。弹窗组件与普通业务组件开发没有太大区别,重点在多弹窗之间的关系控制。例如: 弹窗1,弹窗2 由于触发时机不同,需要不同的层叠关系,后触发的始终在最前端,点击弹窗头改变层叠关系。 单一弹窗多处调用等。这里封装基础的管理钩子,简化这些问题的处理。
功能目标
- 单例,多例弹窗
- 可配置弹窗自定义参数
- 可接收弹窗自定义事件
- 层级控制
- 自定义定位
该钩子的目的主要为了处理弹窗之间的控制关系,具体如何渲染交由调用方
快速使用
// 主容器
import { usePopContainer, buildDefaultPopBind, position } from '@/hooks/usePop'
import UserInfoPop form './UserInfoPop.vue'
// 快捷工具,将内部钩子通过依赖注入,共享给子组件
const [popMap, popTools] = usePopContainer()
const popBind = buildDefaultPopBind(popTools, popTools.componentsCache)
const userPop = popBind('userInfo', UserInfoPop, {
position: { // 组件定位
top: 200
},
userId: 'xxx', // 组件porps
@close(){ // 组件事件
console.log('close')
}
})
// 调用
userPop.open()
setTimeout(userPop.close, 1000 * 3)
// template
<template v-for="(pop, popId) of popMap">
// 渲染弹窗列表
<component
:is="pop.component"
:key="popId"
v-bind="pop.props"
v-on="pop.on"
>
</component>
</template>
多处调用
同一弹窗,多实例 add
// 容器注册
const [popMap, popTools] = usePopContainer()
// 新增弹窗1
popTools.add(popId1, {
component: UserPop, // 弹窗组件
useId: 'xxx', // 弹窗Props
'@close': () => { ... } //弹窗事件
})
// 新增弹窗2
popTools.add(popId2, {
component: UserPop, // 弹窗组件
useId: 'xxx', // 弹窗Props
'@close': () => { ... } //弹窗事件
})
// 覆盖弹窗1
// popId 为弹窗唯一标识, 如果popId相同,组件配置将被替换
popTools.add(popId1, {
component: UserPop, // 弹窗组件
useId: 'yyy', // 弹窗Props
'@close': () => { ... } //弹窗事件
})
所有弹窗都通过popId,查找or判断唯一性。
配置参数:以@
开头的都将组为组件的事件被绑定, 除了@[事件名]
component
其他属性都将作为props,包括 style 等属性
移除 remove
const [popMap, popTools] = usePopContainer()
popTools.add(popId, {
component: UserPop, // 弹窗组件
useId: 'xxx', // 弹窗Props
'@close': () => { ... } //弹窗事件
})
// 移除
popTools.remove(popId)
替换 replace
// 主容器
const [popMap, popTools] = usePopContainer()
// 子组件A
popTools.replace(popId, {
component: UserPop, // 弹窗组件
useId: 'xxx', // 弹窗Props
'@close': () => { ... } //弹窗事件
})
// 子组件B
popTools.replace(popId, {
component: UserPop, // 弹窗组件
useId: 'xxx', // 弹窗Props
'@close': () => { ... } //弹窗事件
})
当有多处调用同一弹窗,而只需要最新的触发弹窗时,使用 replace. 该方法其实就是remove
add
的包装方法
更新 update
const [popMap, popTools] = usePopContainer()
popTools.replace(popId, {
component: UserPop, // 弹窗组件
useId: 'xxx', // 弹窗Props
'@close': () => { ... } //弹窗事件
})
// 更新参数
popTools.update(popId, {
useId: 'yyy'
})
通过popId 查询弹窗,将新传入的参数与原配置做合并
预注册 componentsCache
const [popMap, popTools] = usePopContainer()
// 局部预先注册
// 注册只是预先缓存
popTools.componentsCache.add(popId, {
component: UserPop,
id: 'xxx',
'@cloes': () => {...}
})
// 调用
popTools.add(popId, { component: popId })
// of
popTools.replace(popId, { component: popId })
// add将从componentsCache查询预注册配置
除了局部缓存, componentsCache, 模块还导出了 globalComponentsCache 全局公共缓存。
依赖注入
为了方便父子组件调用,提供了usePopContainer
usePopChildren
方法,
// 父组件
const [popMap, popTools] = usePopContainer()
// 子组件
const { popTools } = usePopChildren()
popTools.add({
...
})
函数接收依赖注入标识, 为传入标识时,使用默认标识
usePop 工具函数
- add(popId, options) 创建弹窗
- update(popId, options) 更新弹窗配置(定位, props,events)
- remove(popId) 移除弹窗
- replace(popId, options) 替换,如果多处调用同一弹窗,希望只显示唯一同类弹窗时,
使用该函数,多个弹窗公用相同的popId
- clearAllPop() 清空所有弹窗
- updateIndex(popId) 更新弹窗层级
- downIndex(popId) 层级下降一级
- topIndex(popId) 层级置顶
core 实现
import { shallowRef, unref, provide, inject } from 'vue'
import { merge } from 'lodash-es'
import { splitProps, counter } from './utils'
export const DEFAULT_POP_SIGN = 'DEFAULT_POP_SIGN'
// 全局层级累加器
export const counterStore = counter()
/**
* 预先pop注册表
* @summary
* 便捷多处pop调用, 调用pop显示方法时,
* 直接通过名称查询对应的组件预设
* 将调用与事件配置解耦
* @returns
*/
function componentsRegistry () {
let componentsCache = new Map([])
function has (componentName) {
return componentsCache.has(componentName)
}
function add (componentName, options) {
componentsCache.set(componentName, options)
}
function remove (componentName) {
if (has(componentName)) {
componentsCache.delete(componentName)
}
}
function fined (componentName) {
return componentsCache.get(componentName)
}
function clear () {
componentsCache = new Map([])
}
function getComponents () {
return [...componentsCache.values()]
}
function getComponentNames () {
return [...componentsCache.keys()]
}
return {
has,
add,
remove,
fined,
clear,
getComponents,
getComponentNames
}
}
export const globalComponentsCache = componentsRegistry()
/**
* 弹窗控制器
* @summary
* 提供多弹窗控制逻辑:
* 1. 单例, 多例: 通过不同的 popId 控制弹窗实例的个数
* 2. 参数接收: open接收初始传给pop的事件和参数配置, update 提供参数更新
* 3. 事件回调: options 配置属性 { @[事件名称]:事件回调 } 将作为事件绑定到pop上
* 4. 动态叠加: 内部将为组件配置 zIndex, 组件内需要自定义接收该参数,判断如何处理层叠关系
* 5. 定位: 定位需要弹窗组件接收 position props 内部绑定样式
*
* @tips
* 这里定位为了兼容 useMove做了接口调整,原接口直接输出定位样式。当前出position属性,
* 组件内需要自行处理定位样式。 这里存在 style 合并和透传的的问题, 通过透传的style与
* props 内定义的style将分开处理, 即最终的结果时两个style的集合, 且透传的style优先级高于
* prop。所以如果直出定位样式,通过透传绑定给弹窗组件,后续的useMove拖拽样式将始终被透传样式覆盖
*
* @api
* - add(popId, options) 创建弹窗
* - update(popId, options) 更新弹窗配置(定位, props,events)
* - remove(popId) 移除弹窗
* - replace(popId, options) 替换,如果多处调用同一弹窗,希望只显示唯一同类弹窗时,
* 使用该函数,多个弹窗公用相同的popId
* - clearAllPop() 清空所有弹窗
* - updateIndex(popId) 更新弹窗层级
* - downIndex(popId) 层级下降一级
* - topIndex(popId) 层级置顶
*
* @example01 - 一般使用
*
* const [
* pops,
* popTools
* ] = usePop()
*
*
* // 容器组件
* <component
* v-for='(pop, popId) of pops'
* :is='pop'
* v-bind='pop.props' // 接收定位样式
* v-on='pop.on' // 接收回调事件
* :key='popId'>
* </component>
*
* // 调用弹窗
* popTools.add('popId', {
* component: POP, // 弹窗组件
* position: { top: 200 } // 弹窗定位
* title: 'xxx', // 弹窗自定义props
* @click(e){ // 弹窗事件
* ....
* }
* })
*
*
* @example02 - 预注册
* 通过预注册组件,再次调用时,只需要传入对应注册名称,而不需要具体的配置项
* const [ pops, popTools ] = usePop()
*
* // 注册本地弹窗
* popTools.componentsCache.add('userInfo', {
* component: CMP,
* opsition: { ... }
* ...
* })
*
* // 调用
* popTools.add('userInfo', { component: 'userInfo' })
*
*/
export function usePop () {
const components = shallowRef({})
const componentsCache = componentsRegistry()
function has (popId) {
return !!unref(components)[popId]
}
/**
* 添加pop
* @param popId
* @param options
* @returns
*/
function add (popId, options = {}) {
if (has(popId)) {
return false
}
let {
component,
..._options
} = options
// 全局缓存
if (globalComponentsCache.has(component)) {
const { component: cacheComponents, ...cacheOptions } = globalComponentsCache.fined(component)
component = cacheComponents
_options = { ...cacheOptions, ..._options }
}
// 局部缓存
if (componentsCache.has(component)) {
const { component: cacheComponents, ...cacheOptions } = componentsCache.fined(component)
component = cacheComponents
_options = { ...cacheOptions, ..._options }
}
counterStore.add()
const newOptions = splitProps({ ..._options, zIndex: counterStore.getCount() })
components.value = {
...components.value,
[popId]: {
popId,
component,
...newOptions
}
}
}
/**
* 更新组件参数
* @param {*} popId
* @param {*} options
* @returns
*/
function update (popId, options = {}) {
if (!has(popId)) {
return false
}
const { component, ...oldOptions } = components.value[popId]
const newOptions = splitProps(options)
components.value = {
...components.value,
[popId]: {
component,
...merge(oldOptions, newOptions)
}
}
}
/**
* 移除pop
* @param popId
*/
function remove (popId) {
if (has(popId)) {
const newCmp = components.value
delete newCmp[popId]
components.value = {
...newCmp
}
}
}
/**
* 多处调用同一pop时, 替换原显示pop。
* @param popId
* @param options
*/
function replace (popId, options) {
remove(popId)
add(popId, options)
}
function clearAllPop () {
components.value = {}
}
/**
* 向上一层级
* @param popId
* @returns
*/
function updateIndex (popId) {
if (!has(popId)) {
return
}
const currentComponent = unref(components)[popId]
const upComponent = Object.values(unref(components)).fined(i => i.zIndex > currentComponent.zIndex)
const currentIndex = currentComponent.zIndex
const upIndex = upComponent.zIndex
update(currentIndex.popId, {
zIndex: upIndex
})
update(upComponent.popId, {
zIndex: currentIndex
})
}
/**
* 向下一层级
* @param {*} popId
* @returns
*/
function downIndex (popId) {
if (!has(popId)) {
return
}
const currentComponent = unref(components)[popId]
const upComponent = Object.values(unref(components)).fined(i => i.zIndex < currentComponent.zIndex)
const currentIndex = currentComponent.zIndex
const upIndex = upComponent.zIndex
update(currentIndex.popId, {
zIndex: upIndex
})
update(upComponent.popId, {
zIndex: currentIndex
})
}
/**
* 顶层
* @param popId
* @returns
*/
function topIndex (popId) {
if (!has(popId)) {
return
}
counterStore.add()
update(popId, {
zIndex: counterStore.getCount()
})
}
return [ components, { has, add, remove, update, replace, clearAllPop, topIndex, updateIndex, downIndex, componentsCache } ]
}
/**
* 嵌套结构下的弹窗钩子
*/
// 容器钩子
export function usePopContainer (provideKey = DEFAULT_POP_SIGN) {
const [popMap, popTools] = usePop()
provide(
provideKey,
{
popTools
}
)
return [ popMap, popTools ]
}
// 子容器钩子
export function usePopChildren (provideKey = DEFAULT_POP_SIGN) {
return inject(provideKey, {})
}
core utils
export function isEvent (propName) {
const rule = /^@/i
return rule.test(propName)
}
// @click => click
export function eventNameTransition (name) {
return name.replace('@', '')
}
// 拆分事件与属性
export function splitProps (cmpProps) {
return Object.entries(cmpProps).reduce((acc, [propName, propValue]) => {
if (isEvent(propName)) {
// 自定义事件
acc.on[eventNameTransition(propName)] = propValue
} else {
acc.props[propName] = propValue
}
return acc
}, { on: {}, props: {} })
}
export function counter (initCount = 0, step = 1) {
let count = initCount
function add (customStep) {
count += customStep || step
}
function reduce (customStep) {
count -= customStep || step
}
function reset (customStep) {
count = customStep || initCount
}
function getCount () {
return count
}
return {
add,
reduce,
reset,
getCount
}
}
工具
import { merge } from 'lodash-es'
/**
* 注册并返回弹窗快捷方法
* @param {*} popTools
* @returns
*/
export function buildDefaultPopBind (popTools, componentsCache) {
return (popId, component, options) => {
componentsCache.add(popId, {
component,
// 默认定位
position: position(),
...bindDefaultEvents(popTools, popId),
...options
})
return {
open (options) {
popTools.add(popId, { component: popId, ...options })
},
close () {
popTools.remove(popId)
},
update (options) {
popTools.update(popId, { component: popId, ...options })
},
replace (options) {
popTools.replace(popId, { component: popId, ...options })
}
}
}
}
export const DEFAULT_POSITION = {
top: 240,
left: 0,
right: 0
}
export function position (options = DEFAULT_POSITION) {
return merge({}, DEFAULT_POSITION, options)
}
export function bindDefaultEvents (popTools, popId) {
return {
'@headerMousedown' () {
popTools.topIndex(popId)
},
'@close' (e) {
popTools.remove(popId)
}
}
}