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

落落落洛克
• 阅读 1167

我的前端学习笔记📒

最近花了点时间把笔记整理到语雀上了,方便童鞋们阅读

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

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

前言

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

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

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

一个小场景

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

如下图:

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

小误区

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

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

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

配置化弹框

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

// 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: 后端控制弹框显示的字段-通过接口请求获取

发布订阅模式来管理弹框

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

// modalControl.js
class ModalControl {
  constructor () {
    // ...
  }
  // 订阅
  add () {
    // ...
    this.nodify()
  }
  // 发布
  notify () {
    // ...
  }
}

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

那什么时候发布呢

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

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

第一版实现

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

class ModalControl {
  constructor () {
    // ...
  }
  // 订阅
  add () {
    // ...
    this.preCheck()
  }
  // 预检测
   preCheck(){
    if(this.modalList.length === n){
      // ...
      this.notify()
    }
  }
  // 发布
  notify () {
    // ...
  }
}

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

constructor构造函数

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

// 上述弹框配置
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

  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这个方法很简单,单纯的用来判断当前页面的弹框是否都订阅完成

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

notify发布

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

  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

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

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😊

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,执行以下操作就可

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对应弹框,实现多对一的关系

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']
    }]
  }
}

第二版代码

/* 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,执行以下操作就可

git clone git@github.com:vnues/modal-control.git

git checkout feature/second

yarn 

yarn serve

待解决问题

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

这里我给出两种思路

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

我的前端学习笔记📒

最近花了点时间把笔记整理到语雀上了,方便童鞋们阅读

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

总结

  • 文中如有错误,欢迎在评论区指正,如果这篇文章帮到了你,欢迎点赞和关注😊

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

点赞
收藏
评论区
推荐文章
落落落洛克 落落落洛克
2年前
性能优化之防抖和节流
我的前端学习笔记📒最近花了点时间把笔记整理到语雀上了,方便童鞋们阅读我的前端学习笔记📒(https://www.yuque.com/wanggangfeng
刚刚好 刚刚好
4个月前
css问题
1、在IOS中图片不显示(给图片加了圆角或者img没有父级)<div<imgsrc""/</divdiv{width:20px;height:20px;borderradius:20px;overflow:h
落落落洛克 落落落洛克
1年前
React系列-轻松学会Hooks(中)
我的前端学习笔记📒最近花了点时间把笔记整理到语雀上了,方便童鞋们阅读我的前端学习笔记📒(https://www.yuque.com/wanggangfeng
落落落洛克 落落落洛克
1年前
Redux系列之分析中间件原理(附经验分享)
我的前端学习笔记📒最近花了点时间把笔记整理到语雀上了,方便童鞋们阅读我的前端学习笔记📒(https://www.yuque.com/wanggangfeng
blmius blmius
1年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
小森森 小森森
4个月前
校园表白墙微信小程序V1.0 SayLove -基于微信云开发-一键快速搭建,开箱即用
后续会继续更新,敬请期待2.0全新版本欢迎添加左边的微信一起探讨!项目地址:(https://www.aliyun.com/activity/daily/bestoffer?userCodesskuuw5n)\2.Bug修复更新日历2.情侣脸功能大家不要使用了,现在阿里云的接口已经要收费了(土豪请随意),\\和注意
落落落洛克 落落落洛克
1年前
JavaScript进阶之实现拖拽(上)
我的前端学习笔记📒最近花了点时间把笔记整理到语雀上了,方便童鞋们阅读我的前端学习笔记📒(https://www.yuque.com/wanggangfeng
落落落洛克 落落落洛克
2年前
React系列之高阶组件
我的前端学习笔记📒最近花了点时间把笔记整理到语雀上了,方便童鞋们阅读我的前端学习笔记📒(https://www.yuque.com/wanggangfeng
晴空闲云 晴空闲云
4个月前
css中box-sizing解放盒子实际宽高计算
我们知道传统的盒子模型,如果增加内边距padding和边框border,那么会撑大整个盒子,造成盒子的宽度不好计算,在实务中特别不方便。boxsizing可以设置盒模型的方式,可以很好的设置固定宽高的盒模型。盒子宽高计算假如我们设置如下盒子:宽度和高度均为200px,那么这会这个盒子实际的宽高就都是200px。但是当我们设置这个盒子的边框和内间距的时候,那
艾木酱 艾木酱
3个月前
快速入门|使用MemFire Cloud构建React Native应用程序
MemFireCloud是一款提供云数据库,用户可以创建云数据库,并对数据库进行管理,还可以对数据库进行备份操作。它还提供后端即服务,用户可以在1分钟内新建一个应用,使用自动生成的API和SDK,访问云数据库、对象存储、用户认证与授权等功能,可专
helloworld_28799839 helloworld_28799839
4个月前
常用知识整理
Javascript判断对象是否为空jsObject.keys(myObject).length0经常使用的三元运算我们经常遇到处理表格列状态字段如status的时候可以用到vue