「本文已參與好文召集令活動,點擊查看:後端、大前端雙賽道投稿,2萬元獎池等你挑戰!」前端
本瓜知道前不久寫的《JS 如何函數式編程》系列各位可能並不感冒,由於一切理論的東西若是脫離實戰的話,那就將毫無心義。程序員
因而乎,本瓜着手於實際工做開發,嘗試應用函數式編程的一些思想。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)
}
複製代碼
這段代碼是很是天然地根據產品所提需求,而後本身理解所編寫。
其實還能夠,是吧?🐶
但你不得不認可,程序員和產品之間有一條沒法逾越的溝通鴻溝。
它大部分是由所站角度不一樣而產生,只能說:李姐李姐!
因此,基於前一個場景,需求發生了點 更新 ~
除了上節所提的 【新建流程】 ,還要加一個 【編輯流程】 ╮(╯▽╰)╭
編輯流程簡單來講就是:砍掉新建流程的第 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'
})
})
}
複製代碼
老實講,不怪產品,咱作需求的過程也是逐步理解需求的過程。理解有變化,再正常不過!(#^.^#) 李姐李姐......
上面已有兩個流程:新建流程、編輯流程。
此次,要再加一個 從新建立流程 ~
從新建立流程可簡單理解爲:在新建流程以前調一個 delDraft 刪除草稿接口;
至此,咱們產生了三個流程:
本瓜這裏做個簡單的腦圖示意邏輯:
個人直覺告訴我:不能再 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 的函數式編程!它的能力就是讓代碼更可讀,這是我所須要的!來吧!!展現!!
咱們在 《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;
};
複製代碼
它能將一個函數調用的輸出路由跳轉到另外一個函數的調用上,而後一直進行下去。
咱們不需關注黑盒子裏面作了什麼,只需關注:這個東西(函數)是什麼!它須要我輸入什麼!它的輸出又是什麼!
但上面提到的 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 裏面,相互獨立,不會干擾外界其它流程。同時,傳參也是很是清晰的,輸入是什麼!輸出又是什麼!一目瞭然!
對照腦圖再看此段代碼,不正是對咱們需求實現的最好詮釋嗎?
對於一個閱讀陌生代碼的人來講,你得先告訴他邏輯是怎樣的,而後再告訴他每一個步驟的內部具體實現。這樣纔是合理的!
功能函數(具體步驟內部實現):
/**
* 調用 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 函數,藉助函數式編程,咱把業務需求流程進行了封裝,明確了輸入輸出,讓咱們的代碼更加可讀了!可擴展性也更高了!這不就是高內聚、低耦合?!
你問我什麼是 JS 函數式編程實戰?我只能說本篇徹底就是出自工做中的實戰!!!
這樣致使本篇代碼量可能有點多,可是這就是實打實的需求變化,代碼迭代、改造的過程。(建議通篇把握、理解)
固然,這不是終點,代碼重構這個過程應該是每時每刻都在進行着。
對於函數式編程,簡單應用 compose 函數,這也只是一個起點!
已經講過,偏函數、函數柯里化、函數組合、數組操做、時間狀態、函數式編程庫等等概念......咱們將再接再礪得使用它們,把代碼屎山進行分類、打包、清理!讓它不斷美麗起來!💩 => 👩🦰
以上,即是本次分享~ 都看到這裏,不如點個贊吧👍👍👍
謝謝支持~
我是掘金安東尼,輸出暴露輸入,技術洞見生活!下次再會~