寫C端,如何優雅的處理多個彈框的顯示?(附帶源碼)

前言

最近寫的移動端業務常常跟彈框打交道,偶爾處理對於多個彈框的顯示問題也是捉襟見肘,特別是產品常常改需求,那麼有沒有一種優雅的解決方案去處理上面這種問題,或者說,淘寶拼多多等是怎麼處理這種問題的前端

因爲項目一開始沒有作好規劃或者說一開始就不是你維護的,致使首頁的彈窗組件可能放了十多個甚至更多,不只是首頁有,首頁內又引入了十多個個子組件,這些子組件內也有彈框,另外子組件的子組件也可能存在彈框,每一個彈窗都有對應的一組控制顯隱邏輯,可是你不可能讓全部符合顯示條件的彈窗都全都一會兒在首頁彈出來,如何有順序的管理這些彈框是重中之重的事情git

一個小場景

上面這麼分析可能有同窗仍是不瞭解這個業務痛點,咱們舉個例子,假設首頁頁面有個A組件,A組件有一個彈框A_Modal須要在打開首頁顯示出來,enen...很簡單,咱們按照平時的邏輯請求後端接口拿到數據去控制彈框顯示就行,咱們繼續接着迭代,此時遇到了一個B組件,一樣也是要顯示在首頁由於是新活動,因此優先級比較大須要顯示B_Modal彈框,這時候你可能要去找找控制A組件的接口找到後端說這個組件不顯示了或者說本身手動重置爲false,一個組件能夠這樣搞,可是幾十個呢?,不太現實github

以下圖:後端

這些彈框是都要在首頁上顯示的彈框設計模式

小誤區

❗️注意如下這種交互彈框不在咱們討論範圍以內,好比經過按鈕彈出彈框這種,像這類彈框經過交互事件咱們控制就行,咱們要處理的彈框場景是經過後端接口來顯示彈框,因此後面咱們所說的彈框都是這種狀況,注意便可api

帶着這個業務痛點,我去踩坑了幾種方案,下面來分享下如下這種配置化彈框方案(借鑑了動態表單的思路來實現數組

配置化彈框

以前寫管理後臺系統的時候有了解過動態表單,實際就是經過一串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實現多個接口一塊兒決定彈框是否展現,我的仍是推薦第一種解決方案

個人前端學習筆記📒

最近花了點時間把筆記整理到語雀上了,方便童鞋們閱讀

總結

  • 文中若有錯誤,歡迎在評論區指正,若是這篇文章幫到了你,歡迎點贊和關注😊

相關文章
相關標籤/搜索