使用compose函数优化代码提高可读性及扩展性
目录
- 前言
- 场景说明
- 需求更新
- 需求再更新
- 需求再再更新
- compose 函数
- composePromise
- 逐渐美丽起来
- 阶段总结
前言 本瓜知道前不久写的《JS 如何函数式编程》系列各位可能并不感冒,因为一切理论的东西如果脱离实战的话,那就将毫无意义。
于是乎,本瓜着手于实际工作开发,尝试应用函数式编程的一些思想。
最终惊人的发现:这个实现过程并不难,但是效果却不小!
实现思路:借助 compose 函数对连续的异步过程进行组装,不同的组合方式实现不同的业务流程。
这样不仅提高了代码的可读性,还提高了代码的扩展性。我想:这也许就是高内聚、低耦合吧~
撰此篇记之,并与各位分享。
场景说明 在和产品第一次沟通了需求后,我理解需要实现一个应用 新建流程,具体是这样的:
第 1 步:调用 sso 接口,拿到返回结果 res_token;
第 2 步:调用 create 接口,拿到返回结果 res_id;
第 3 步:处理字符串,拼接 Url;
第 4 步:建立 websocket 链接;
第 5 步:拿到 websocket 后端推送关键字,渲染页面;
- 注:接口、参数有做一定简化
为了快速响应产品需求,于是本瓜迅速写出了以下代码:
/** * 新建流程 * @param {*} appId * @param {*} tag */export const handleGetIframeSrc = https://www.it610.com/article/function(appId, tag) {let h5Id// 第 1 步: 调用 sso 接口,获取tokengetsingleSignOnToken({ 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 步: 处理字符串,拼接 Urlthis.handleInsIframeUrl(res, token, appId)this.setH5Id(res.result.h5Id)h5Id = res.result.h5Idresolve(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].secretIdlet editUrl = res.result.editUrlconst 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() { // 编辑 iframeconst { editUrl, appId, h5Id } = this.ruleForm// 第 1 步: 调用 sso 接口,获取tokengetsingleSignOnToken({ formSource: 'ins' }).then(data =https://www.it610.com/article/> {return new Promise((resolve, reject) => {resolve(data.result)})}).then(token => { // 第 2 步:处理字符串,拼接 Urlreturn new Promise((resolve, reject) => {const secretId = this.$store.state.userinfo.enterpriseList[0].secretIdconst 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.resultif (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 = h5Idthis.handleGetIframeSrc(item, editUrl, editUrlH5Id)}).catch(() => {this.handleGetIframeSrc(item)appH5delete({ h5Id: h5Id, version: version })})},/** * 新建流程、编辑流程、重新创建流程; */handleGetIframeSrc(item, editUrl, editUrlH5Id) {let ws_h5IdgetsingleSignOnToken({ formSource: item.tag }).then(data =https://www.it610.com/article/> { // 调用 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.h5Idthis.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 = editUrlH5Idthis.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].secretIdconst 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 的函数式编程!它的能力就是让代码更可读,这是我所需要的!来吧!!展示!!
compose 函数 【使用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; };
它能将一个函数调用的输出路由跳转到另一个函数的调用上,然后一直进行下去。
文章图片
我们不需关注黑盒子里面做了什么,只需关注:这个东西(函数)是什么!它需要我输入什么!它的输出又是什么!
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-callreturn 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.resulth5Id === -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 =https://www.it610.com/article/> {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 =https://www.it610.com/article/> {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 函数,这也只是一个起点!
已经讲过,偏函数、函数柯里化、函数组合、数组操作、时间状态、函数式编程库等等概念......我们将再接再厉得使用它们,把代码屎山进行分类、打包、清理!让它不断美丽起来
更多关于compose优化代码可读性扩展性的资料请关注脚本之家其它相关文章!
推荐阅读
- VMware vSphere Replication 8.5部署及使用教程
- 微信小程序使用|微信小程序使用 ECharts
- grub 命令使用
- SpringBoot中使用HTTP客户端工具Retrofit
- 在vue项目如何使用base64加密
- vue华视电子身份证阅读器的使用
- pandas100个骚操作|pandas100个骚操作五(使用 explode 实现 pandas 列转行的 2 个常用技巧)
- [ C语言 ] 还不懂指针的一定要进来,带你初识指针,简单使用指针,它没有你想的那么难。
- 前端相关|JS中的类,函数,对象、原型
- 前端面试必知(JS)|【前端面试必知】ES6中函数新增了哪些扩展