vue h5 调用支付宝app失败后如何在原页面支付?
结论
唤起支付宝 App 失败后,可以继续在当前 App 的 H5 WebView 中打开支付宝收银台,无需主动调用系统默认浏览器。
需要注意以下几点:
- “支付宝 App 支付”和“H5 支付”是两种不同的接入方式。
- H5 页面不能直接调用支付宝原生支付 SDK。使用原生 SDK,需要宿主 App 的 Android 或 iOS 代码配合。
- 未安装支付宝时,
alipays://等 Scheme 不会自动、可靠地切换到 H5 支付,也不会自动生成支付页面。 - 正确的处理方式是:唤起 App 失败后,前端请求服务端创建手机网站支付订单,再将支付宝返回的支付表单提交到当前 WebView。
- 支付流程能否一直留在当前 WebView,还要看宿主 App 是否允许 WebView 加载支付宝域名、处理重定向和打开新窗口。
如果没有“优先使用原生 App 支付”的硬性要求,通常直接接入支付宝手机网站支付更稳妥。支付宝收银台会根据设备环境展示可用的支付方式。
为什么调用 App 失败后可能打开默认浏览器
页面通过 Scheme、Universal Link 或 Android Intent 唤起支付宝时,具体行为由操作系统和宿主 App 决定。
例如:
alipays://...
如果设备没有安装支付宝,可能出现以下情况:
- Scheme 没有响应;
- 系统提示找不到能够处理该链接的应用;
- Android Intent 配置了浏览器回退地址,因而打开默认浏览器;
- 宿主 App 拦截外部链接,并将其交给系统浏览器;
- 页面停留在原 WebView,但支付流程没有启动。
因此,“唤起支付宝 App”并不是自带 H5 降级能力的支付接口。业务方需要通过服务端自行创建 H5 支付页面。
推荐实现流程
可以将支付流程分为两条路径:
- 前端向业务服务端发起创建订单请求。
- 尝试通过支付宝支持的跳转地址唤起支付宝 App。
- 根据页面可见性变化,判断页面是否可能已成功跳转。
- 如果页面在限定时间内仍然可见,请求服务端创建 H5 支付。
- 服务端调用支付宝手机网站支付接口。
- 前端将服务端返回的 HTML 表单写入当前页面并自动提交。
- 支付完成后,通过同步返回地址恢复页面,并由服务端根据异步通知确认最终支付状态。
页面可见性只能用来改善交互体验,无法准确判断用户是否安装了支付宝,也不能用来判断支付是否成功。
Vue 端示例
下面的示例展示了“先尝试唤起 App,失败后在当前 WebView 发起 H5 支付”的基本结构。appPayUrl 应由服务端根据实际接入方案返回,不应由前端拼接带签名的支付参数。
<script setup>
import { ref } from 'vue'
const paying = ref(false)
async function createOrder() {
const response = await fetch('/api/pay/orders', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
productId: 'PRODUCT_ID'
})
})
if (!response.ok) {
throw new Error('创建订单失败')
}
return response.json()
}
async function startH5Pay(orderId) {
const response = await fetch('/api/pay/alipay/wap', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ orderId })
})
if (!response.ok) {
throw new Error('创建支付宝 H5 支付失败')
}
const { formHtml } = await response.json()
// formHtml 是服务端调用 alipay.trade.wap.pay 后返回的完整表单。
document.open()
document.write(formHtml)
document.close()
}
function tryOpenAlipay(appPayUrl, onFallback) {
let pageHidden = false
let fallbackExecuted = false
const handleVisibilityChange = () => {
if (document.hidden) {
pageHidden = true
}
}
const cleanup = () => {
document.removeEventListener(
'visibilitychange',
handleVisibilityChange
)
}
const fallback = () => {
if (fallbackExecuted) return
fallbackExecuted = true
cleanup()
onFallback()
}
document.addEventListener(
'visibilitychange',
handleVisibilityChange
)
// 应由真实的用户点击事件触发,否则可能被浏览器或 WebView 拦截。
window.location.href = appPayUrl
window.setTimeout(() => {
if (!pageHidden && document.visibilityState === 'visible') {
fallback()
} else {
cleanup()
}
}, 1800)
}
async function pay() {
if (paying.value) return
paying.value = true
try {
const { orderId, appPayUrl } = await createOrder()
if (!appPayUrl) {
await startH5Pay(orderId)
return
}
tryOpenAlipay(appPayUrl, async () => {
try {
await startH5Pay(orderId)
} catch (error) {
paying.value = false
console.error(error)
alert('支付页面打开失败,请稍后重试')
}
})
} catch (error) {
paying.value = false
console.error(error)
alert('创建支付订单失败,请稍后重试')
}
}
</script>
<template>
<button :disabled="paying" @click="pay">
{{ paying ? '正在打开支付...' : '支付宝支付' }}
</button>
</template>
代码中的超时时间只用于改善体验,无法准确检测支付宝是否安装。部分手机跳转 App 时不会及时触发 visibilitychange,有些设备则会过一段时间才弹出确认框。
服务端创建 H5 支付
服务端需要调用支付宝手机网站支付接口 alipay.trade.wap.pay,然后将 SDK 生成的完整 HTML 表单返回给前端。
下面是伪代码。具体类名和配置方式应以项目使用的支付宝服务端 SDK 为准:
app.post('/api/pay/alipay/wap', async (req, res) => {
const { orderId } = req.body
const order = await findOrder(orderId)
if (!order || order.status !== 'UNPAID') {
return res.status(400).json({
message: '订单不存在或状态不允许支付'
})
}
const formHtml = await alipayClient.pageExecute(
'alipay.trade.wap.pay',
{
bizContent: {
out_trade_no: order.paymentNo,
total_amount: order.amount,
subject: order.subject,
product_code: 'QUICK_WAP_WAY'
},
return_url: 'https://example.com/pay/alipay/return',
notify_url: 'https://api.example.com/pay/alipay/notify'
},
'POST'
)
res.json({ formHtml })
})
支付宝 SDK 应负责签名和生成表单。应用私钥、支付宝公钥和签名逻辑必须保存在服务端,不能写入 Vue 代码。
在原 WebView 中提交支付表单
除了使用 document.write(),还可以创建临时容器,将表单插入 DOM 后提交:
function submitAlipayForm(formHtml) {
const container = document.createElement('div')
container.style.display = 'none'
container.innerHTML = formHtml
document.body.appendChild(container)
const form = container.querySelector('form')
if (!form) {
container.remove()
throw new Error('支付宝支付表单无效')
}
form.submit()
}
如果返回内容依赖内联脚本自动提交,使用 innerHTML 插入后,脚本通常不会执行。显式查找 <form> 并调用 submit() 会更可靠。
不要用 iframe 嵌入支付宝收银台。支付页面可能受到安全策略、Cookie、重定向或页面嵌套限制,容易发生无法登录、无法跳转或页面空白等问题。
宿主 App 需要配合的配置
如果 H5 页面运行在自有 App 的 WebView 中,原生端至少要确认以下事项:
- WebView 可以访问 HTTPS 页面和支付宝相关域名;
- 页面跳转不会被统一交给系统默认浏览器;
- WebView 能够正确处理
location.href、表单提交和多次重定向; - 普通
https://支付地址优先在当前 WebView 中加载; alipays://等外部 Scheme 交给系统或支付宝 App 处理;- WebView 支持必要的 Cookie、DOM Storage 和 JavaScript;
- WebView 能够正确处理
window.open或新窗口跳转; - Android、iOS 的 Scheme 白名单或查询限制已按实际接入方式配置。
Android WebView 可以根据 URL 类型分别处理:
override fun shouldOverrideUrlLoading(
view: WebView,
request: WebResourceRequest
): Boolean {
val uri = request.url
val scheme = uri.scheme.orEmpty()
return when (scheme) {
"http", "https" -> {
// 返回 false,让普通网页继续在当前 WebView 中加载。
false
}
"alipays" -> {
try {
val intent = Intent(Intent.ACTION_VIEW, uri)
view.context.startActivity(intent)
} catch (e: ActivityNotFoundException) {
// 未安装支付宝时不要强制打开系统浏览器,
// 由 H5 页面执行手机网站支付降级逻辑。
}
true
}
else -> true
}
}
实际项目还需要校验允许跳转的 Scheme 和域名,不能将任意外部 URL 直接交给系统处理。
支付结果必须由服务端确认
return_url 只是用户支付后的页面跳转地址。用户可能没有返回页面,也可能遇到网络中断或参数被修改,因此不能仅凭 return_url 将订单标记为支付成功。
可靠的处理流程如下:
- 服务端接收支付宝异步通知;
- 验证通知签名;
- 校验商户号、订单号、金额和订单状态;
- 更新本地订单;
- H5 返回页面调用业务接口查询订单状态;
- 服务端确认支付成功后,页面再显示支付成功。
async function queryPaymentResult(orderId) {
const response = await fetch(`/api/pay/orders/${orderId}`)
const order = await response.json()
if (order.status === 'PAID') {
// 展示支付成功页面
} else {
// 展示处理中或允许用户重新查询
}
}
更稳妥的选择
如果需求只是“有支付宝就用支付宝 App,没有就继续网页支付”,可以优先统一从 alipay.trade.wap.pay 进入支付宝收银台,由支付宝根据当前环境提供可用的支付方式。这比前端自行判断设备是否安装支付宝更稳定。
如果产品必须明确区分原生 App 支付和 H5 支付,宿主 App 就需要接入支付宝原生支付 SDK,并通过 JSBridge 接收 Vue 页面发起的支付请求。原生调用失败或确认未安装支付宝后,再通知 H5 执行 alipay.trade.wap.pay。纯 H5 无法像原生 App 那样可靠地检测安装状态或调用支付功能。
备注:内容仅供参考。
