使用compose函数优化代码提高可读性及扩展性


Posted in Javascript onJune 16, 2022

前言

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

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

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

实现思路:借助 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)
}

这段代码是非常自然地根据产品所提需求,然后自己理解所编写。

其实还可以,是吧??

需求更新

但你不得不承认,程序员和产品之间有一条无法逾越的沟通鸿沟。

它大部分是由所站角度不同而产生,只能说:李姐李姐!

所以,基于前一个场景,需求发生了点 更新 ~

除了上节所提的 【新建流程】 ,还要加一个 【编辑流程】 ╮(╯▽╰)╭

编辑流程简单来说就是:砍掉新建流程的第 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 删除草稿接口;

至此,我们产生了三个流程:

  • 新建流程;
  • 编辑流程;
  • 重新创建流程;

本瓜这里作个简单的脑图示意逻辑:

使用compose函数优化代码提高可读性及扩展性

我的直觉告诉我:不能再 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 的函数式编程!它的能力就是让代码更可读,这是我所需要的!来吧!!展示!!

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;
        };

它能将一个函数调用的输出路由跳转到另一个函数的调用上,然后一直进行下去。

使用compose函数优化代码提高可读性及扩展性

我们不需关注黑盒子里面做了什么,只需关注:这个东西(函数)是什么!它需要我输入什么!它的输出又是什么!

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 里面,相互独立,不会干扰外界其它流程。同时,传参也是非常清晰的,输入是什么!输出又是什么!一目了然!

对照脑图再看此段代码,不正是对我们需求实现的最好诠释吗?

对于一个阅读陌生代码的人来说,你得先告诉他逻辑是怎样的,然后再告诉他每个步骤的内部具体实现。这样才是合理的!

使用compose函数优化代码提高可读性及扩展性

功能函数(具体步骤内部实现):

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

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

更多关于compose优化代码可读性扩展性的资料请关注三水点靠木其它相关文章!


Tags in this post...

Javascript 相关文章推荐
Jquery显示、隐藏元素以及添加删除样式
Aug 09 Javascript
Jquery操作radio的简单实例
Jan 06 Javascript
js实现文本框中焦点在最后位置
Mar 04 Javascript
JS实现灵巧的下拉导航效果代码
Aug 25 Javascript
JS判断来路是否是百度等搜索索引进行弹窗或自动跳转的实现代码
Oct 09 Javascript
COM组件中调用JavaScript函数详解及实例
Feb 23 Javascript
jquery实现提示语淡入效果
May 05 jQuery
bootstrap3中container与container_fluid外层容器的区别讲解
Dec 04 Javascript
JavaScript使用表单元素验证表单的示例代码
Aug 20 Javascript
JavaScript RegExp 对象用法详解
Sep 24 Javascript
关于JS模块化的知识点分享
Oct 16 Javascript
js实现盒子移动动画效果
Aug 09 Javascript
html中两种获取标签内的值的方法
Jun 16 #jQuery
JavaScript前端面试扁平数据转tree与tree数据扁平化
Jun 14 #Javascript
vue如何在data中引入图片的正确路径
Jun 05 #Vue.js
Vue Mint UI mt-swipe的使用方式
Jun 05 #Vue.js
vue @ ~ 相对路径 路径别名设置方式
Jun 05 #Vue.js
vue css 相对路径导入问题级踩坑记录
Jun 05 #Vue.js
vue中data里面的数据相互使用方式
Jun 05 #Vue.js
You might like
PHP的中使用非缓冲模式查询数据库的方法
2017/02/05 PHP
HTML 自动伸缩的表格Table js实现
2009/04/01 Javascript
使用JQuery和s3captche实现一个水果名字的验证
2009/08/14 Javascript
JavaScript实现简单的时钟实例代码
2013/11/23 Javascript
jquery map方法使用示例
2014/04/23 Javascript
jQuery函数map()和each()介绍及异同点分析
2014/11/08 Javascript
jQuery带时间的日期控件代码分享
2015/08/26 Javascript
详解照片瀑布流效果(js,jquery分别实现与知识点总结)
2017/01/01 Javascript
JS+HTML5实现图片在线预览功能
2017/07/22 Javascript
vue.js的computed,filter,get,set的用法及区别详解
2018/03/08 Javascript
详解Vue开发微信H5微信分享签名失败问题解决方案
2018/08/09 Javascript
微信小程序常用赋值方法小结
2019/04/30 Javascript
vue中created和mounted的区别浅析
2019/08/13 Javascript
在Node.js中将SVG图像转换为PNG,JPEG,TIFF,WEBP和HEIF格式的方法
2019/08/22 Javascript
小程序如何获取多个formId实现详解
2019/09/20 Javascript
vue 导航内容设置选中状态样式的例子
2019/11/01 Javascript
JavaScript实现模态对话框实例
2020/01/13 Javascript
使用相同的Apache实例来运行Django和Media文件
2015/07/22 Python
Python中使用bidict模块双向字典结构的奇技淫巧
2016/07/12 Python
python并发编程多进程 互斥锁原理解析
2019/08/20 Python
python中web框架的自定义创建
2019/09/08 Python
Pytorch之view及view_as使用详解
2019/12/31 Python
Python如何实现邮件功能
2020/05/27 Python
python支持多继承吗
2020/06/19 Python
css3学习之2D转换功能详解
2016/12/23 HTML / CSS
外包公司软件测试工程师
2014/11/01 面试题
2014信息技术专业毕业生自我评价
2014/01/17 职场文书
蛋糕店的商业计划书范文
2014/01/27 职场文书
数学检讨书1000字
2014/02/24 职场文书
民主评议党员总结
2014/10/20 职场文书
学生违纪检讨书200字
2014/10/21 职场文书
2014年生活老师工作总结
2014/12/23 职场文书
一个家长教育孩子的心得体会
2016/01/15 职场文书
2019旅游导游工作总结
2019/06/27 职场文书
opencv用VS2013调试时用Image Watch插件查看图片
2021/07/26 Python
MySQL添加索引特点及优化问题
2022/07/23 MySQL