阅读 37932

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

「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!

有言在先

本瓜知道前不久写的《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 函数,这也只是一个起点!

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

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

谢谢支持~

我是掘金安东尼,输出暴露输入,技术洞见生活!下次再会~

文章分类
前端