Рефакторинг ajax-запросов

Ajax

Условие

Есть одностраничный интерфейс, построенный с помощью Emberjs, EmberData, jQuery, HandlebarsJS, Twitter Bootstrap и собранный в одно целое при помощи brunch. Ajax-запросы идут через «стандартный» $.ajax() метод. Таких запросов много — ~100. У них всех callback’и сделаны как анонимные функции. Да, интерфейс может работать в двух режимах — тестовом и рабочем. Отличаются эти режимы как раз адресами, на которые посылаются ajax-запросы. В test-mode запросы идут на обычные JSON-файлы, а в нормальном режиме — на динамически формируемые url’ы. Получается, что в коде интерфейса в большом количестве мест раскиданы проверки вида:

var url;
if(App.testMode) {
	url = 'mock_url';
}
else {
	url = 'real_url';
}

Сами же запросы выглядят как:

$.ajax({
	url: url,
	type: 'GET/POST/PUT/DELETE',
	data: '...',
	dataType: '...',
	timeour: App.timeout,
	success: function(data) {/*дохрена кода*/},
	error: function(request, ajaxOptions, error){/*дохрена кода*/},
	statusCodes: require('data/statusCodes') // привет brunch
});

statusCodes для всех запросов одинаковый, success, error обработчики — функции, которые в таком виде невозможно протестировать без выполнения самого запроса (а очень хочется).

Еще стоит учитывать, что над интерфейсом работает больше 10 человек, которые находятся на разных континентах и говорят на разных языках.

Что делать?

Заставить всех писать success/error-handlers как обычные функции и в $.ajax подставлять их имена — не реально. Да и есть куча кода уже написана в «старом стиле». Вариант — своя обертка над $.ajax() и переписывание запросов на новый лад.

Обертку назвали App.ajax (думали не долго 🙂 ). Это объект, у которого есть всего один метод — send. Этот метод принимает только один агрумент, который является config-объектом вида:

{
	name: '........',
	sender: some_object,
	data: {
		field1: value1,
		field2: value2,
		field3: value3
	},
	beforeSend: 'method1Name',
	success: 'method2Name',
	error: 'method3Name'
}

Про name будет сказано ниже. sender — это объект, для которого будут вызваны обработчики success/error и т.д. То есть, тут мы передаем имена методов. Так что проблема анонимных функций уже пропадает сама собой. data — объект с данными, которые подставляются в url или же передаются в самом запросе. Обязательными полями в config-объекте являются только name и data.

Еще надо убрать большое количество проверок App.testMode. Лучше эту проверку вынести в сам App.ajax. Она у нас определяла url. Сами же url’ы у нас тоже были разбросаны по всем файлам. Имеет смысл их тоже вынести в App.ajax. Для этого в файле с App.ajax создадим объект вида:

var urls = {
	'%some_name1%': {
		'real': 'real_url1',
		'mock': 'mock_url1.json'
	},
	'%some_name2%': {
		'real': 'real_url2',
		'mock': 'mock_url2.json'
	}
	...
}

Здесь %some_name1%, %some_name2% — это то самое name, которое передается в config-объекте в метод send, real — рабочая ссылка, mock — testMode ссылка. Таким образом, нам удается избавиться от однотипных проверок для получения нужного url. У нас будет всего одна проверка в самом send.

Так, у нас же еще есть data, которая передается в запросе или подставляется в url-запроса. По-началу казалось, что совмещать все данные в одном объекте — плохая идея, но по мере внедрения это ощущение пропало. Формирование url’a с подстановкой в него нужных параметров — задача общая для всех запросов, так что можно сделать отдельный метод, который вызывать для каждого запроса. Было решено, что значения, которые надо заменить в url, помечаются фигурными скобками. Пример:

'{stack2Version}/stackServices?fields=configurations/StackConfigurations/type';

Метод для подстановки реальных значений выглядит так:

/**
 * Replace data-placeholders to its values
 *
 * @param {String} url
 * @param {Object} data
 * @return {String}
 */
var formatUrl = function(url, data) {
	var keys = url.match(/\{\w+\}/g);
	keys = (keys === null) ? [] :  keys;
	if (keys) {
		keys.forEach(function(key){
			var raw_key = key.substr(1, key.length - 2);
			var replace;
			if (!data[raw_key]) {
				replace = '';
			}
			else {
				replace = data[raw_key];
			}
			url = url.replace(new RegExp(key, 'g'), replace);
	    });
  	}
  	return url;
};

Все, с url’ами разобрались. Остаются параметры запроса и данные. Очевидно, что для каждого запроса они свои (хотя для некоторых их вовсе нет). А для каждого url у нас уже есть уникальная «ячейка» в виде поля в объекте urls. Может, стоит и остальное туда запихнуть? Практика показала, что стОит. Теперь urls может выглядеть так:

var urls = {
	'%some_name1%': {
		'real': 'real_url',
		'mock': 'mock_url.json',
		'format': function (data, opt) {
			return {
				type: 'PUT',
				async: false,
				data: {
					field1: data.some_value1,
					field2: data.some_value2
				}
			};
		}
	}
	...
}

Функция format не является обязательной, но, если она объявлена, то она будет вызвана. Она возвращает объект с параметрами, которые будут дополнять (или перезаписывать) стандартные параметры запроса. Тут можно объявить тип запроса, асинхронность, тип данных и т.д.

Собираем все вместе

Что выполняется в send:

  1. Простая проверка переданного config-объекта
  2. Дополнение config.data стандартными значениями
  3. Формирование самого объекта-запроса
  4. Добавление необходимых callback’ов
  5. Выполнение самого запроса

Теперь это же, но в виде кода:

  1. Простая проверка переданного config-объекта

    if (!config.sender) {
    	console.warn('Ajax sender should be defined!');
    	return null;
    }

    sender нам нужен всегда. Даже, если нет callback’ов.

  2. Дополнение config.data стандартными значениями

    // default parameters
    var params = {
    	param1: 'value1',
    	param2: 'value2'
    };
    // extend default parameters with provided
    if (config.data) {
    	jQuery.extend(params, config.data);
    }

    Что бы не заставлять того, кто будет использовать App.ajax, передавать в каждом запросе эти параметры.

  3. Формирование самого объекта-запроса

    var opt = {};
    opt = formatRequest.call(urls[config.name], params);

    formatRequest:

    /**
     * this = object from config
     * @return {Object}
     */
    var formatRequest = function(data) {
    	var opt = {
    		type : this.type || 'GET',
    		timeout : App.timeout,
    		dataType: 'json',
    		statusCode: require('data/statusCodes')
    	};
    	if(App.testMode) {
    		opt.url = formatUrl(this.mock, data);
    		opt.type = 'GET';
    	}
    	else {
    		opt.url = App.apiPrefix + formatUrl(this.real, data);
    	}
    	if(this.format) {
    		jQuery.extend(opt, this.format(data, opt));
    	}
    	return opt;
    };

    Такая логика была в нашем интерфейсе. Вначале определяли дефолтные значения для типа запроса, таймаута, типа данных, кодов ответов. Потом формировали url. Если для данного url’а была объявлена функция format, то вызывали ее и дополняли (или перезаписывали) дефолтные значения полученным результатом.

  4. Добавление необходимых callback’ов

    // object sender should be provided for processing beforeSend, success and error responses
    opt.beforeSend = function(xhr) {
    	if(config.beforeSend) {
    		config.sender[config.beforeSend](opt, xhr, params);
    	}
    };
    opt.success = function(data) {
    	if(config.success) {
    		config.sender[config.success](data, opt, params);
    	}
    };
    opt.error = function(request, ajaxOptions, error) {
    	if (config.error) {
    		config.sender[config.error](request, ajaxOptions, error, opt);
    	}
    };

    Callback’ами у нас являются методы объекта sender. Все они получают «стандартные» параметры: data для success; request, ajaxOptions, error для error; xhr для beforeSend. Так же каждому из них передается еще и объект с параметрами ajax-запроса (а для некоторых — еще и весь config-объект).

  5. Выполнение самого запроса

    return $.ajax(opt);

    Возвращая результат выполнения $.ajax, мы обеспечиваем возможность использовать конструкции вида:

    App.ajax.send({...}).retry({...}).then(...);

Эпилог

Выполнение ajax-запроса теперь может выглядеть так:

App.ajax.send({
	name: 'cluster.poll',
	sender: this,
	data: {
		cluster: this.get('cluster.name'),
		requestId: this.get('content.requestId')
	},
	success: 'SuccessCallback',
	error: 'ErrorCallback'
});
App.ajax.send({
	name: 'cluster.poll2',
	sender: this
});
App.ajax.send({
	name: 'cluster.poll3',
	sender: this,
	success: 'SuccessCallback'
});

Вариантов много — зависит от задачи. Чего же удалось добиться с помощью такого рефакторинга:

  • Убрали дублирование условных конструкций для определения url
  • Убрали возможность использовать анонимные функции для callback’ов
  • Убрали дублирование формата запроса (статус-коды, типа данных, таймаут и т.д.)
  • Инкапсулировали всю логику выполнения ajax-запросов в одном месте
  • Сделанная обертка легко дополняется при необходимости
  • Появилась возможность тестировать запросы и callback’и отдельно друг от друга.

, , ,

3 комментария
  1. Интересующийся сказал(а):

    Доброе время суток

    Человек я несведущий, поэтому, возможно, скажу несуразность: данным методом вы избавляетесь от ужасных ссылок, типа: site.com/test#!page/c1whb

  2. Интересующийся сказал(а):

    забыл поставить знак вопроса)

    Либо я что-то путаю и таких конструкций у ajax, как я привёл в пример, нет?

  3. KronuS сказал(а):

    Цели данного рефакторинга описаны в списке в самом конце заметки.
    На счет ссылок — они все хранятся в одном месте. Сами они не меняются.

Оставить комментарий

Top ↑ | Main page | Back