使用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 应用代码 方便的排序功能
Feb 06 Javascript
SWFObject 2.1以上版本语法介绍
Jul 10 Javascript
jQueryUI如何自定义组件实现代码
Nov 14 Javascript
javascript阻止scroll事件多次执行的思路及实现
Nov 08 Javascript
利用jQuery简单实现产品展示图片左右滚动功能(示例代码)
Jan 02 Javascript
js实现鼠标经过表格行变色的方法
May 12 Javascript
JavaScript通过事件代理高亮显示表格行的方法
May 27 Javascript
jquery实现滑动特效代码
Aug 10 Javascript
js时间戳和c#时间戳互转方法(推荐)
Feb 15 Javascript
原生javascript实现读写CSS样式的方法详解
Feb 20 Javascript
iview实现select tree树形下拉框的示例代码
Dec 21 Javascript
JS扁平化输出数组的2种方法解析
Sep 17 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框架Swoole定时器Timer特性分析
2014/08/19 PHP
php读取csc文件并输出
2015/05/21 PHP
php类的自动加载操作实例详解
2016/09/28 PHP
php each 返回数组中当前的键值对并将数组指针向前移动一步实例
2016/11/22 PHP
PHP实现QQ、微信和支付宝三合一收款码实例代码
2018/02/19 PHP
PHP+Redis事务解决高并发下商品超卖问题(推荐)
2020/08/03 PHP
DEFER怎么用?
2006/07/01 Javascript
JS操作iframe里的dom(实例讲解)
2014/01/29 Javascript
对于jQuery性能的一些优化建议
2015/08/13 Javascript
理解javascript中的with关键字
2016/02/15 Javascript
JavaScript事件学习小结(五)js中事件类型之鼠标事件
2016/06/09 Javascript
JavaScript提升性能的常用技巧总结【经典】
2016/06/20 Javascript
Javascript实现倒计时(防页面刷新)实例
2016/12/13 Javascript
jQuery Validate验证表单时多个name相同的元素只验证第一个的解决方法
2016/12/24 Javascript
javascript笔记之匿名函数和闭包
2017/02/06 Javascript
jQuery中on方法使用注意事项详解
2017/02/15 Javascript
vue-cli项目如何使用vue-resource获取本地的json数据(模拟服务端返回数据)
2017/08/04 Javascript
快速了解vue-cli 3.0 新特性
2018/02/28 Javascript
JS实现简单的星期格式转换功能示例
2018/07/23 Javascript
Node对CommonJS的模块规范
2019/11/06 Javascript
在vue中对数组值变化的监听与重新响应渲染操作
2020/07/17 Javascript
[51:26]VP vs VG 2018国际邀请赛小组赛BO2 第二场 8.19
2018/08/21 DOTA
详解Python编程中基本的数学计算使用
2016/02/04 Python
Python 稀疏矩阵-sparse 存储和转换
2017/05/27 Python
Windows下Python3.6安装第三方模块的方法
2018/11/22 Python
django开发post接口简单案例,获取参数值的方法
2018/12/11 Python
Python 按字典dict的键排序,并取出相应的键值放于list中的实例
2019/02/12 Python
python实现随机漫步方法和原理
2019/06/10 Python
Pycharm如何打断点的方法步骤
2019/06/13 Python
Python企业编码生成系统之主程序模块设计详解
2019/07/26 Python
django queryset 去重 .distinct()说明
2020/05/19 Python
基于keras中的回调函数用法说明
2020/06/17 Python
python转化excel数字日期为标准日期操作
2020/07/14 Python
英国空调、除湿机和通风设备排名第一:Air Con Centre
2019/02/25 全球购物
爱国卫生月活动总结范文
2014/04/25 职场文书
2015年个人现实表现材料
2014/12/10 职场文书