AngularJS 在表单提交前同步调用 server
支付老李
结论
不要把 $http 改成同步请求。正确的处理方式是先取消提交事件,等异步请求完成并填好隐藏字段,再通过原生表单 API 将数据提交到支付网关。
onsubmit 只能立即返回 true 或 false,不能等待 Promise。同步 XHR 会阻塞浏览器主线程,不适合支付流程。
推荐实现
可以用 AngularJS 的 ng-submit 接管表单提交:
<form
id="bookingForm"
name="bookingForm"
method="post"
action="https://payment-gateway.example/pay"
ng-submit="submitPayment($event)">
<!-- 用户填写的字段 -->
<input
type="hidden"
name="paymentToken"
ng-model="payment.token">
<input
type="hidden"
name="transactionId"
ng-model="payment.transactionId">
<button type="submit" ng-disabled="submitting">
{{ submitting ? '正在处理…' : 'Payment' }}
</button>
</form>
在 Controller 中先调用自己的服务器,请求成功后再提交表单:
app.controller('BookingController', function ($scope, $http, $timeout) {
$scope.payment = {};
$scope.submitting = false;
$scope.errorMessage = '';
$scope.submitPayment = function ($event) {
$event.preventDefault();
if ($scope.submitting) {
return;
}
$scope.submitting = true;
$scope.errorMessage = '';
var requestData = {
// 从当前表单模型中读取需要提交给后端的数据
};
$http.post('/api/payments/create', requestData)
.then(function (response) {
if (!response.data ||
!response.data.paymentToken ||
!response.data.transactionId) {
throw new Error('服务器返回的支付信息不完整');
}
$scope.payment.token = response.data.paymentToken;
$scope.payment.transactionId =
response.data.transactionId;
/*
* 等待当前 digest 完成,确保 ng-model 已经把值写入
* hidden inputs,然后调用原生 submit()。
*/
$timeout(function () {
var form =
document.getElementById('bookingForm');
HTMLFormElement.prototype.submit.call(form);
}, 0);
})
.catch(function (error) {
$scope.submitting = false;
$scope.errorMessage = '暂时无法创建支付,请稍后重试。';
// 可在这里记录 error,但不要向用户暴露敏感信息。
console.error('Unable to create payment', error);
});
};
});
这里调用:
HTMLFormElement.prototype.submit.call(form);
不要再次触发 submit button,否则会重新进入 ng-submit,导致 $http 请求重复执行。如果表单里有 name="submit" 的控件,这种写法还能避免该控件覆盖 form.submit 方法。
为什么不能等待 onsubmit 返回
下面的写法拿不到服务器最终返回的布尔值:
var success = scope.createPayment();
return success;
$http 会立即返回 Promise,网络响应稍后才会到达。此时 allowSubmit() 已经执行完毕,浏览器不会暂停提交事件来等待 Promise。
即使改成:
scope.createPayment().then(function () {
return true;
});
其中的 return true 也只是返回给 .then(),无法返回给已经结束的 onsubmit。
因此,需要把提交过程分成两个阶段:
- 第一次触发表单提交时调用
preventDefault(),阻止浏览器直接提交。 $http请求成功后填充隐藏字段,再主动调用原生submit()。
如果不想依赖 AngularJS 更新隐藏字段
请求成功后也可以直接给表单控件赋值,然后立即提交:
$http.post('/api/payments/create', requestData)
.then(function (response) {
var form = document.getElementById('bookingForm');
form.elements.paymentToken.value =
response.data.paymentToken;
form.elements.transactionId.value =
response.data.transactionId;
HTMLFormElement.prototype.submit.call(form);
});
这种写法不需要等待 digest,但隐藏字段的 name 必须与支付网关要求完全一致。
注意事项
- 提交期间应禁用按钮或使用状态锁,防止用户连续点击并创建多个支付订单。
- 只有请求成功且返回字段完整时,才能把表单提交到支付网关。请求失败时应停留在当前页面,并允许用户重试。
- 金额、订单号、签名和回调地址等重要数据应由服务器生成或校验,不能信任浏览器提交的值。
- 不要在日志、URL 或错误提示中输出支付密钥、签名原文、信用卡信息等敏感数据。
form.submit()会跳过浏览器的原生表单校验。如果需要校验,应在发送$http请求前检查 AngularJS 表单状态,例如:
if ($scope.bookingForm.$invalid) {
$scope.bookingForm.$setSubmitted();
return;
}
- 不建议使用同步
XMLHttpRequest。它会冻结页面,网络较慢时浏览器看起来像失去响应,而且主线程同步请求已经是不推荐的旧式做法。 - 如果支付网关支持由服务器创建支付会话并返回跳转 URL,可以在
$http请求成功后直接设置window.location。这种方式通常更简单,但能否使用取决于支付网关的接口设计。
备注:内容仅供参考。
