感謝 compose 函數,讓個人代碼屎山💩逐漸美麗了起來~

「本文已參與好文召集令活動,點擊查看:後端、大前端雙賽道投稿,2萬元獎池等你挑戰!前端

有言在先

本瓜知道前不久寫的《JS 如何函數式編程》系列各位可能並不感冒,由於一切理論的東西若是脫離實戰的話,那就將毫無心義。程序員

I6cDpC.th.png

因而乎,本瓜着手於實際工做開發,嘗試應用函數式編程的一些思想。web

最終驚人的發現:這個實現過程並不難,可是效果卻不小!編程

實現思路:藉助 compose 函數對連續的異步過程進行組裝,不一樣的組合方式實現不一樣的業務流程。後端

這樣不只提升了代碼的可讀性,還提升了代碼的擴展性。我想:這也許就是高內聚、低耦合吧~數組

撰此篇記之,並與各位分享。websocket

場景說明

在和產品第一次溝通了需求後,我理解須要實現一個應用 新建流程,具體是這樣的:markdown

第 1 步:調用 sso 接口,拿到返回結果 res_token;app

第 2 步:調用 create 接口,拿到返回結果 res_id;less

第 3 步:處理字符串,拼接 Url;

第 4 步:創建 websocket 連接;

第 5 步:拿到 websocket 後端推送關鍵字,渲染頁面;

  • 注:接口、參數有作必定簡化

上面除了第 3 步、第 5 步,剩下的都是要調接口的,而且先後步驟都有傳參的須要,能夠理解爲一個連續且有序的異步調用過程。

爲了快速響應產品需求,因而本瓜迅速寫出瞭如下代碼:

/**
 * 新建流程
 * @param {*} appId
 * @param {*} tag
 */

export const handleGetIframeSrc = function(appId, tag) {
  let h5Id
// 第 1 步: 調用 sso 接口,獲取token
  getsingleSignOnToken({ formSource: tag }).then(data => { 
    return new Promise((resolve, reject) => {
      resolve(data.result)
    })
  }).then(token => { 
    const para = { appId: appId }
    return new Promise((resolve, reject) => {
// 第 2 步: 調用 create 接口,新建應用
      appH5create(para).then(res => {
// 第 3 步: 處理字符串,拼接 Url
        this.handleInsIframeUrl(res, token, appId)
        this.setH5Id(res.result.h5Id)
        h5Id = res.result.h5Id
        resolve(h5Id)
      }).catch(err => {
        this.$message({
          message: err.message || '出現錯誤',
          type: 'error'
        })
      })
    })
  }).then(h5Id => { 
// 第 4 步:創建 websocket 連接;
    return new Promise((resolve, reject) => {
      webSocketInit(resolve, reject, h5Id)
    })
  }).then(doclose => {
// 第 5 步:拿到 websocket 後端推送關鍵字,渲染頁面;
    if (doclose) { this.setShowEditLink({ appId: appId, h5Id: h5Id, state: true }) }
  }).catch(err => {
    this.$message({
      message: err.message || '出現錯誤',
      type: 'error'
    })
  })
}

const handleInsIframeUrl = function(res, token, appId) { 
// url 拼接
  const secretId = this.$store.state.userinfo.enterpriseList[0].secretId
  let editUrl = res.result.editUrl
  const infoId = editUrl.substr(editUrl.indexOf('?') + 1, editUrl.length - editUrl.indexOf('?'))
  editUrl = res.result.editUrl.replace(infoId, `from=a2p&${infoId}`)
  const headList = JSON.parse(JSON.stringify(this.headList))
  headList.forEach(i => {
    if (i.appId === appId) { i.srcUrl = `${editUrl}&token=${token}&secretId=${secretId}` }
  })
  this.setHeadList(headList)
}
複製代碼

這段代碼是很是天然地根據產品所提需求,而後本身理解所編寫。

其實還能夠,是吧?🐶

需求更新

但你不得不認可,程序員和產品之間有一條沒法逾越的溝通鴻溝

它大部分是由所站角度不一樣而產生,只能說:李姐李姐!

因此,基於前一個場景,需求發生了點 更新 ~

I6UGrz.th.png

除了上節所提的 【新建流程】 ,還要加一個 【編輯流程】 ╮(╯▽╰)╭

編輯流程簡單來講就是:砍掉新建流程的第 2 步調接口,再稍微調整傳參便可。

因而本瓜直接 copy 一下再做簡單刪改,不到 1 分鐘,編輯流程的代碼就誕生了~

/**
 * 編輯流程
 */
 
const handleToIframeEdit = function() { // 編輯 iframe
  const { editUrl, appId, h5Id } = this.ruleForm
// 第 1 步: 調用 sso 接口,獲取token
  getsingleSignOnToken({ formSource: 'ins' }).then(data => {
    return new Promise((resolve, reject) => {
      resolve(data.result)
    })
  }).then(token => { 
// 第 2 步:處理字符串,拼接 Url
    return new Promise((resolve, reject) => {
      const secretId = this.$store.state.userinfo.enterpriseList[0].secretId
      const infoId = editUrl.substr(editUrl.indexOf('?') + 1, editUrl.length - editUrl.indexOf('?'))
      const URL = editUrl.replace(infoId, `from=a2p&${infoId}`)
      const headList = JSON.parse(JSON.stringify(this.headList))
      headList.forEach(i => {
        if (i.appId === appId) { i.srcUrl = `${URL}&token=${token}&secretId=${secretId}` }
      })
      this.setHeadList(headList)
      this.setShowEditLink({ appId: appId, h5Id: h5Id, state: false })
      this.setShowNavIframe({ appId: appId, state: true })
      this.setNavLabel(this.headList.find(i => i.appId === appId).name)
      resolve(h5Id)
    })
  }).then(h5Id => {
// 第 3 步:創建 websocket 連接;
    return new Promise((resolve, reject) => {
      webSocketInit(resolve, reject, h5Id)
    })
  }).then(doclose => {
// 第 4 步:拿到 websocket 後端推送關鍵字,渲染頁面;
    if (doclose) { this.setShowEditLink({ appId: appId, h5Id: h5Id, state: true }) }
  }).catch(err => {
    this.$message({
      message: err.message || '出現錯誤',
      type: 'error'
    })
  })
}
複製代碼

需求再更新

老實講,不怪產品,咱作需求的過程也是逐步理解需求的過程。理解有變化,再正常不過!(#^.^#) 李姐李姐......

I6UIKu.th.png

上面已有兩個流程:新建流程、編輯流程

此次,要再加一個 從新建立流程 ~

從新建立流程可簡單理解爲:在新建流程以前調一個 delDraft 刪除草稿接口;

至此,咱們產生了三個流程:

  1. 新建流程;
  2. 編輯流程;
  3. 從新建立流程;

本瓜這裏做個簡單的腦圖示意邏輯:

I6Xi9Q.png

個人直覺告訴我:不能再 copy 一份新建流程做修改了,由於這樣就太拉了。。。沒錯,它沒有耦合,可是它也沒有內聚,這不是我想要的。因而,我開始封裝了......

實現上述腦圖的代碼:

/**
 * 判斷是否存在草稿記錄?
 */
judgeIfDraftExist(item) {
  const para = { appId: item.appId }
  return appH5ifDraftExist(para).then(res => {
    const { editUrl, h5Id, version } = res.result
    if (h5Id === -1) { // 不存在草稿
      this.handleGetIframeSrc(item)
    } else { // 存在草稿
      this.handleExitDraft(item, h5Id, version, editUrl)
    }
  }).catch(err => {
    console.log(err)
  })
},
/**
 * 選擇繼續編輯?
 */
handleExitDraft(item, h5Id, version, editUrl) {
  this.$confirm('有未完成的信息收集連接,是否繼續編輯?', '提示', {
    confirmButtonText: '繼續編輯',
    cancelButtonText: '從新建立',
    type: 'warning'
  }).then(() => {
    const editUrlH5Id = h5Id
    this.handleGetIframeSrc(item, editUrl, editUrlH5Id)
  }).catch(() => {
    this.handleGetIframeSrc(item)
    appH5delete({ h5Id: h5Id, version: version })
  })
},
/**
 * 新建流程、編輯流程、從新建立流程;
 */
handleGetIframeSrc(item, editUrl, editUrlH5Id) {
  let ws_h5Id
  getsingleSignOnToken({ formSource: item.tag }).then(data => { 
// 調用 sso 接口,拿到返回結果 res_token;
    return new Promise((resolve, reject) => {
      resolve(data.result)
    })
  }).then(token => {
    const para = { appId: item.appId }
    return new Promise((resolve, reject) => {
      if (!editUrl) { // 新建流程、從新建立流程
// 調用 create 接口,拿到返回結果 res_id;
        appH5create(para).then(res => {
// 處理字符串,拼接 Url;
          this.handleInsIframeUrl(res.result.editUrl, token, item.appId)
          this.setH5Id(res.result.h5Id)
          ws_h5Id = res.result.h5Id
          this.setShowNavIframe({ appId: item.appId, state: true })
          this.setNavLabel(item.name)
          resolve(true)
        }).catch(err => {
          this.$message({
            message: err.message || '出現錯誤',
            type: 'error'
          })
        })
      } else { // 編輯流程
        this.handleInsIframeUrl(editUrl, token, item.appId)
        this.setH5Id(editUrlH5Id)
        ws_h5Id = editUrlH5Id
        this.setShowNavIframe({ appId: item.appId, state: true })
        this.setNavLabel(item.name)
        resolve(true)
      }
    })
  }).then(() => { 
// 創建 websocket 連接;
    return new Promise((resolve, reject) => {
      webSocketInit(resolve, reject, ws_h5Id)
    })
  }).then(doclose => {
// 拿到 websocket 後端推送關鍵字,渲染頁面;
    if (doclose) { this.setShowEditLink({ appId: item.appId, h5Id: ws_h5Id, state: true }) }
  }).catch(err => {
    this.$message({
      message: err.message || '出現錯誤',
      type: 'error'
    })
  })
},

handleInsIframeUrl(editUrl, token, appId) {
// url 拼接
  const secretId = this.$store.state.userinfo.enterpriseList[0].secretId
  const infoId = editUrl.substr(editUrl.indexOf('?') + 1, editUrl.length - editUrl.indexOf('?'))
  const url = editUrl.replace(infoId, `from=a2p&${infoId}`)
  const headList = JSON.parse(JSON.stringify(this.headList))
  headList.forEach(i => {
    if (i.appId === appId) { i.srcUrl = `${url}&token=${token}&secretId=${secretId}` }
  })
  this.setHeadList(headList)
}
複製代碼

如此,咱們便將 新建流程、編輯流程、從新建立流程 所有整合到了上述代碼;

需求再再更新

上面的封裝看起來彷佛還不錯,可是這時我懼怕了!想到:若是這個時候,還要加流程或者改流程呢??? 我是打算繼續用 if...else 疊加在那個主函數裏面嗎?仍是打算直接 copy 一份再做刪改?

我都能碰見它會充斥着各類判斷,變量賦值、引用飛來飛去,最終成爲一坨💩,沒錯,代碼屎山的💩

我摸了摸左胸的左心房,它告訴我:「饒了接盤俠吧~」

因而乎,本瓜嘗試引進了以前吹那麼 nb 的函數式編程!它的能力就是讓代碼更可讀,這是我所須要的!來吧!!展現!!

I6cPMf.png

compose 函數

咱們在 《XDM,JS如何函數式編程?看這就夠了!(三)》 這篇講過函數組合 compose!沒錯,咱們此次就要用到這個傢伙!

還記得那句話嗎?

組合 ———— 聲明式數據流 ———— 是支撐函數式編程最重要的工具之一!

最基礎的 compose 函數是這樣的:

function compose(...fns) {
    return function composed(result){
        // 拷貝一份保存函數的數組
        var list = fns.slice();
        while (list.length > 0) {
            // 將最後一個函數從列表尾部拿出
            // 並執行它
            result = list.pop()( result );
        }
        return result;
    };
}

// ES6 箭頭函數形式寫法
var compose =
    (...fns) =>
        result => {
            var list = fns.slice();
            while (list.length > 0) {
                // 將最後一個函數從列表尾部拿出
                // 並執行它
                result = list.pop()( result );
            }
            return result;
        };
複製代碼

它能將一個函數調用的輸出路由跳轉到另外一個函數的調用上,而後一直進行下去。

I6c6uy.png

咱們不需關注黑盒子裏面作了什麼,只需關注:這個東西(函數)是什麼!它須要我輸入什麼!它的輸出又是什麼!

composePromise

但上面提到的 compose 函數是組合同步操做,而在本篇的實戰中,咱們須要組合是異步函數!

因而它被改形成這樣:

/**
 * @param  {...any} args
 * @returns
 */

export const composePromise = function(...args) {
  const init = args.pop()
  return function(...arg) {
    return args.reverse().reduce(function(sequence, func) {
      return sequence.then(function(result) {
        // eslint-disable-next-line no-useless-call
        return func.call(null, result)
      })
    }, Promise.resolve(init.apply(null, arg)))
  }
}
複製代碼

原理:Promise 能夠指定一個 sequence,來規定一個執行 then 的過程,then 函數會等到執行完成後,再執行下一個 then 的處理。啓動sequence 可使用 Promise.resolve() 這個函數。構建 sequence 可使用 reduce 。

咱們再寫一個小測試在控制檯跑一下!

let compose = function(...args) {
  const init = args.pop()
  return function(...arg) {
    return args.reverse().reduce(function(sequence, func) {
      return sequence.then(function(result) {
        return func.call(null, result)
      })
    }, Promise.resolve(init.apply(null, arg)))
  }
}

let a = async() => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('xhr1')
      resolve('xhr1')
    }, 5000)
  })
}

let b = async() => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('xhr2')
      resolve('xhr2')
    }, 3000)
  })
}
let steps = [a, b] // 從右向左執行
let composeFn = compose(...steps)

composeFn().then(res => { console.log(666) })

// xhr2
// xhr1
// 666
複製代碼

它會先執行 b ,3 秒後輸出 "xhr2",再執行 a,5 秒後輸出 "xhr1",最後輸出 666

你也能夠在控制檯帶參 debugger 試試,頗有意思:

composeFn(1, 2).then(res => { console.log(66) })
複製代碼

逐漸美麗起來

測試經過!藉助上面 composePromise 函數,咱們更加有信心用函數式編程 composePromise 重構 咱們的代碼了。

  • 實際上,這個過程一點不費力~

實現以下:

/**
 * 判斷是否存在草稿記錄?
 */
handleJudgeIfDraftExist(item) {
    return appH5ifDraftExist({ appId: item.appId }).then(res => {
      const { editUrl, h5Id, version } = res.result
      h5Id === -1 ? this.compose_newAppIframe(item) : this.hasDraftConfirm(item, h5Id, editUrl, version)
    }).catch(err => {
      console.log(err)
    })
},
/**
 * 選擇繼續編輯?
 */
hasDraftConfirm(item, h5Id, editUrl, version) {
    this.$confirm('有未完成的信息收集連接,是否繼續編輯?', '提示', {
      confirmButtonText: '繼續編輯',
      cancelButtonText: '從新建立',
      type: 'warning'
    }).then(() => {
      this.compose_editAppIframe(item, h5Id, editUrl)
    }).catch(() => {
      this.compose_reNewAppIframe(item, h5Id, version)
    })
},
複製代碼

敲黑板啦!畫重點啦!

/**
* 新建應用流程
* 入參: item
* 輸出:item
*/
compose_newAppIframe(...args) {
    const steps = [this.step_getDoclose, this.step_createWs, this.step_splitUrl, this.step_appH5create, this.step_getsingleSignOnToken]
    const handleCompose = composePromise(...steps)
    handleCompose(...args)
},
/**
* 編輯應用流程
* 入參: item, draftH5Id, editUrl
* 輸出:item
*/
compose_editAppIframe(...args) {
    const steps = [this.step_getDoclose, this.step_createWs, this.step_splitUrl, this.step_getsingleSignOnToken]
    const handleCompose = composePromise(...steps)
    handleCompose(...args)
},
/**
* 從新建立流程
* 入參: item,draftH5Id,version
* 輸出:item
*/
compose_reNewAppIframe(...args) {
    const steps = [this.step_getDoclose, this.step_createWs, this.step_splitUrl, this.step_appH5create, this.step_getsingleSignOnToken, this.step_delDraftH5Id]
    const handleCompose = composePromise(...steps)
    handleCompose(...args)
},
複製代碼

咱們經過 composePromise 執行不一樣的 steps,來依次執行(從右至左)裏面的功能函數;你能夠任意組合、增刪或修改 steps 的子項,也能夠任意組合出新的流程來應付產品。而且,它們都被封裝在 compose_xxx 裏面,相互獨立,不會干擾外界其它流程。同時,傳參也是很是清晰的,輸入是什麼!輸出又是什麼!一目瞭然!

對照腦圖再看此段代碼,不正是對咱們需求實現的最好詮釋嗎?

對於一個閱讀陌生代碼的人來講,你得先告訴他邏輯是怎樣的,而後再告訴他每一個步驟的內部具體實現。這樣纔是合理的!

I6Xi9Q.png

功能函數(具體步驟內部實現):

/**
* 調用 sso 接口,拿到返回結果 res_token;
*/
step_getsingleSignOnToken(...args) {
    const [item] = args.flat(Infinity)
    return new Promise((resolve, reject) => {
      getsingleSignOnToken({ formSource: item.tag }).then(data => {
        resolve([...args, data.result]) // data.result 即 token
      })
    })
},
/**
*  調用 create 接口,拿到返回結果 res_id;
*/
step_appH5create(...args) {
    const [item, token] = args.flat(Infinity)
    return new Promise((resolve, reject) => {
      appH5create({ appId: item.appId }).then(data => {
        resolve([item, data.result.h5Id, data.result.editUrl, token])
      }).catch(err => {
        this.$message({
          message: err.message || '出現錯誤',
          type: 'error'
        })
      })
    })
},
/**
* 調 delDraft 刪除接口;
*/
step_delDraftH5Id(...args) {
    const [item, h5Id, version] = args.flat(Infinity)
    return new Promise((resolve, reject) => {
      appH5delete({ h5Id: h5Id, version: version }).then(data => {
        resolve(...args)
      })
    })
},
/**
*  處理字符串,拼接 Url;
*/
step_splitUrl(...args) {
    const [item, h5Id, editUrl, token] = args.flat(Infinity)
    const infoId = editUrl.substr(editUrl.indexOf('?') + 1, editUrl.length - editUrl.indexOf('?'))
    const url = editUrl.replace(infoId, `from=a2p&${infoId}`)
    const headList = JSON.parse(JSON.stringify(this.headList))
    headList.forEach(i => {
      if (i.appId === item.appId) { i.srcUrl = `${url}&token=${token}` }
    })
    this.setHeadList(headList)
    this.setH5Id(h5Id)
    this.setShowNavIframe({ appId: item.appId, state: true })
    this.setNavLabel(item.name)
    return [...args]
},
/**
*  創建 websocket 連接;
*/
step_createWs(...args) {
    return new Promise((resolve, reject) => {
      webSocketInit(resolve, reject, ...args) 
})
  },
/**
*  拿到 websocket 後端推送關鍵字,渲染頁面;
*/
step_getDoclose(...args) {
    const [item, h5Id, editUrl, token, doclose] = args.flat(Infinity)
    if (doclose) { this.setShowEditLink({ appId: item.appId, h5Id: h5Id, state: true }) }
    return new Promise((resolve, reject) => {
      resolve(true)
    })
},
複製代碼

功能函數的輸入、輸出也是清晰可見的。

至此,咱們能夠認爲:藉助 compose 函數,藉助函數式編程,咱把業務需求流程進行了封裝,明確了輸入輸出,讓咱們的代碼更加可讀了!可擴展性也更高了!這不就是高內聚、低耦合?!

I6UWZD.th.png

階段總結

你問我什麼是 JS 函數式編程實戰?我只能說本篇徹底就是出自工做中的實戰!!!

這樣致使本篇代碼量可能有點多,可是這就是實打實的需求變化,代碼迭代、改造的過程。(建議通篇把握、理解)

固然,這不是終點,代碼重構這個過程應該是每時每刻都在進行着。

對於函數式編程,簡單應用 compose 函數,這也只是一個起點!

已經講過,偏函數、函數柯里化、函數組合、數組操做、時間狀態、函數式編程庫等等概念......咱們將再接再礪得使用它們,把代碼屎山進行分類、打包、清理!讓它不斷美麗起來!💩 => 👩‍🦰

以上,即是本次分享~ 都看到這裏,不如點個贊吧👍👍👍

謝謝支持~

我是掘金安東尼,輸出暴露輸入,技術洞見生活!下次再會~

相關文章
相關標籤/搜索