vue usePop弹窗控制器的实现

Vue
341
0
0
2023-05-08
目录
  • 功能目标
  • 快速使用
  • 多处调用
  • 同一弹窗,多实例 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)
    }
  }
}