小程序场景流程化构建
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 流程
知道大致什么样的形态我们可以称为流程后,就需要思考一下在开发阶段为什么需要抽象流程。
流程优势
我在实际使用场景下感觉到的优势:
- 在任何需要掉起流程地方都可以调起流程。
- 开发者只需关心流程成功还是失败,无需知道内部复杂实现。
- 多个简单的流程可自由组合成一个复杂流程。
如果我们把上面的实名认证进行流程化抽象后,我们可以得到下面这样的流程图。
当我们把流程拆分出来后,逻辑就简单很多了,每一个流程都是独立的模块。各个模块之间还可以互相调用,来组合出一个更大型的流程。
流程哲学
程序应该只关注一个目标,并尽可能把它做好。让程序能够互相协同工作。应该让程序处理文本数据流,因为这是一个通用的接口。 —— Malcolm Douglas McIlroy 道格拉斯·麦克罗伊
其实这里也符合 unix 哲学,每个流程只做一件事。这样我们只需要维护好单个流程内部的逻辑就可以了,每个流程返回的数据还可以带到下一个流程内进行使用,这非常像一个 Promise 链。
How to make 流程
在我们知道是什么、为什么后,就可以看看具体到代码层面上我们如何去构建流程,当然这里的场景是小程序,但只要是 SPA 架构的 web 页面,类似的思路一样是可以尝试使用的。我们先来整理一下所需开发的功能点:
- 何时知道要跳回起始页面
- 怎么知道起始页面是哪个
- 多流程的数据流向是怎样的。
基本原理
其实问题很简单,解决方法也是我为什么说需要 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
快去尝试将你的业务场景流程化吧!