注册

感谢 compose 函数,让我的代码屎山?逐渐美丽了起来~

有言在先


本瓜知道前不久写的《JS 如何函数式编程》系列各位可能并不感冒,因为一切理论的东西如果脱离实战的话,那就将毫无意义。


I6cDpC.th.png


于是乎,本瓜着手于实际工作开发,尝试应用函数式编程的一些思想。


最终惊人的发现:这个实现过程并不难,但是效果却不小!


实现思路:借助 compose 函数对连续的异步过程进行组装,不同的组合方式实现不同的业务流程。


这样不仅提高了代码的可读性,还提高了代码的扩展性。我想:这也许就是高内聚、低耦合吧~


撰此篇记之,并与各位分享。


场景说明


在和产品第一次沟通了需求后,我理解需要实现一个应用 新建流程,具体是这样的:


第 1 步:调用 sso 接口,拿到返回结果 res_token;


第 2 步:调用 create 接口,拿到返回结果 res_id;


第 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 函数,这也只是一个起点!


已经讲过,偏函数、函数柯里化、函数组合、数组操作、时间状态、函数式编程库等等概念......我们将再接再厉得使用它们,把代码屎山进行分类、打包、清理!让它不断美丽起来!💩 => 👩‍🦰


以上,便是本次分享~ 都看到这里,不如点个赞吧👍👍👍


链接:https://juejin.cn/post/6989020415444123662

0 个评论

要回复文章请先登录注册