使用compose函數(shù)優(yōu)化代碼提高可讀性及擴(kuò)展性
前言
本瓜知道前不久寫的《JS 如何函數(shù)式編程》系列各位可能并不感冒,因?yàn)橐磺欣碚摰臇|西如果脫離實(shí)戰(zhàn)的話,那就將毫無意義。
于是乎,本瓜著手于實(shí)際工作開發(fā),嘗試應(yīng)用函數(shù)式編程的一些思想。
最終驚人的發(fā)現(xiàn):這個(gè)實(shí)現(xiàn)過程并不難,但是效果卻不??!
實(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)!?。?/p>
這樣導(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