写C端,如何优雅的处理多个弹框的显示?(附带源码)

2021-01-08 14:30:59 浏览数 (2)

前言

❝最近写的移动端业务经常跟弹框打交道,偶尔处理对于多个弹框的显示问题也是捉襟见肘,特别是产品经常改需求,那么有没有一种优雅的解决方案去处理上面这种问题,或者说,淘宝拼多多等是怎么处理这种问题的 ❞

由于项目一开始没有做好规划或者说一开始就不是你维护的,导致首页的弹窗组件可能放了十多个甚至更多,不仅是首页有,首页内又引入了十多个个子组件,这些子组件内也有弹框,另外子组件的子组件也可能存在弹框,每个弹窗都有对应的一组控制显隐逻辑,但是你不可能让所有符合显示条件的弹窗都全都一下子在首页弹出来,如何有顺序的管理这些弹框是重中之重的事情

一个小场景

❝上面这么分析可能有同学还是不了解这个业务痛点,我们举个例子,假设首页页面有个A组件,A组件有一个弹框A_Modal需要在打开首页显示出来,enen...很简单,我们按照平时的逻辑请求后端接口拿到数据去控制弹框显示就行,我们继续接着迭代,此时遇到了一个B组件,同样也是要显示在首页因为是新活动,所以优先级比较大需要显示B_Modal弹框,这时候你可能要去找找控制A组件的接口找到后端说这个组件不显示了或者说自己手动重置为false,一个组件可以这样搞,但是几十个呢?,不太现实

如下图:

❝这些弹框是都要在首页上显示的弹框 ❞

小误区

❝❗️注意以下这种交互弹框不在我们讨论范围之内,比如通过按钮弹出弹框这种,像这类弹框通过交互事件我们控制就行,我们要处理的弹框场景是通过后端接口来显示弹框,所以后面我们所说的弹框都是这种情况,注意即可 ❞

带着这个业务痛点,我去踩坑了几种方案,下面来分享下以下这种配置化弹框方案(借鉴了动态表单的思路来实现

配置化弹框

❝之前写管理后台系统的时候有了解过动态表单,实际就是通过一串JSON数据渲染出表单,那么我们是不是可以基于这种思路,通过可配置化的数据来控制弹框的显示,显然是可以的 ❞

代码语言:javascript复制
// modalConfig.js
export default {
  // 首页
  index: {
    // 弹框列表
    modalList: [{
      id: 1, // 弹框的id
      name: 'modalA',
      level: 100,
      // 弹框的优先级
      // 由前端控制弹框是否显示
      // 当我们一个活动过去了废弃一个弹框时候,可以不需要通过后端去更改
      frontShow: true
    }, {
      id: 2,
      name: 'modalB',
      level: 122,
      frontShow: true
    }, {
      id: 3,
      name: 'modalC',
      level: 70,
      frontShow: true
    }]
  }
}

这样做的好处就是利于管理弹框,并且最重要的一点,我可以知道我的页面有多少弹框一目了然的去配置,这里我们先讲解下每个弹框modal的属性

  • id:弹框id-弹框的唯一id
  • name: 弹框名称-可以根据名称很快找到该页面上的弹框
  • level: 弹框优先级-杜绝一个页面可能提示展示多个弹窗的情况
  • frontShow: 前端控制弹框显示的字段-默认为true
  • backShow: 后端控制弹框显示的字段-通过接口请求获取

发布订阅模式来管理弹框

❝配置完弹框数据,我们还缺少一个调度系统去统一管理这些弹框,这时候自然而然就可以想到发布订阅这种设计模式 ❞

代码语言:javascript复制
// modalControl.js
class ModalControl {
  constructor () {
    // ...
  }
  // 订阅
  add () {
    // ...
    this.nodify()
  }
  // 发布
  notify () {
    // ...
  }
}

正常情况下,后端单个接口会返回给我们字段来控制弹框的显示,当然也可能存在多个接口去控制弹框的显示,对于这些情况,我们前端自己去做一层合并,只要保证最后得出一个控制弹框是否展示的字段就行,此时我们就可以在相应的位置取注册我们的弹框类即可

那什么时候发布呢

注意这里的发布跟我们平时的发布判断情况可能不一样,以前我们可能通过在一个生命周期钩子或者按钮触发等事件去发布,但是我们仔细想想,进入首页由接口控制显示,这样动作的发生需要2个条件

  • 每次发生一次订阅操作都伴随着一次执行一次预检测操作,检测所有的弹框是否都订阅完
  • 真正触发的时机是当前页面的弹框都订阅完了,因为只有这样才能拿到所有弹框的优先级,才能判断显示哪个弹框

第一版实现

❝根据上面的分析单个接口返回的就是一个订阅,而发布是等到所有的弹框都订阅完才执行,于是我们可以快速写出以下代码结构 ❞

代码语言:javascript复制
class ModalControl {
  constructor () {
    // ...
  }
  // 订阅
  add () {
    // ...
    this.preCheck()
  }
  // 预检测
   preCheck(){
    if(this.modalList.length === n){
      // ...
      this.notify()
    }
  }
  // 发布
  notify () {
    // ...
  }
}

实现这个弹框类,我们来拆分实现这四个方法就行了

constructor构造函数

❝根据以上思路,ModalControl类的 constructor方法中需要设置的初始值差不多也就知道了 ❞

代码语言:javascript复制
// 上述弹框配置
import modalMap from './modalMap'
constructor (type) {
  this.type = type // 页面类型
 this.modalFlatMap = {} // 用于缓存所有已经订阅的弹窗的信息
 this.modalList = getAllModalList(modalMap[this.type]) // 该页面下所有需要订阅的弹框列表,数组长度就是n值
}
// 弹框信息
modalInfo = {
    name: modalItem.name,
    level: modalItem.level,
    frontShow: modalItem.frontShow,
    backShow: infoObj.backShow,
    handler: infoObj.handler // 表示选择出了需要展示的弹窗时,该执行的函数
 }

constructor构造函数接收一个所有弹框的配置项,里面声明两个属性,modalFlatMap用于缓存所有已经订阅的弹窗的信息modalList表示该页面下所有需要订阅的弹框列表,数组长度就是n值

add订阅

❝我们以弹框的id的作为唯一key值,当请求后端数据接口成功后,在该请求方法相应的回调里进行订阅操作,并且每次订阅都会去检测下调用preCheck方法来判断当前页面的所有弹框是否已经订阅完,如果,则触发notify

代码语言:javascript复制
  add (modalItem, infoObj) {
    this.modalFlatMap[modalItem.name] = {
      id: modalItem.id,
      level: modalItem.level,
      frontShow: modalItem.frontShow,
      backShow: infoObj.backShow,
      handler: infoObj.handler
    }
    this.preCheck()
  }

preCheck检测

preCheck这个方法很简单,单纯的用来判断当前页面的弹框是否都订阅完成

代码语言:javascript复制
 if (this.modalList.length === Object.values(this.modalFlatMap).length) {
      this.notify()
  }

notify发布

❝当我们页面上的弹框全部都订阅完后就会触发notify发布,这个notify主要做了这么一件事情:过滤不需要显示的弹框,筛选出当前页面需要显示并且优先级最高的弹框,然后触发其handler方法

代码语言:javascript复制
  notify () {
    const highLevelModal = Object.values(this.modalFlatMap).filter(item => item.backShow && item.frontShow).reduce((t, c) => {
      return c.level > t.level ? c : t
    }, { level: -1 })
    highLevelModal.handler && highLevelModal.handler()
  }

单例模式完善ModalControl

❝到上面的步骤,其实我们的弹框管理类已经差不多完成了,但是考虑到弹框可能分布在子组件或者孙组件等等,这时候如果都在每个组件实例化弹框类,那么他们实际是没有关联的,此时单例模式就派上用场了

代码语言:javascript复制
const controlTypeMap = {}
// 获取单例
function createModalControl (type) {
  if (!controlTypeMap[type]) {
    controlTypeMap[type] = new ModalControl(type)
  }
  console.log('controlTypeMap[type]', controlTypeMap[type])
  return controlTypeMap[type]
}

export default createModalControl

第一版代码

❝第一版的代码就这样完成了,是不是很简单,搭配modalConfig发布订阅模式,我们可以处理大部分问题了,为自己打个call? ❞

代码语言:javascript复制
class ModalControl {
  constructor (type) {
    this.type = type
    this.modalFlatMap = {}
    this.modalList = getAllModalList(modalMap[this.type])
  }

  add (modalItem, infoObj) {
    this.modalFlatMap[modalItem.name] = {
      id: modalItem.id,
      level: modalItem.level,
      frontShow: modalItem.frontShow,
      backShow: infoObj.backShow,
      handler: infoObj.handler
    }
    this.preCheck()
  }

  preCheck () {
    if (this.modalList.length === Object.values(this.modalFlatMap).length) {
      this.notify()
    }
  }

  notify () {
    const highLevelModal = Object.values(this.modalFlatMap).filter(item => item.backShow && item.frontShow).reduce((t, c) => {
      return c.level > t.level ? c : t
    }, { level: -1 })
    highLevelModal.handler && highLevelModal.handler()
  }
}

const controlTypeMap = {}
// 获取单例
function createModalControl (type) {
  if (!controlTypeMap[type]) {
    controlTypeMap[type] = new ModalControl(type)
  }
  console.log('controlTypeMap[type]', controlTypeMap[type])
  return controlTypeMap[type]
}

export default createModalControl

demo验证一下

❝第一版的代码例子?在该仓库下demo,执行以下操作就可 ❞

代码语言:javascript复制
git clone git@github.com:vnues/modal-control.git

git checkout feature/first

yarn 

yarn serve

第二版

❝第一版的ModalControl可以解决我们开发中遇到的场景,但是我们还要考虑一下复杂场景

接下来,我们来完善我们的弹框类ModalControl,我们先来分析下需要注意哪些问题吧

  • 可能存在多个接口控制弹框显示(比如A接口也可以调取这个弹框,后面持续迭代,B接口也可能调取这个弹框),所以不再是那种一对一的关系,而是多对一的关系,多个接口都可以控制这个弹框的显示,这里通过apiFlag来标识弹框,不再使用name

得益于我们的modalConfig配置,我们只需要补充一个apiFlag字段,便可以解决上述问题,是不是很方便,其实后续的复杂场景,也在这里补充字段完善就行

modalConfig

❝增加apiFlag字段,由name字段对应弹框变为apiFlag对应弹框,实现多对一的关系

代码语言:javascript复制
export default {
  // 首页
  index: {
    // 弹框列表
    modalList: [{
      id: 1, // 弹框的id
      name: 'modalA',
      level: 100,
      frontShow: true,
      apiFlag: ['mockA_1', 'mockA_2']
    }, {
      id: 2,
      name: 'modalB',
      level: 122,
      frontShow: true,
      apiFlag: ['mockB_1', 'mockB_2']
    }, {
      id: 3,
      name: 'modalC',
      level: 70,
      frontShow: true,
      apiFlag: ['mockC_1']
    }]
  }
}

第二版代码

代码语言:javascript复制
/* eslint-disable no-console */
/* eslint-disable no-unused-vars */
import modalMap from './modalConfig'

const getAllModalList = mapObj => {
  let currentList = []
  if (mapObj.modalList) {
    currentList = currentList.concat(
      mapObj.modalList.reduce((t, c) => t.concat(c.id), [])
    )
  }
  if (mapObj.children) {
    currentList = currentList.concat(
      Object.values(mapObj.children).reduce((t, c) => {
        return t.concat(getAllModalList(c))
      }, [])
    )
  }
  return currentList
}

const getModalItemByApiFlag = (apiFlag, mapObj) => {
  let mapItem = null
  // 首先查找 modalList
  const isExist = (mapObj.modalList || []).some(item => {
    if (item.apiFlag === apiFlag || (Array.isArray(item.apiFlag) && item.apiFlag.includes(apiFlag))) {
      mapItem = item
    }
    return mapItem
  })
  // modalList没找到,继续找 children
  if (!isExist) {
    Object.values(mapObj.children || []).some(mo => {
      mapItem = getModalItemByApiFlag(apiFlag, mo)
      return mapItem
    })
  }
  return mapItem
}
class ModalControl {
  constructor (type) {
    this.type = type
    this.modalFlatMap = {} // 用于缓存所有已经订阅的弹窗的信息
    this.modalList = getAllModalList(modalMap[this.type]) // 该页面下所有需要订阅的弹框列表,数组长度就是n值
  }

  add (apiFlag, infoObj) {
    const modalItem = getModalItemByApiFlag(apiFlag, modalMap[this.type])
    console.log('modalItem', modalItem)
    this.modalFlatMap[apiFlag] = {
      level: modalItem.level,
      name: modalItem.name,
      frontShow: modalItem.frontShow,
      backShow: infoObj.backShow,
      handler: infoObj.handler
    }
    this.preCheck()
  }

  preCheck () {
    if (this.modalList.length === Object.values(this.modalFlatMap).length) {
      this.notify()
    }
  }

  notify () {
    const highLevelModal = Object.values(this.modalFlatMap).filter(item => item.backShow && item.frontShow).reduce((t, c) => {
      return c.level > t.level ? c : t
    }, { level: -1 })
    highLevelModal.handler && highLevelModal.handler()
  }
}

const controlTypeMap = {}
// 获取单例
function createModalControl (type) {
  if (!controlTypeMap[type]) {
    controlTypeMap[type] = new ModalControl(type)
  }
  console.log('controlTypeMap[type]', controlTypeMap[type])
  return controlTypeMap[type]
}

export default createModalControl

demo验证一下

❝第一版的代码例子?在该仓库下demo,执行以下操作就可 ❞

代码语言:javascript复制
git clone git@github.com:vnues/modal-control.git

git checkout feature/second

yarn 

yarn serve

待解决问题

❝细心的童鞋可能会发现,竟然第一版和第二版分别实现了一对一多对一的关系,那么一对多的关系如何实现呢?也即是多个接口一起决定弹框是否展示

这里我给出两种思路

  • 多个接口一起决定弹框是否展示,我们完全可以在接口层做合并,最终实现出来的效果就是一对一
  • 订阅方法做去重,利用高阶函数再次封装对应的handler实现多个接口一起决定弹框是否展示,个人还是推荐第一种解决方案

前端学习笔记?

❝最近花了点时间把笔记整理到语雀上了,方便同学们阅读:公众号回复笔记或者简历

0 人点赞