Vue SPA 中 Checkout() 获取数据后再跳转支付网关
支付阿杰
结论
先阻止表单默认提交,再在 Checkout() 中等待 axios.get() 完成。更新表单数据并等待 Vue 刷新 DOM 后,调用原生表单提交方法。支付网关收到的将是更新后的完整数据,而不是尚未赋值的空字段。
推荐实现
在模板中使用 @submit.prevent,避免浏览器立即跳转,同时给表单添加 ref:
<form
ref="paymentForm"
method="post"
:action="Action"
@submit.prevent="Checkout"
>
<input type="hidden" name="invoiceNumber" :value="InvoiceNumber">
<input type="hidden" name="invoiceDate" :value="InvoiceDate">
<input type="hidden" name="amount" :value="Amount">
<input type="hidden" name="terminalCode" :value="TerminalCode">
<input type="hidden" name="merchantCode" :value="MerchantCode">
<input type="hidden" name="redirectAddress" :value="RedirectAddress">
<input type="hidden" name="timeStamp" :value="TimeStamp">
<input type="hidden" name="action" :value="Action">
<input type="hidden" name="sign" :value="Sign">
<button type="submit" :disabled="submitting">
Checkout
</button>
</form>
Vue 2 中可以这样写:
methods: {
async Checkout() {
if (this.submitting) {
return;
}
this.submitting = true;
try {
const response = await axios.get('./api/pasargad');
const data = response.data;
this.InvoiceNumber = data.InvoiceNumber;
this.InvoiceDate = data.InvoiceDate;
this.Amount = data.Amount;
this.TerminalCode = data.TerminalCode;
this.MerchantCode = data.MerchantCode;
this.RedirectAddress = data.RedirectAddress;
this.TimeStamp = data.TimeStamp;
this.Action = data.Action;
this.Sign = data.Sign;
// 等待 Vue 将最新数据写入 input 和 form action
await this.$nextTick();
const form = this.$refs.paymentForm;
// 使用原生 submit,避免再次触发 @submit.prevent 而形成循环
HTMLFormElement.prototype.submit.call(form);
} catch (error) {
this.submitting = false;
console.error('Unable to prepare payment data:', error);
// 此处可以显示“支付信息获取失败,请重试”等提示
}
}
}
这些响应式字段还需要在组件的 data 中声明:
data() {
return {
InvoiceNumber: '',
InvoiceDate: '',
Amount: '',
TerminalCode: '',
MerchantCode: '',
RedirectAddress: '',
TimeStamp: '',
Action: '',
Sign: '',
submitting: false
};
}
为什么需要 nextTick
await axios.get() 只能保证接口请求已经结束。组件属性赋值后,Vue 通常会把真实 DOM 的更新放入异步队列。
如果此时立即提交表单,隐藏输入框的 value 或表单的 action 可能仍是旧值。因此,赋值后需要执行:
await this.$nextTick();
等 DOM 中的表单字段更新完毕,再提交表单。
也可以使用 Promise 写法
如果不使用 async/await,也可以继续使用 then()。不过,提交操作必须放在请求成功回调和 $nextTick() 之后:
methods: {
Checkout() {
if (this.submitting) {
return;
}
this.submitting = true;
axios.get('./api/pasargad')
.then(response => {
const data = response.data;
this.InvoiceNumber = data.InvoiceNumber;
this.InvoiceDate = data.InvoiceDate;
this.Amount = data.Amount;
this.TerminalCode = data.TerminalCode;
this.MerchantCode = data.MerchantCode;
this.RedirectAddress = data.RedirectAddress;
this.TimeStamp = data.TimeStamp;
this.Action = data.Action;
this.Sign = data.Sign;
return this.$nextTick();
})
.then(() => {
HTMLFormElement.prototype.submit.call(
this.$refs.paymentForm
);
})
.catch(error => {
this.submitting = false;
console.error('Unable to prepare payment data:', error);
});
}
}
箭头函数会保留 Vue 方法中的 this,所以不需要 var vm = this,也不需要 .bind(vm)。
注意事项
- 必须使用
@submit.prevent,否则浏览器可能在异步请求完成前提交表单中的原有内容。 - 不要在成功回调之外调用
form.submit(),否则无法保证接口已经返回。 - 所有需要发送给网关的
<input>都必须设置正确的name。浏览器提交表单时依据的是name,不是 Vue 属性名。 HTMLFormElement.prototype.submit.call(form)不会再次触发submit事件,因此不会重新进入Checkout()。- 原生
submit()会绕过浏览器的表单校验。如果表单中还有用户填写的字段,应在提交前调用form.checkValidity(),必要时再调用form.reportValidity()。 - 支付金额、订单号和签名必须由服务端生成并校验,不能信任前端传入的数据。
- 如果接口返回的
Action是支付网关地址,应由服务端限制允许的目标域名,避免直接跳转到未经验证的地址。 submitting可以防止用户连续点击,避免生成多个订单或重复提交。接口失败时不要跳转,并恢复按钮状态。
备注:内容仅供参考。
