使用compose函數(shù)優(yōu)化代碼提高可讀性及擴(kuò)展性
前言
本瓜知道前不久寫的《JS 如何函數(shù)式編程》系列各位可能并不感冒,因?yàn)橐磺欣碚摰臇|西如果脫離實(shí)戰(zhàn)的話,那就將毫無意義。
于是乎,本瓜著手于實(shí)際工作開發(fā),嘗試應(yīng)用函數(shù)式編程的一些思想。
最終驚人的發(fā)現(xiàn):這個(gè)實(shí)現(xiàn)過程并不難,但是效果卻不?。?/p>
實(shí)現(xiàn)思路:借助 compose 函數(shù)對(duì)連續(xù)的異步過程進(jìn)行組裝,不同的組合方式實(shí)現(xiàn)不同的業(yè)務(wù)流程。
這樣不僅提高了代碼的可讀性,還提高了代碼的擴(kuò)展性。我想:這也許就是高內(nèi)聚、低耦合吧~
撰此篇記之,并與各位分享。
場(chǎng)景說明
在和產(chǎn)品第一次溝通了需求后,我理解需要實(shí)現(xiàn)一個(gè)應(yīng)用 新建流程,具體是這樣的:
- 第 1 步:調(diào)用 sso 接口,拿到返回結(jié)果 res_token;
- 第 2 步:調(diào)用 create 接口,拿到返回結(jié)果 res_id;
- 第 3 步:處理字符串,拼接 Url;
- 第 4 步:建立 websocket 鏈接;
- 第 5 步:拿到 websocket 后端推送關(guān)鍵字,渲染頁面;
注:接口、參數(shù)有做一定簡(jiǎn)化
上面除了第 3 步、第 5 步,剩下的都是要調(diào)接口的,并且前后步驟都有傳參的需要,可以理解為一個(gè)連續(xù)且有序的異步調(diào)用過程。
為了快速響應(yīng)產(chǎn)品需求,于是本瓜迅速寫出了以下代碼:
/**
* 新建流程
* @param {*} appId
* @param {*} tag
*/
export const handleGetIframeSrc = function(appId, tag) {
let h5Id
// 第 1 步: 調(diào)用 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 步: 調(diào)用 create 接口,新建應(yīng)用
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 || '出現(xiàn)錯(cuò)誤',
type: 'error'
})
})
})
}).then(h5Id => {
// 第 4 步:建立 websocket 鏈接;
return new Promise((resolve, reject) => {
webSocketInit(resolve, reject, h5Id)
})
}).then(doclose => {
// 第 5 步:拿到 websocket 后端推送關(guān)鍵字,渲染頁面;
if (doclose) { this.setShowEditLink({ appId: appId, h5Id: h5Id, state: true }) }
}).catch(err => {
this.$message({
message: err.message || '出現(xiàn)錯(cuò)誤',
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)
}
這段代碼是非常自然地根據(jù)產(chǎn)品所提需求,然后自己理解所編寫。
其實(shí)還可以,是吧???
需求更新
但你不得不承認(rèn),程序員和產(chǎn)品之間有一條無法逾越的溝通鴻溝。
它大部分是由所站角度不同而產(chǎn)生,只能說:李姐李姐!
所以,基于前一個(gè)場(chǎng)景,需求發(fā)生了點(diǎn) 更新 ~
除了上節(jié)所提的 【新建流程】 ,還要加一個(gè) 【編輯流程】 ╮(╯▽╰)╭
編輯流程簡(jiǎn)單來說就是:砍掉新建流程的第 2 步調(diào)接口,再稍微調(diào)整傳參即可。
于是本瓜直接 copy 一下再作簡(jiǎn)單刪改,不到 1 分鐘,編輯流程的代碼就誕生了~
/**
* 編輯流程
*/
const handleToIframeEdit = function() { // 編輯 iframe
const { editUrl, appId, h5Id } = this.ruleForm
// 第 1 步: 調(diào)用 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 后端推送關(guān)鍵字,渲染頁面;
if (doclose) { this.setShowEditLink({ appId: appId, h5Id: h5Id, state: true }) }
}).catch(err => {
this.$message({
message: err.message || '出現(xiàn)錯(cuò)誤',
type: 'error'
})
})
}
需求再更新
老實(shí)講,不怪產(chǎn)品,咱做需求的過程也是逐步理解需求的過程。理解有變化,再正常不過!(#^.^#) 李姐李姐......
上面已有兩個(gè)流程:新建流程、編輯流程。
這次,要再加一個(gè) 重新創(chuàng)建流程 ~
重新創(chuàng)建流程可簡(jiǎn)單理解為:在新建流程之前調(diào)一個(gè) delDraft 刪除草稿接口;
至此,我們產(chǎn)生了三個(gè)流程:
- 新建流程;
- 編輯流程;
- 重新創(chuàng)建流程;
本瓜這里作個(gè)簡(jiǎn)單的腦圖示意邏輯:

我的直覺告訴我:不能再 copy 一份新建流程作修改了,因?yàn)檫@樣就太拉了。。。沒錯(cuò),它沒有耦合,但是它也沒有內(nèi)聚,這不是我想要的。于是,我開始封裝了......
實(shí)現(xiàn)上述腦圖的代碼:
/**
* 判斷是否存在草稿記錄?
*/
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)
})
},
/**
* 選擇繼續(xù)編輯?
*/
handleExitDraft(item, h5Id, version, editUrl) {
this.$confirm('有未完成的信息收集鏈接,是否繼續(xù)編輯?', '提示', {
confirmButtonText: '繼續(xù)編輯',
cancelButtonText: '重新創(chuàng)建',
type: 'warning'
}).then(() => {
const editUrlH5Id = h5Id
this.handleGetIframeSrc(item, editUrl, editUrlH5Id)
}).catch(() => {
this.handleGetIframeSrc(item)
appH5delete({ h5Id: h5Id, version: version })
})
},
/**
* 新建流程、編輯流程、重新創(chuàng)建流程;
*/
handleGetIframeSrc(item, editUrl, editUrlH5Id) {
let ws_h5Id
getsingleSignOnToken({ formSource: item.tag }).then(data => {
// 調(diào)用 sso 接口,拿到返回結(jié)果 res_token;
return new Promise((resolve, reject) => {
resolve(data.result)
})
}).then(token => {
const para = { appId: item.appId }
return new Promise((resolve, reject) => {
if (!editUrl) { // 新建流程、重新創(chuàng)建流程
// 調(diào)用 create 接口,拿到返回結(jié)果 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 || '出現(xiàn)錯(cuò)誤',
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 后端推送關(guān)鍵字,渲染頁面;
if (doclose) { this.setShowEditLink({ appId: item.appId, h5Id: ws_h5Id, state: true }) }
}).catch(err => {
this.$message({
message: err.message || '出現(xiàn)錯(cuò)誤',
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)
}
如此,我們便將 新建流程、編輯流程、重新創(chuàng)建流程 全部整合到了上述代碼;
需求再再更新
上面的封裝看起來似乎還不錯(cuò),但是這時(shí)我害怕了!想到:如果這個(gè)時(shí)候,還要加流程或者改流程呢??? 我是打算繼續(xù)用 if...else 疊加在那個(gè)主函數(shù)里面嗎?還是打算直接 copy 一份再作刪改?
我都能遇見它會(huì)充斥著各種判斷,變量賦值、引用飛來飛去,最終成為一坨??,沒錯(cuò),代碼屎山的??
我摸了摸左胸的左心房,它告訴我:“饒了接盤俠吧~”
于是乎,本瓜嘗試引進(jìn)了之前吹那么 nb 的函數(shù)式編程!它的能力就是讓代碼更可讀,這是我所需要的!來吧!!展示!!
compose 函數(shù)
我們?cè)?《XDM,JS如何函數(shù)式編程?看這就夠了?。ㄈ?/a> 這篇講過函數(shù)組合 compose!沒錯(cuò),我們這次就要用到這個(gè)家伙!
還記得那句話嗎?
組合 ———— 聲明式數(shù)據(jù)流 ———— 是支撐函數(shù)式編程最重要的工具之一!
最基礎(chǔ)的 compose 函數(shù)是這樣的:
function compose(...fns) {
return function composed(result){
// 拷貝一份保存函數(shù)的數(shù)組
var list = fns.slice();
while (list.length > 0) {
// 將最后一個(gè)函數(shù)從列表尾部拿出
// 并執(zhí)行它
result = list.pop()( result );
}
return result;
};
}
// ES6 箭頭函數(shù)形式寫法
var compose =
(...fns) =>
result => {
var list = fns.slice();
while (list.length > 0) {
// 將最后一個(gè)函數(shù)從列表尾部拿出
// 并執(zhí)行它
result = list.pop()( result );
}
return result;
};
它能將一個(gè)函數(shù)調(diào)用的輸出路由跳轉(zhuǎn)到另一個(gè)函數(shù)的調(diào)用上,然后一直進(jìn)行下去。

我們不需關(guān)注黑盒子里面做了什么,只需關(guān)注:這個(gè)東西(函數(shù))是什么!它需要我輸入什么!它的輸出又是什么!
composePromise
但上面提到的 compose 函數(shù)是組合同步操作,而在本篇的實(shí)戰(zhàn)中,我們需要組合是異步函數(shù)!
于是它被改造成這樣:
/**
* @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 可以指定一個(gè) sequence,來規(guī)定一個(gè)執(zhí)行 then 的過程,then 函數(shù)會(huì)等到執(zhí)行完成后,再執(zhí)行下一個(gè) then 的處理。啟動(dòng)sequence 可以使用 Promise.resolve() 這個(gè)函數(shù)。構(gòu)建 sequence 可以使用 reduce 。
我們?cè)賹懸粋€(gè)小測(cè)試在控制臺(tái)跑一下!
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] // 從右向左執(zhí)行
let composeFn = compose(...steps)
composeFn().then(res => { console.log(666) })
// xhr2
// xhr1
// 666
它會(huì)先執(zhí)行 b ,3 秒后輸出 "xhr2",再執(zhí)行 a,5 秒后輸出 "xhr1",最后輸出 666
你也可以在控制臺(tái)帶參 debugger 試試,很有意思:
composeFn(1, 2).then(res => { console.log(66) })
逐漸美麗起來
測(cè)試通過!借助上面 composePromise 函數(shù),我們更加有信心用函數(shù)式編程 composePromise 重構(gòu) 我們的代碼了。
實(shí)際上,這個(gè)過程一點(diǎn)不費(fèi)力~
實(shí)現(xiàn)如下:
/**
* 判斷是否存在草稿記錄?
*/
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)
})
},
/**
* 選擇繼續(xù)編輯?
*/
hasDraftConfirm(item, h5Id, editUrl, version) {
this.$confirm('有未完成的信息收集鏈接,是否繼續(xù)編輯?', '提示', {
confirmButtonText: '繼續(xù)編輯',
cancelButtonText: '重新創(chuàng)建',
type: 'warning'
}).then(() => {
this.compose_editAppIframe(item, h5Id, editUrl)
}).catch(() => {
this.compose_reNewAppIframe(item, h5Id, version)
})
},
敲黑板啦!畫重點(diǎn)啦!
/**
* 新建應(yīng)用流程
* 入?yún)? 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)
},
/**
* 編輯應(yīng)用流程
* 入?yún)? 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)
},
/**
* 重新創(chuàng)建流程
* 入?yún)? 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 執(zhí)行不同的 steps,來依次執(zhí)行(從右至左)里面的功能函數(shù);你可以任意組合、增刪或修改 steps 的子項(xiàng),也可以任意組合出新的流程來應(yīng)付產(chǎn)品。并且,它們都被封裝在 compose_xxx 里面,相互獨(dú)立,不會(huì)干擾外界其它流程。同時(shí),傳參也是非常清晰的,輸入是什么!輸出又是什么!一目了然!
對(duì)照腦圖再看此段代碼,不正是對(duì)我們需求實(shí)現(xiàn)的最好詮釋嗎?
對(duì)于一個(gè)閱讀陌生代碼的人來說,你得先告訴他邏輯是怎樣的,然后再告訴他每個(gè)步驟的內(nèi)部具體實(shí)現(xiàn)。這樣才是合理的!

功能函數(shù)(具體步驟內(nèi)部實(shí)現(xiàn)):
/**
* 調(diào)用 sso 接口,拿到返回結(jié)果 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
})
})
},
/**
* 調(diào)用 create 接口,拿到返回結(jié)果 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 || '出現(xiàn)錯(cuò)誤',
type: 'error'
})
})
})
},
/**
* 調(diào) 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 后端推送關(guān)鍵字,渲染頁面;
*/
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)
})
},
功能函數(shù)的輸入、輸出也是清晰可見的。
至此,我們可以認(rèn)為:借助 compose 函數(shù),借助函數(shù)式編程,咱把業(yè)務(wù)需求流程進(jìn)行了封裝,明確了輸入輸出,讓我們的代碼更加可讀了!可擴(kuò)展性也更高了!這不就是高內(nèi)聚、低耦合?!
階段總結(jié)
你問我什么是 JS 函數(shù)式編程實(shí)戰(zhàn)?我只能說本篇完全就是出自工作中的實(shí)戰(zhàn)!??!
這樣導(dǎo)致本篇代碼量可能有點(diǎn)多,但是這就是實(shí)打?qū)嵉男枨笞兓?,代碼迭代、改造的過程。(建議通篇把握、理解)
當(dāng)然,這不是終點(diǎn),代碼重構(gòu)這個(gè)過程應(yīng)該是每時(shí)每刻都在進(jìn)行著。
對(duì)于函數(shù)式編程,簡(jiǎn)單應(yīng)用 compose 函數(shù),這也只是一個(gè)起點(diǎn)!
已經(jīng)講過,偏函數(shù)、函數(shù)柯里化、函數(shù)組合、數(shù)組操作、時(shí)間狀態(tài)、函數(shù)式編程庫等等概念......我們將再接再厲得使用它們,把代碼屎山進(jìn)行分類、打包、清理!讓它不斷美麗起來
更多關(guān)于compose優(yōu)化代碼可讀性擴(kuò)展性的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
JavaScript專題之underscore防抖實(shí)例學(xué)習(xí)
這篇文章主要為大家介紹了JavaScript專題之underscore防抖實(shí)例學(xué)習(xí),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-09-09
微信小程序 action-sheet 反饋上拉菜單簡(jiǎn)單實(shí)例
這篇文章主要介紹了微信小程序 action-sheet 反饋上拉菜單簡(jiǎn)單實(shí)例的相關(guān)資料,需要的朋友可以參考下2017-05-05
定時(shí)器在頁面最小化時(shí)不執(zhí)行實(shí)現(xiàn)示例
這篇文章主要為大家介紹了定時(shí)器在頁面最小化時(shí)不執(zhí)行的實(shí)現(xiàn)示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-07-07
利用js實(shí)現(xiàn)簡(jiǎn)單開關(guān)燈代碼
這篇文章主要分享的是如何利用js實(shí)現(xiàn)簡(jiǎn)單開關(guān)燈代碼,下面文字圍繞js實(shí)現(xiàn)簡(jiǎn)單開關(guān)燈的相關(guān)資料展開具體內(nèi)容,需要的朋友可以參考以下,希望對(duì)大家又所幫助2021-11-11

