使用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 03 Javascript
浅谈javascript中createElement事件
Dec 05 Javascript
JavaScript使用cookie记录临时访客信息的方法
Apr 07 Javascript
JavaScript获取数组最小值和最大值的方法
Jun 09 Javascript
基于insertBefore制作简单的循环插空效果
Sep 21 Javascript
jQuery插件开发发送短信倒计时功能代码
May 09 jQuery
利用JS制作万年历的方法
Aug 16 Javascript
详解vue移动端项目代码拆分记录
Mar 15 Javascript
vue路由对不同界面进行传参及跳转的总结
Apr 20 Javascript
小程序如何获取多个formId实现详解
Sep 20 Javascript
React中获取数据的3种方法及优缺点
Feb 18 Javascript
Node.js API详解之 tty功能与用法实例分析
Apr 27 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
多数据表共用一个页的新闻发布
2006/10/09 PHP
php $_SERVER["REQUEST_URI"]获取值的通用解决方法
2010/06/21 PHP
PHP 第二节 数据类型之转换
2012/04/28 PHP
smarty模板引擎中内建函数if、elseif和else的使用方法
2015/01/22 PHP
PHP中trait使用方法详细介绍
2017/05/21 PHP
TP5框架使用QueryList采集框架爬小说操作示例
2020/03/26 PHP
Javascript与flash交互通信基础教程
2008/08/07 Javascript
JavaScript 基础篇之运算符、语句(二)
2012/04/07 Javascript
Jquery时间验证和转换工具小例子
2013/07/01 Javascript
jquery实现简单文字提示效果
2015/12/02 Javascript
延时加载JavaScript代码提高速度
2015/12/27 Javascript
JS实现淡入淡出图片效果的方法分析
2016/12/20 Javascript
Bootstrap入门教程一Hello Bootstrap初识
2017/03/02 Javascript
vue-router 导航钩子的具体使用方法
2017/08/31 Javascript
微信小程序数据统计和错误统计的实现方法
2019/06/26 Javascript
写给新手同学的vuex快速上手指北小结
2020/04/14 Javascript
Node.js API详解之 module模块用法实例分析
2020/05/13 Javascript
vue3.0 上手体验
2020/09/21 Javascript
微信小程序实现可拖动悬浮图标(包括按钮角标的实现)
2020/12/29 Javascript
[01:05]主宰至宝剑心之遗
2017/03/16 DOTA
举例讲解Python程序与系统shell交互的方式
2015/04/09 Python
python OpenCV学习笔记实现二维直方图
2018/02/08 Python
对python内置map和six.moves.map的区别详解
2018/12/19 Python
Python闭包和装饰器用法实例详解
2019/05/22 Python
Python3如何在Windows和Linux上打包
2020/02/25 Python
如何通过Python3和ssl实现加密通信功能
2020/05/09 Python
使用keras实现BiLSTM+CNN+CRF文字标记NER
2020/06/29 Python
在 Python 中使用 MQTT的方法
2020/08/18 Python
python搜索算法原理及实例讲解
2020/11/18 Python
Redbubble法国:由独立艺术家设计的独特产品
2019/01/08 全球购物
领先的英国注册在线药房 :Simply Meds Online
2019/03/28 全球购物
Kathmandu新西兰官网:新西兰户外运动品牌
2019/07/27 全球购物
财政专业求职信范文
2014/02/19 职场文书
药品销售内勤岗位职责
2015/04/13 职场文书
今日说法观后感
2015/06/08 职场文书
Django给表单添加honeypot验证增加安全性
2021/05/06 Python