前端微信支付与支付宝支付的坑

1. 支付流程

微信移动端支付官方文档

支付宝移动端支付官方文档

支付宝 PC 端支付官方文档

微信支付在移动端 H5 浏览器中可以唤醒微信应用进行支付,其业务流程为:

  • 用户在浏览器端点击支付
  • 前端向后台请求微信支付中间页的跳转链接
  • 在前端打开该链接,并等待跳转到支付中间页
  • 唤醒用户手机的微信支付
  • 支付完毕返回浏览器
  • 中间页自动跳转到设置的重定向页面

支付宝在移动端 H5 浏览器中也可以唤醒支付宝应用进行支付,其业务流程为:

  • 用户在浏览器点击支付
  • 前端向后台请求表单信息
  • 前端将请求到的表单作为 DOM 节点插入到 Body 中,并使用 JS 提交该表单,之后会自动跳转到支付中间页
  • 唤醒用户支付宝支付
  • 支付完毕返回浏览器(安卓端会自动返回浏览器,iOS端需要手动切回浏览器)
  • 中间页自动跳转到设置的重定向页面

微信支付与支付宝支付的总体流程相似,在客户端的操作效果如下:

2. 微信支付的坑

在标准浏览器下可以按照如下方式进行页面的跳转:

1
2
3
4
5
6
7
request("/api/pay", (res)=>{
let orderid = res.data.orderid // 获取该订单编号
let redirectUrl = "www.test.com/paydone?orderid=" + orderid // 生成重定向页面的地址

let middlePageUrl = res.data.url // 获取生成的支付中间页
window.open(`${middlePageUrl}&redirect_url=${encodeURIComponent(redirectUrl)}`) // 拼接 url(为了配置重定向页面)
})

Safari 浏览器在异步方法中使用 window.open() 无效

在 Safari 浏览器的异步方法中禁用了 window.open() 方法,因此我们不能在想服务器请求到支付中间页的 url 后再打开中间页,而是再请求前先打开一个中间页,再将中间页的 url 进行替换,才能跳转到中间页,因此需要改代码为:

1
2
3
4
5
6
7
8
let w = window.open()
request("/api/pay", (res)=>{
let orderid = res.data.orderid // 获取该订单编号
let redirectUrl = "http://www.test.com/paydone?orderid=" + orderid // 生成重定向页面的地址

let middlePageUrl = res.data.url // 获取生成的支付中间页
w.location = `${middlePageUrl}&redirect_url=${encodeURIComponent(redirectUrl)}` // 调整新打开页面的 url
})

华为浏览器问题

当客户端发起支付请求后,开启微信支付中间页唤醒微信支付。在标准浏览器下,支付完成之后页面会自动跳转到 redirect_url,但是华为浏览器的行为是将支付中间页的 url 替换为跳转前的页面(也就是上图的A页面)并刷新页面,导致 redirect_url 失效,最终导致华为浏览器的支付效果为:

所以对于垃圾华为来说,微信支付的自动跳转会失效,因此不能使用重定向的功能。那么就需要在支付前的页面(A页面)开启一个监听,监听是否支付完成,如果支付完成就自动关闭支付页,重新返回 A 页面并提示用户支付已经完成。

1
2
3
4
5
6
7
8
9
10
let w = window.open()
let status = "pedding"
watch("pay_done", function(){
w.close()
status = "done"
})
request("/api/pay", (res)=>{
let middlePageUrl = res.data.url // 获取生成的支付中间页
w.location = middlePageUrl
})

但是 Safari 浏览器会禁止在 A 页面关闭 B 页面这种操作(存疑),所以我们推荐在 IOS 端使用页面重定向方案,在安卓端使用 A 页面监听支付状态,关闭 B 页面这种操作。

3. 支付宝支付的坑

在标准浏览器下完成支付宝支付:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 用户点击支付按钮
payBtn.onclick = function() {
request("/api/pay", (res)=>{
let formHTML = res.data.formHTML
// 创建 DOM 对象
let tmpNode = document.createElement("div")
tmpNode.innerHTML = formHtml
// 在临时 DOM 对象中获取 form,并获取 form 的 id
let form = tmpNode.querySelector("form")
formId = form.getAttribute("id")
// 插入 form
document.body.appendChild(form)
// 提交 form
document.getElementById(formId).submit()
})
}

此外这里还有个坑,默认返回的 html 是一个 form 标签以及一个 script 标签,script 标签内写的是执行提交表单的脚本。但是如果将其直接插入 body 是不会执行的,需要手动创建一个 script 对象,并将该对象的 innerHTML 替换为从服务器端获取的 html 模板中的 script 标签中的内容,再插入到 body 中才会执行(但是再上述 DEMO 中我们没有用到该 script 标签而是自行手动执行)。

IOS 端无法在异步方法中提交表单

如果按照上面的代码使用支付宝付款,那么会无法触发 submit() 方法,为了解决该问题,需要修改操作逻辑,也就是先请求支付宝表单,将表单插入页面 body 中,然后我们再引导用户点击一个按钮,触发表单的提交事件,因此需要改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 再支付按钮生成时就从服务器拉取支付宝表单并插入 DOM 中
request("/api/pay", (res)=>{
let formHTML = res.data.formHTML
// 创建 DOM 对象
let tmpNode = document.createElement("div")
tmpNode.innerHTML = formHtml
// 在临时 DOM 对象中获取 form,并获取 form 的 id
let form = tmpNode.querySelector("form")
form.setAttribute("target", "_blank") // 设置表单在新窗口打开
formId = form.getAttribute("id")
// 插入 form
document.body.appendChild(form)
})

// 用户点击支付按钮
payBtn.onclick = function() {
document.getElementById(formId).submit()
}