小程序场景流程化构建

What is 流程

第一步我们需要对流程有一个认识,需要知道一个流程的基本形态是怎样的。

流程案例

  • 使用 APP 第三方支付时,点击选择使用微信支付后会拉起应用,用户可以选择各种银行卡或信用卡进行支付,若密码失败则在微信内继续处理,最终跳回 APP 后确认支付成功后,即可进行后续处理。

  • 使用 APP 第三方登录时,点击选择使用 QQ 登录后会拉起应用,用户可以选择快速登录或输入账号密码进行登录,若密码失败则在 QQ 内继续处理,最终跳回 APP 后确认为登录成功后,即可进行后续处理。

  • 使用微信小程序人脸识别时,点击开始使用人脸识别后会拉起微信的人脸识别,若识别失败则在人脸识别内重新进行人脸识别,最终回到拉起人脸识别前的那个页面,得到是否成功的结果。

流程抽象

从上面的案例来看,其实我大部分都是 Copy/Paste 的文案,说明一个流程的基本形态是很固定的,它不仅限于在 APP 之间的跳转,小程序与微信 API 的使用,在任何我们认为属于流程的场景,我们都可以尝试去构建我们自己的流程。

基础流程

将刚才说的以流程图来表示就是这样的,各种复杂的处理都在流程页面内,业务页面最终只需要知道成功还是失败即可。如果我们以开发者的角度来看,我们在业务侧只需要这样处理。

// 对开发者来说,是可以抽象成一个 Promise 来表示的。
// pending 表示流程未结束
// resolve 表示流程返回成功
// reject  表示流程返回失败

sdk.pay(opts).then(successHandler).catch(failHandler)
sdk.face(opts).then(successHandler).catch(failHandler)
sdk.login(opts).then(successHandler).catch(failHandler)

对开发者来说,这样的流程调用只能说非常的清爽!当流程结束后,开发者可以在 then 里进行后续处理。若无法正常开启流程或用户主动取消流程,则可以在 catch 内进行处理。

使用场景

一般我们会在各种通用场景下,都会需要调用流程。当你发现你的项目,在各种场景下都可能需要某个流程时,你就可以开始考虑将相关内容抽象流程化。

在我们开发政务服务相关的小程序时,在整个小程序内,我们都需要涉及实名校验,整个流程一环扣一环,远远不是检查一下是否要登录就选择登录这么简单的事情。

认证流程

这里是一个政务服务的认证场景,场景的流程是很长的。

Why we need 流程

知道大致什么样的形态我们可以称为流程后,就需要思考一下在开发阶段为什么需要抽象流程。

流程优势

我在实际使用场景下感觉到的优势:

  1. 在任何需要掉起流程地方都可以调起流程。
  2. 开发者只需关心流程成功还是失败,无需知道内部复杂实现。
  3. 多个简单的流程可自由组合成一个复杂流程。

如果我们把上面的实名认证进行流程化抽象后,我们可以得到下面这样的流程图。

抽象认证流程

当我们把流程拆分出来后,逻辑就简单很多了,每一个流程都是独立的模块。各个模块之间还可以互相调用,来组合出一个更大型的流程。

流程哲学

程序应该只关注一个目标,并尽可能把它做好。让程序能够互相协同工作。应该让程序处理文本数据流,因为这是一个通用的接口。 —— Malcolm Douglas McIlroy 道格拉斯·麦克罗伊

其实这里也符合 unix 哲学,每个流程只做一件事。这样我们只需要维护好单个流程内部的逻辑就可以了,每个流程返回的数据还可以带到下一个流程内进行使用,这非常像一个 Promise 链。

How to make 流程

在我们知道是什么、为什么后,就可以看看具体到代码层面上我们如何去构建流程,当然这里的场景是小程序,但只要是 SPA 架构的 web 页面,类似的思路一样是可以尝试使用的。我们先来整理一下所需开发的功能点:

  1. 何时知道要跳回起始页面
  2. 怎么知道起始页面是哪个
  3. 多流程的数据流向是怎样的。

基本原理

其实问题很简单,解决方法也是我为什么说需要 SPA 架构进行实现。我们只需要在调用 API 后,记录当前页面栈并全局监听一个事件。当流程结束后,我们再通过事件通知来决定是否需要跳回调用页。我们只需要记录页面栈,配合全局唯一的事件,跨页面通信并进行相应处理。而多数据流向,我们是可以通过前一个流程返回的数据直接带到新流程进行使用。

以下提供基本代码,除基础库外,示例代码不可直接运行(有许多伪代码)。

流程基础库

  // sdk.js

  interface FlowOpts {
    // 决定是如何开启一个流程
    startType: 'navigateTo' | 'redirctTo',      
    // 流程结束后,是否需要保留当前页面
    finishType: 'keep' | 'pop',
    // 带给页面的参数
    params: Record<string, any>
  }

  /**
   * 创建通用流程
   * @param url 跳转参数
   * @param options 创建参数,具体类型参照 FlowOpts
   * @return Promise Response
   */
  function createFlow(url: string, options: FlowOpts = {}) {
    const startPageLength = getCurrentPages().length
    const successEvent = url + '-' + extend.generateGUID()

    options.startType = options.startType || 'navigateTo'
    options.finishType = options.finishType || 'pop'
    options.params = options.params || {}

    const urlWithOptions = urlJoinParams(url, {
        successEvent,
        ...options.params
    })

    return new Promise((resolve, reject) => {
      Event.addEventListener(successEvent, (res: any) => {
        if (options.finishType === 'keep') {
          // 保存到当前页面
          resolve(res.target)
        } else {
          // 如果是弹回流程开始页
          udb.navigatoBackToStart(startPageLength)
            .then(() => {
              resolve(res.target)
            })
        }
      })
  
      wx[options.finishType]({
        url: urlWithOptions,
        complete() {
          Event.removeEventListener(successEvent)
        }
      })
    })
  },

  // 流程跳回开始
  navigatoBackToStart(startPageLength: number) {
    return new Promise((resolve, reject) => {
      const endPageLength = getCurrentPages().length
      const delta = endPageLength - startPageLength

      // 回退页面
      wx.navigateBack({ delta })
      delayResolve()

      // 确保异步回退成功
      function delayResolve() {
        setTimeout(() => {
          const currentPageLength = getCurrentPages().length
          if (endPageLength > 1 && startPageLength === currentPageLength) {
            resolve()
          } else {
            delayResolve()
          }
        }, 100)
      }
    })
  },

创建流程

// my-flow.js

// 业务创建流程
function startFlowOne(options) {
  if(!canStart) {
    // 能否发起流程的业务逻辑判断
    return Promise.reject()
  }

  return createFlow('/pages/flow-one/index', options)
}
function startFlowTwo(options) {
  return createFlow('/pages/flow-two/index', options)
}
// 组合流程
function startFlowOneTwo(options) {
  return startFlowOne({
    startType: 'keep',
    params: options
  }).then(flowRes => {
    // 多流程数据合并
    return startFlowTwo({
      params: {
        ...flowRes,
        ...options
      }
    })
  })
}

具体流程页面,处理完业务后发起成功事件

// page/flow-one/index.js

Page({
  onLoad(parmas) {
    if(params) {
      this.successEvent = params.successEvent
    }
  },
  handleSubmit() {
    request({
      url: 'example.com',
      data: params,
      methods: 'POST'
    }).then((res) => {
      Event.dispatch(this.successEvent, res)
    })
  }
})

真正提供到给业务开发使用。

// pages/index/index.js

Page({
  handleTap() {
    // 非常简单!
    return sdk.startFlowOneTwo({ id: 3 })
      .then((res) => {
        // 成功后的业务代码
      })
      .catch(() => {
        // 无法正常调起流程
      })
  }
}) 

通过上面的代码,我们就可以把业务再进行合理抽象,通过流程化降低业务复杂度。

遗留问题

在小程序上使用还有哪些特别要注意的点呢,其中有一个就是要千万注意页面栈的问题。在小程序内是有十层页面栈限制的,如果你的流程特别的长,需要格外注意这一点并进行相应的优化。但如果我们在 web 页面使用,是没有这类问题的。

Ending

快去尝试将你的业务场景流程化吧!





posted @ 2019-01-24 15:24  Yika丶J  阅读(1022)  评论(0编辑  收藏  举报