ASP.net MVC 5 中按钮点击后 JavaScript 重定向失效
结论
window.location.href 没有按预期生效,是因为按钮触发了两次页面导航:
- 点击事件通过 JavaScript 跳转;
- 按钮的默认行为继续提交表单,浏览器随后跳转到
SavePayment的响应页面。
普通表单提交会导航顶层页面。由于表单提交的响应后到,它覆盖了前面的 JavaScript 跳转,最终页面显示的是 Hello。
如果点击后只需立即跳转,应阻止表单的默认提交。不过,这样无法保证 SavePayment 请求成功发出。实际需求如果是“等待支付结果,接近超时时跳回首页”,则应阻止普通表单提交,改用 AJAX 发起支付请求,再由独立计时器控制页面跳转。
只需要立即跳转
监听表单的 submit 事件比只监听按钮的 click 更合适,因为用户按 Enter 提交表单时也能生效。
$(function () {
$('#paymentForm').on('submit', function (event) {
event.preventDefault();
window.location.assign('/Home/Index');
});
});
对应表单:
<form id="paymentForm" method="post" action="/Payment/SavePayment">
@Html.AntiForgeryToken()
<button id="formSubmitButton" type="submit">
Submit
</button>
</form>
关键是调用 event.preventDefault()。如果没有这行代码,浏览器仍会按默认行为提交表单。
这段代码取消了普通表单提交,因此不能再认为 SavePayment 一定会被调用。即使先发送请求再跳转,页面导航也可能中断浏览器尚未完成的请求。
支付场景应改用 AJAX
支付场景通常需要完成以下流程:
- 发起支付请求;
- 请求及时成功时进入成功页面;
- 请求等待过久时返回首页;
- IIS 后续返回的超时响应不能覆盖首页。
可以通过 AJAX 发起支付请求。AJAX 响应不会自动替换当前页面,所以服务器稍后返回错误页面时,也不会像普通表单提交那样覆盖整个浏览器页面。
$(function () {
$('#paymentForm').on('submit', function (event) {
event.preventDefault();
var $form = $(this);
var finished = false;
var request;
// 如果“1 分 58 秒”是指 1:58,则为 118000 毫秒。
var redirectTimer = window.setTimeout(function () {
if (finished) {
return;
}
finished = true;
// 终止客户端等待。服务器端操作不一定会因此停止。
if (request) {
request.abort();
}
window.location.replace('/Home/Index');
}, 118000);
request = $.ajax({
url: $form.attr('action'),
type: 'POST',
data: $form.serialize()
}).done(function (response) {
if (finished) {
return;
}
finished = true;
window.clearTimeout(redirectTimer);
window.location.assign('/Payment/Hello');
}).fail(function (xhr, textStatus) {
if (finished || textStatus === 'abort') {
return;
}
finished = true;
window.clearTimeout(redirectTimer);
window.location.replace('/Home/Index');
});
});
});
计时器和 AJAX 回调都会检查 finished,确认页面是否已经开始跳转,避免请求成功与计时器几乎同时触发时发生两次导航。
window.location.replace() 不会将当前支付页留在浏览器历史记录中,通常适合超时后返回首页的情况。如果希望用户可以通过“后退”回到支付页,可改用 window.location.assign()。
Controller 最好返回结果,而不是 View
AJAX 请求收到 return View("Hello") 后,只会取得响应中的 HTML,不会自动导航到该页面。更清楚的做法是让 POST 返回 JSON,再由 JavaScript 根据结果决定跳转地址:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult SavePayment(PaymentFormMV data)
{
// 执行或提交支付操作。
return Json(new
{
success = true,
redirectUrl = Url.Action("Hello", "Payment")
});
}
[HttpGet]
public ActionResult Hello()
{
return View();
}
前端读取服务器返回的地址:
request = $.ajax({
url: $form.attr('action'),
type: 'POST',
data: $form.serialize(),
dataType: 'json'
}).done(function (result) {
if (finished) {
return;
}
finished = true;
window.clearTimeout(redirectTimer);
if (result.success) {
window.location.assign(result.redirectUrl);
} else {
window.location.replace('/Home/Index');
}
});
这样也能明确区分职责:POST 处理数据,GET 显示结果页面。
为什么 IIS 的超时页面不会再覆盖首页
使用普通表单提交时,SavePayment 的响应直接对应浏览器当前文档,因此它返回的 View 或错误页会成为新页面。
改用 AJAX 后,SavePayment 的响应只是后台请求的结果。计时器跳转到首页后:
- 浏览器已经离开当前页面;
- 原页面的 JavaScript 环境会被销毁;
- 浏览器通常会取消尚未完成的 AJAX 请求;
- 即使服务器随后继续执行并发生超时,该响应也不是顶层页面导航,无法替换已经打开的首页。
需要注意的是,request.abort() 只会让浏览器停止等待,不能保证服务器停止处理。服务器可能已经开始扣款、写入数据库或调用第三方支付接口。
支付业务还需要注意
浏览器计时器只能改善页面体验,不能作为判断支付状态的最终依据。后台请求超时不代表支付失败。第三方可能已经扣款,只是应用没有及时收到结果。
支付接口至少需要做到:
- 使用订单号或支付请求 ID 保证幂等,避免重复提交导致重复扣款;
- 明确保存“处理中”“成功”“失败”等状态;
- 允许首页或结果页根据订单号查询最终状态;
- 耗时的支付操作尽量交给后台任务处理,前端通过轮询、回调或消息通知获取结果;
- 不要把
118000毫秒当成精确的服务端超时边界。后台标签页、设备休眠或系统调度都可能让浏览器计时器延迟执行; - 禁用提交按钮或设置状态标志,防止用户连续点击并发出重复请求。
更可靠的结构是由前端提交支付任务,服务器尽快返回订单或任务标识,前端再查询处理结果。超过前端等待时间后,只结束等待并返回安全页面,不要直接把支付判定为失败。
备注:内容仅供参考。
