Статьи

Стратегии для работы с несколькими вызовами Ajax

Давайте рассмотрим довольно тривиальное, но, вероятно, типичное приложение на основе Ajax. У меня есть серия кнопок:

Shot1

При нажатии каждая кнопка попадает в сервис на моем сервере приложений и получает некоторые данные. В моем случае просто простое имя:

Shot2

Код для этого довольно прост. (И обратите внимание — для целей этой записи в блоге я сохраняю вещи очень простыми и включаю свой JavaScript на HTML-странице. Пожалуйста, храните ваш HTML и JavaScript в разных файлах!)

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width">
</head>
<body>

<button data-prodid="1" class="loadButton">Load One</button>
<button data-prodid="2" class="loadButton">Load Two</button>

<div id="resultDiv"></div>

<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script>
<script>
$(document).ready(function() {
	$result = $("#resultDiv");
	
	$(".loadButton").on("click", function(e) {
		var thisId = $(this).data("prodid");
		console.log("going to load product id "+thisId);
		$result.text("");
		$.getJSON("service.cfc?method=getData",{id:thisId}, function(res) {
			console.log("back with "+JSON.stringify(res));
			$result.text("Product "+res.name);
		});
	});
});
</script>
</body>
</html>

Я предполагаю, что это имеет смысл для всех, так как это симпатичный Ajax с jQuery, но если это не так, просто включите ниже комментарий. Итак, это  работает , но у нас есть небольшая проблема. Что происходит, когда пользователь нажимает обе кнопки практически одновременно? Ну, вы бы сказали, что последний выигрывает, верно? А ты уверен? Что, если что-то пойдет не так (база данных gremlin — всегда вините базу данных) и последний удар  первым  вернет?

Untitled2

То, что вы видите (надеюсь, что все еще новичок в создании анимированных GIF-файлов), — это то, что пользователь нажимает первую кнопку, затем вторую и видит сначала результат от второй кнопки, а затем мигает первая.

Теперь, чтобы быть справедливым, вы могли бы просто обвинить пользователя. Я  все  за то, чтобы обвинять пользователя. Но как мы можем предотвратить это?

Одна стратегия состоит в том, чтобы отключить все кнопки, которые вызывают этот конкретный запрос Ajax, до тех пор, пока запрос не будет завершен. Давайте посмотрим на эту версию.

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width">
</head>
<body>

<button data-prodid="1" class="loadButton">Load One</button>
<button data-prodid="2" class="loadButton">Load Two</button>

<div id="resultDiv"></div>

<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script>
<script>
$(document).ready(function() {
	$result = $("#resultDiv");
	
	$(".loadButton").on("click", function(e) {
		//disable the rest
		$(".loadButton").attr("disabled","disabled");
		var thisId = $(this).data("prodid");
		console.log("going to load product id "+thisId);
		$result.text("Loading info...");
		$.getJSON("service.cfc?method=getData",{id:thisId}, function(res) {
			console.log("back with "+JSON.stringify(res));
			$(".loadButton").removeAttr("disabled");
			$result.text("Product "+res.name);
		});
	});
});
</script>
</body>
</html>

I’ve added a simple call to disable all the buttons based on class. I then simple remove that attribute when the Ajax request is done. Furthermore, I also include some text to let the user know that – yes – something is happening – and maybe you should just calm the heck down and wait for it. The result makes it more obvious that something is happening and actively prevents the user from clicking the other buttons.

Untitled3

Another strategy would be to actually kill the existing Ajax request. This is rather simple. The native XHR object has an abort method that will kill it, and jQuery’s Ajax methods returns a wrapped XHR object that gives us access to the same method.

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width">
</head>
<body>

<button data-prodid="1" class="loadButton">Load One</button>
<button data-prodid="2" class="loadButton">Load Two</button>

<div id="resultDiv"></div>

<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script>
<script>

$(document).ready(function() {
	$result = $("#resultDiv");

	var xhr;
	var active=false;

	$(".loadButton").on("click", function(e) {
		var thisId = $(this).data("prodid");
		console.log("going to load product id "+thisId);
		$result.text("Loading info...");
		
		if(active) { console.log("killing active"); xhr.abort(); }
		active=true;
		xhr = $.getJSON("service.cfc?method=getData",{id:thisId}, function(res) {
			console.log("back with "+JSON.stringify(res));
			$result.text("Product "+res.name);
			active=false;
		});
	});
});
</script>
</body>
</html>

I use two variables, xhr and active, so that I can track when an active xhr request. There are other ways to track the status of the XHR object – for example, via readyState – but a simple flag seemed to work best. Obviously you could do it differently but the main idea (“If active, kill it”), provides an alternative to the first method.

When using this, you can actually see the requests killed in dev tools:

Untitled4

Any comments on this? How are you handling this yourself in your Ajax-based applications?

p.s. As a quick aside, Brian Rinaldi shared with me a cool little UI library that turns buttons themselves into loading indicators: Ladda