跟混亂的頁面彈窗說再見

對於一些快速迭代的產品來講,特別是移動端 C端產品,基於用戶運營的目的,在 app首頁給用戶展現各類各樣的彈窗是很常見的事情,在產品初期,因爲迭代版本和運營策略變化地還不是太大,因此可能以爲沒什麼,但當產品運營到後期,各類八竿子打不着的運營策略輪番上陣,彈窗的樣式、邏輯等都變了不知道多少遍的時候,問題就出來了前端

因爲前期沒有作好規劃,首頁的彈窗組件可能放了十多個甚至更多,不只是首頁有,首頁內又引入了十多個個子組件,這些子組件內也有,搞很差這些子組件內還有子組件,子組件的子組件一樣還有彈窗,每一個彈窗都有對應的一組控制顯隱邏輯,分散在多個組件多個方法中,可是首頁只有一個頁面,你不可能讓全部符合顯示條件的彈窗,全都一會兒彈出來,反正我是沒見過這麼作的 app,那麼如何管理這些彈窗就成了頭等大事vue

而每每當你意識到這一點的時候,極可能也正是局勢發展到沒法控制的時候 治不了,等死吧react

場景:A彈窗和 B彈窗位於主組件內,C彈窗位於主組件的子組件 C中,D彈窗位於主組件的子組件 B中,E彈窗位於主組件的子組件F的子組件G中jquery

PM:我但願剛進入這個頁面的時候,只有當 A彈窗 和 B彈窗以及 C彈窗,都不展現的時候,才展現 D彈窗,若是 D彈窗展現過了,除非 B彈窗以後又展現了一遍,不然不管什麼狀況下都不展現 E彈窗git

FE:???github

因此,防患於未然是很重要的 別問我是怎麼有這個感悟的vuex

稍加思考一下,其實這件事情並不難辦,交給後端經過接口控制全部彈窗的顯隱就好了 主要是架構的提早規劃,以及低耦合的代碼邏輯redux

彈窗的配置化

先肯定一個大致思路,首先,必需要明確地知道當前頁面共有哪些彈窗組件,包括頁面的子組件以及子組件的子組件內的彈窗組件,這是必須的,不然你連有哪些組件都不知道怎麼精確控制?小程序

因此,仍是上面那句話,提早規劃,防患於未然是很重要的,否則等頁面迭代了幾十版,當初寫代碼的人都不在了你纔想到去統計一下頁面上到底有多少個彈窗,那真是夠你喝一壺的後端

那麼就須要在一個地方統一把這些彈窗全記錄下來,方便管理,因而能夠獲得下面這種數據結構:

// modalMap.js
export default {
  // 記錄首頁 index頁面內的彈窗項
  index: {
    modalList: [{
      id: 1,
      condition: 'modal_1',
      level: 100,
      feShow: true
    }, {
      id: 2,
      condition: 'modal_2',
      level: 22,
      feShow: true
    }, {
      id: 3,
      condition: 'modal_3',
      level: 70,
      feShow: false
    }],
    children: {
      child1: {
        modalList: [{
          id: 11,
          condition: 'condition_1_1',
          level: 82,
          feShow: true
        }, {
          id: 12,
          condition: ['condition_1_2', 'condition_1_3', 'condition_1_4'],
          level: 12,
          feShow: true
        }],
        children: {
          child1_1: {
            modalList: [{
              id: 21,
              condition: ['condition_1_1_1', 'condition_1_1_2'],
              level: 320,
              feShow: true
            }, {
              id: 22,
              condition: 'condition_1_1_3',
              level: 300,
              feShow: true
            }]
          }
        }
      }
    }
  }
  // ...還能夠繼續記錄其餘頁面的彈窗結構
}
複製代碼

modalMap.js文件記錄每一個頁面內全部的彈窗項,例如,首頁 index內的彈窗項都在屬性名index對應的值數據結構中,index這個頁面主組件內存在兩個 modal,它們的 id分別爲 12,若是 index這個頁面主組件的子組件內也有 modal,則繼續嵌套,例如,index主組件的子組件 child1中也有 modal,那麼就把 child1放到 indexchildren中繼續記錄,以此類推

這種結構看起來比較清晰,主組件及主組件內的子組件內的 modal都很清晰,一目瞭然,固然,你能夠不用這種結構,徹底取決於你,這裏就暫時這麼定義

每一個 modal除了 id以外,還有 conditionlevelshow屬性

condition是做爲一種標識存在,後面會說到,level 用於標識當前 modal的層級,每一個頁面正常只能同時展現一個 modal,但若是有多個 modal都同一時間都知足展現的條件,則對比它們的 level值,哪一個大就優先展現哪一個,其他的忽略掉,杜絕一個頁面可能提示展現多個彈窗的狀況;

feShow屬性則是在 modal內部來決定 modal最終是否展現,這樣一來就能夠無視外界條件,很輕鬆地在前端經過配置來禁止掉彈窗的顯示

經過發佈/訂閱模式來管理彈窗

彈窗的配置結構已經肯定了,下一步就是對這些配置的管理了

通常狀況下,多個頁面同時知足條件須要進行展現的場景,大多數都是發生在剛進入頁面,頁面發出多個請求,這些請求的返回結果分別控制對應的一個彈窗的展現

由於發出去的這些請求極可能分屬於不一樣的業務線或部門管轄,相互獨立,因此說若是把彈窗的控制權交給後端來作,實際上是有點困難的,再加上請求是異步的,前端想要用意大利麪條式代碼來保證彈窗之間的互斥性也不太容易,綜合起來,也就致使了當頁面上迭代出了數十個以上彈窗的時候,若是沒有提早規劃好,仍是很容易出現彈窗同時展現的問題的

這裏暫時就以剛進入頁面的狀況爲例,進行邏輯梳理

首先,我須要知道頁面上有哪些彈窗可能會在剛進入頁面的時候彈出來(即經過接口控制單個彈窗的展示與否),而後在全部彈窗的數據都拿到了的時候(即跟彈窗相關的接口都已經返回數據),才進行彈窗的展現

這種狀況比較適合使用發佈/訂閱者模式,單個接口的數據返回就是一個訂閱,當全部接口都訂閱以後,就進行發佈,也就是彈窗展現

// modalManage.js
class ModalManage {
  constructor () {
    // ...
  }
  add () {
    // ...
    this.nodify()
  }
  notify () {
    // ...
  }
}
複製代碼

經過 ModalManage類來管理彈窗,在 new ModalManage的時候傳入一個標識值,用於預先告知接下來一共將會進行 n 次訂閱,add就是訂閱方法,當接口返回彈窗是否展現信息的時候,就調用 add方法訂閱一次,而且緊接着在 add方法裏調用 notify,這個方法就是發佈方法

notify方法中將檢查當前訂閱的次數是否已經達到了 n次,若是是,就說明訂閱完畢,全部彈窗的信息都已經接收完畢,下一步就能夠根據這些收集起來的彈窗信息,根據必定的邏輯進行展現,例如只展現 level值最大的那個彈窗

根據以上思路,ModalManage類的 constructor方法中須要設置的初始值差很少也就知道了

constructor (modalList) {
  this.modalFlatMap = {}
  this.modalList = modalList
}
複製代碼

modalFlatMap用於緩存全部已經訂閱的彈窗的信息

conditionListModalManage 類在初始化時接收一個的參數,這個參數其實就是剛進入頁面時,頁面上全部可能展現的彈窗(包括子組件的彈窗)的 id集合,也就是必需要知道頁面上到底有多少個可能同時展現的彈窗,以上述示例代碼 modalMap.js爲例, index頁面的 modalList值就是 ['1', '2', '3', '11', '12', '21', '22']

這裏其實直接傳彈窗數量就好了,index中有 7個彈窗可能同時展現,因此能夠直接傳 7,我這裏之因此要傳名稱進去,其實是爲了方便調試,若是代碼出問題了,好比頁面上實際有 5個接口能夠控制 5個彈窗的展現,但你卻只訂閱了 4次,若是隻傳數字,你就須要一個個找過去看是哪個忘記訂閱了,但若是傳名稱,你一會兒就能調試出來,也就是代碼的可維護性會好一點

到這裏其實有個問題

若是一個彈窗都是隻由一個異步接口控制,那麼上述沒什麼問題,可是若是某個彈窗的展現,須要根據多個異步接口的返回值來控制,那麼就有問題了,特定彈窗的 id只有一個,但由於由多個接口控制,那麼根據上述邏輯,就可能存在對同一個 id進行重複訂閱的操做,因此這裏須要改一下

constructor (conditionList) {
  this.modalFlatMap = {}
  this.conditionList = conditionList
  this.hasAddConditionList = []
}
複製代碼

id只做爲一個標識符,使用 condition來控制訂閱,若是一個彈窗經過一個異步條件控制,則 condition的值就是一個字符串(這個字符串只是起到一個標識做用),若是由 n(n>=1)個異步條件控制,則值爲一個數組,數組的長度就是 n conditionList就是頁面上全部 condition的集合,hasAddConditionList用於緩存當前已經進行訂閱操做的 condition

當頁面上任意一個彈窗的狀態(便是否知足展現的條件)肯定下來後,就進行訂閱操做:

// modalManage.js
add (condition, infoObj) {
  if (!this.conditionList.includes(condition)) return console.log('無效訂閱:', condition)
  if (this.hasAddConditionList.includes(condition)) return console.log('重複訂閱:', condition)
  this.hasAddConditionList.push(condition)
  const modalItem = getModalItemByCondition(condition, modalMap)
  const existMap = this.modalFlatMap[modalItem.id]
  if (existMap) {
    // 說明當前彈窗由多個邏輯字段控制
    const handler = existMap.handler
    existMap.rdShow = existMap.rdShow && infoObj.rdShow
    existMap.handler = () => {
      handler && handler()
      infoObj.handler && infoObj.handler()
    }
  } else {
    this.modalFlatMap[modalItem.id] = {
      level: modalItem.level,
      feShow: modalItem.feShow,
      rdShow: infoObj.rdShow,
      handler: infoObj.handler
    }
  }
  this.notify()
}
複製代碼

this.modalFlatMap的屬性名就是彈窗的 id,每一個屬性的值都是一個包含 4個屬性的對象,levelfeShow就是上面 modalMap.js中的 levelfeShowrdShow是從異步接口或其餘邏輯返回的用於控制彈窗是否展現的值,handler則是用於當選擇出了須要展現的彈窗時,該執行的函數

若是對於同一個 id,有多個condition進行訂閱,則將這些訂閱的彈窗信息進行合併

this.hasAddConditionList記錄了訂閱列表的信息,當訂閱列表的長度和 thisconditionList長度相同時,說明全部的彈窗狀態都已經準備就緒,能夠根據這些彈窗的優先級進行展現了,也就是 notify方法要作的事情

notify方法中,先排除掉屬性 feShow && rdShowfalse的彈窗項,再對比剩下的彈窗的 level,只展現 level最大的那個彈窗:

// modalManage.js
notify () {
  if (this.hasAddConditionList.length === this.conditionList.length) {
    const highLevelModal = Object.values(this.modalFlatMap).filter(item => item.rdShow && item.feShow).reduce((t, c) => {
      return c.level > t.level ? c : t
    }, { level: -1 })
    highLevelModal.handler && highLevelModal.handler()
  }
}
複製代碼

使用單例模式管理嵌套組件以及多個頁面的彈窗

上述的 ModalManage類已經足以管理彈窗了,但還有個問題,若是一個頁面上的彈窗,分散位於頁面主組件及其子組件,甚至是子組件的子組件內,怎麼辦?

這個時候就須要使用單例了

// 單例管理
const manageTypeMap = {}
// 獲取單例
function createModalManage (type) {
  if (!manageTypeMap[type]) {
    manageTypeMap[type] = new ModalManage(getAllConditionList(modalMap[type]))
  }
  return manageTypeMap[type]
}
複製代碼

經過 createModalManage這個方法來建立 ModalManage實例,根據傳入的 type來決定是否建立新的實例,若是單例管理對象 manageTypeMap中不存在 type對於的實例,則 new一個 ModalManage實例,存入 manageTypeMap中,並返回這個新實例,不然就返回 manageTypeMap中已經建立好了的實例

這樣一來,不管彈窗分散在多少個組件內,不管這些組件嵌套得有多深,都可以在保證代碼低耦合的前提下,順利地訂閱/發佈事件

這裏的 getAllConditionList方法是個工具方法,用於從 modalMap中獲取頁面對應的彈窗數據結構:

const getAllConditionList = modalInfo => {
  let currentList = []
  if (modalInfo.modalList) {
    currentList = currentList.concat(
      modalInfo.modalList.reduce((t, c) => t.concat(c.condition), [])
    )
  }
  if (modalInfo.children) {
    currentList = currentList.concat(
      Object.values(modalInfo.children).reduce((t, c) => {
        return t.concat(getAllConditionList(c))
      }, [])
    )
  }
  return currentList
}
複製代碼

至於 createModalManage的參數type,其值能夠就是一個字符串,例如若是須要管理首頁 index上可能同時展現的全部的彈窗,則能夠將 type 的值指定爲 index,在 index主組件以及其包含彈窗的子組件內,都經過這個字段來獲取 ModalManage單例對象:

const modalManage = createModalManage('index')
複製代碼

這樣作同時也解決了另一個問題,就是多個頁面的彈窗管理問題,index頁面經過 index建立 ModalManage單例,詳情頁就能夠經過 detail來建立 ModalManage單例,雙方互不干擾

總結

上述全部示例代碼已經上傳到 github,有興趣地能夠看下

本文只是對彈窗這麼一種具體的案例進行分析,實際上應用於其餘場景,例如頁面同一個位置的懸浮掛件管理等都是可行的

不管是彈窗的管理仍是掛件的管理,放在 mvvm框架中,都是數據的管理,主流前端框架對於複雜的數據管理,都已經有對應的解決方案,例如 vuexredux等,這些解決方案固然也可以解決上面的問題

本文主要是對這種理念的探討,探討出一種通用的解決方案,不管你用的是 vuereactangular仍是jquery一把梭,亦或是微信小程序、支付寶小程序、快應用等,均可以在不依賴其餘庫的前提下,低成本地輕鬆套入使用

相關文章
相關標籤/搜索