image

Доброго времени суток, уважаемые хабравчане!

Возникла передо мной сегодня задача генерации GET-параметров и всего URL в целом, на стороне клиента, прям вот щас, без возможности «поговорить» с сервером. Сразу оговорюсь, про этот пост я узнал вот прям перед написанием данной статьи ибо сначала закончил писать, а потом уже прибег к поиску, да и пост тот — не со всем про то же самое, что у меня.

Итак, к делу.

Задача и проблемы


Проблемы — те же что и в посте, который я привел выше:
  • Невозможность использовать window.location для «приготовления» URL;
  • Нельзя работать сразу с несколькими window.location в силу политики безопасности браузеров;
  • Отсутствие известных готовых решений ( да и сейчас, уже апосля, я не нашел подобного кода )

Задачи которые я поставил перед собой:
  • Удобный синтаксис
  • Возможность как читать части URL так и изменять их
  • Работа с GET-параметрами
  • Кроссбраузерность и универсальность


Писал я на чистейшем JavaScript, причем без использования prototype.__defineGetter__ или prototype.__defineSetter__ в угоду кроссбраузерности ибо IE < 9 такого не умеет. Более подробно про getters/setters написано в этом посте.

Для тех кому интересно — сядем разберем, а кому надо готовое решение — милости прошу в конец поста, ссылки на скачивание — там.

Приступим! Раньше сядем — раньше выйдем.


Конструктор


Код конструктора
var URL = function( param, param2 ){
	param = param || false;
	param2 = ( param2 === false ) ? false : true;
	
	this.urlEncode = param2;
	this.data = { scheme: false, user: false, pass: false, host: false, port: false, path: false, query: false, params: {}, fragment: false };
	
	if( typeof(param) == 'string' ){
		this.url = param;
		
		this.parse();
	} else if ( typeof(param) == 'object' ){
		for(var key in param){
			if( this.data.hasOwnProperty( key ) ){
				if( param[ key ] || ( key == 'params' && typeof(param.params) == 'object' ) )
					this.data[ key ] = param[ key ];
			}
		}
		
		this.update();
	}
}


Подробнее

  • Как я уже говорил — необходима универсальность. Т.е. возможность как работать с неполными урлами, так и вообще создавать оные с нуля, а поэтому мы можем как передать исходный URL в конструктор, передать туда хэш с нужными нам, соответствующими параметрами или же вовсе, не передавать ничего.
  • Все параметры урла хранятся в хэше ( в JS это просто объект с параметрами ), связано это с getters/setters о которых чуточку позже. Именованы они в стиле parse_url() из PHP, мне так просто удобнее.


Парсинг


Надо парсить уже имеющийся URL, делать мы это будем при помощи RegExp. Нет, можно конечно все обрабатывать при помощи str.split(), но это, как мне кажется — особый вид фетишизма.
regExp = /^(?:([a-z0-9_\-\.]+):\/\/)*(?:([a-z0-9_\-\.]+)(?:\:)*([a-z0-9_\-\.]+)*\@)*([a-z0-9][a-z0-9_\-\.]+)(?:\:([\d]+))*(?:\/([^?#]*))*(?:\?([^?#]*))*(?:\#([^?#]*))*/gi;

И по частям
  • (?:([a-z0-9_\-\.]+):\/\/)* — SCHEME, если верить википедии, то схема имеет вид ххх:// причем, там могут быть и - и _. В угоду универсальности, установлен * т.е. схема может быть и не указана.
  • (?:([a-z0-9_\-\.]+)(?:\:)*([a-z0-9_\-\.]+)*\@)* — USER:PASSWORD, пароля без юзернейма не бывает, а юзернейм без пароля бывает.
  • ([a-z0-9][a-z0-9_\-\.]+) — HOST, насколько я знаю, начинаться доменное имя может только с буквы/цифры, а дальше уже могут идти и — и _ и. Более того, не бывает доменных имен короче 6 символов, но ведь ссылки то бывают и внутрисетевые, где хостнеймами как хочешь так и рулишь, та что сойдет и 1+ символ.
  • (?:\:([\d]+))* — PORT, данный параметр опционален,: а далее цифры
  • (?:\/([^?#]*))* — PATH, путь до файла, в общем-то, по-идее, это любое количество любых символов, но, отсечем? и # дабы не спарсить в путь GET-параметры или фрагментарный указатель. Путь может быть и неуказан.
  • (?:\?([^?#]*))* — QUERY, набор GET-параметров, пар ключ=значение. Так же может быть и не указан.
  • (?:\#([^?#]*))* — FRAGMENT, фрагментарный указатель. Если кто не знает — то /index.html#fragment дает команду браузеру проскроллить к DOM-элементу с id="fragment"


Работать, ясное дело, будет на всех языках, понимающих RegExp. Пользуйтесь, не стесняйтесь.

Парсер
	parse: function(){
		this.res = /^(?:([a-z0-9_\-\.]+):\/\/)*(?:([a-z0-9_\-\.]+)(?:\:)*([a-z0-9_\-\.]+)*\@)*([a-z0-9][a-z0-9_\-\.]+)(?:\:([\d]+))*(?:\/([^?#]*))*(?:\?([^?#]*))*(?:\#([^?#]*))*/gi.exec( this.url );
		
		this.data.scheme = this.res[ 1 ] || false;
		this.data.user = this.res[ 2 ] || false;
		this.data.pass = this.res[ 3 ] || false;
		this.data.host = this.res[ 4 ] || false;
		this.data.port = this.res[ 5 ] || false;
		this.data.path = this.res[ 6 ] || false;
		this.data.query = this.res[ 7 ] || false;
		this.data.fragment = this.res[ 8 ] || false;
		
		if( this.data.query ){
			this.parts = this.data.query.split( '&' );
			for(  var i = 0; i < this.parts.length; i++ ){
				param = this.parts[ i ].split( '=' );
				this.data.params[ param[ 0 ] ] = decodeURIComponent( param[ 1 ] );
			}
		}
		

		delete this.res;	
		delete this.parts;	
	}


Тут ничего ничего сложного: разбиение по указанному выше regExp и сохранение данных в хеш this.data
Разве что, я упоминал ранее — необходима удобная работа с GET-параметрами урла, а посему разбиваем query при помощи split ( split() в данном случае «дешевле» чем regExp ) и сохраняем это в тот же пресловутый хэш. Стоит отметить использование decodeURIComponent, ведь GET-параметры могут быть urlencoded.

Вариант 1. «По красоте»


Getters/Setters


Для удобной работы с чтением/изменением параметров я решил выбрать JS way геттеры и сеттеры. T.e. метод по названию свойства и если метод вызывается с указанием параметра — это setter, если без параметра — это getter.
Объявлять я их буду через URL.prototype = { } дабы не плодить в памяти избыточные экземпляры метода.
В пример приведу один метод, в силу того что они похожи:
	scheme: function( param ){	
		if( typeof( param ) != 'undefined' ){
			this.data.scheme = param;
			
			return this.update();
		} else {
			return this.data.scheme ? this.data.scheme : false;
		}
	}

Замечу, что в случае изменения значения возвращается не String, а Object сделано это для того, чтобы можно было писать цепочки сеттеров:
var url = new URL();

url.scheme('https').host('example.com').path('index.php').params({'p1':"v1", 'p2':"в2"}).url;
// вернет: https://example.com/index.php?p1=v1&p2=%D0%B22


Отдельно остановимся на геттер/сеттере для свойства params
	params: function( param1, param2 ){
		if( typeof( param1 ) != 'undefined' ){
			if( typeof( param1 ) == 'string' ){
				if( typeof( param2 ) != 'undefined' && ( param2 == '' || param2 === false ) ){
					if( this.data.params.hasOwnProperty( param1 ) ){
						delete this.data.params[ param1 ];
					}
				} else if( typeof( param2 ) != 'undefined' ){
					this.data.params[ param1 ] = param2;
				} else{
					return this.data.params[ param1 ] ? this.data.params[ param1 ] : false;
				}
			} else if( typeof( param1 ) == 'object' ){
				for( var key in param1 ){
					if( typeof( param1[ key ] ) != 'undefined' && ( param1[ key ] == '' || param1[ key ] === false ) ){
						if( this.data.params.hasOwnProperty( key ) )
							delete this.data.params[ key ];
					} else{
						this.data.params[ key ] = param1[ key ];
					}
				}
			}
			
			return this.update();
		} else {
			return this.data.params ? this.data.params : false;
		}
	}

Как видим — оба параметра опциональные.
И как я говорил — я ставил перед собой целью — удобство работы с GET-параметрами, а значит мы должны уметь:
  • Читать
  • Изменять
  • Удалять

как отдельно взятый параметр так и группы параметров.

Соответственно синтаксис будет таков:
  • Не передается ни один параметр — читаем все GET-параметры
  • Передается только первый параметр — читаем один GET-параметр
  • Передается два параметра — пишем GET-параметр с именем param1 и значением param2
  • В качестве значения параметра передается пустое значение или false — указанный GET-параметр удаляется


Собираем URL обратно


Как вы заметили, в геттерах вызывается this.update() выполняет он 2 функции:
  • Собирает URL воедино, в свойстве url
  • Обновляет свойство query при манипуляциях с GET-параметрами

Код сборщика
	update: function(){
		this.data.query = '';
		for( var key in this.data.params ){
			this.data.query += this.urlEncode ? key+'='+encodeURIComponent( this.data.params[ key ] )+'&' : key+'='+this.data.params[ key ]+'&';
		}
		
		if( this.data.query )
			this.data.query = this.data.query.slice( 0, -1 );
		
		this.url = '';
		this.url += this.data.scheme ? this.data.scheme+'://' : '';
		this.url += this.data.user ? this.data.user+':' : '';
		this.url += this.data.pass ? this.data.pass+'@' : '';
		this.url += this.data.host ? this.data.host+'/' : '';
		this.url += this.data.path ? this.data.path : '';
		this.url += this.data.query ? '?'+this.data.query : '';
		this.url += this.data.fragment ? '#'+this.data.fragment : '';
		
		return this;
	}


Стоит отметить, что при сборке GET-параметров, значения параметров преобразуются в escape-последовательность.
Во-первых: это правильно.
Во-вторых: если мы GET-параметром передаем данные вводимые пользователем, то вставленный юзером амперсанд разрушит последовательность ключ-значение и все покатится в тартарары.

Ну, а если уж, прям кровь из носу, вам не нужна urlencoded строка — у вас два варианта:
Передаем вторым параметром в конструкторе false
  1. Вручную ставим свойство URL.urlEncode=false;
  2. Вызываем метод URL.update();

test = new URL({"path":"index.php", "params":{"param1":"value1", "param2":"значение параметра&"}}, false);
test.url;
//index.php?param1=value1&param2=значение параметра&

test2 = new URL({"path":"index.php", "params":{"param1":"value1", "param2":"значение параметра&"}});
test2.url;
//index.php?param1=value1&param2=%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D0%B5%20%D0%BF%D0%B0%D1%80%D0%B0%D0%BC%D0%B5%D1%82%D1%80%D0%B0%26

test2.urlEncode=false;
test2.update().url;
//index.php?param1=value1&param2=значение параметра&


Ну и чтобы было удобно — метод для перехода по сгенерированной ссылке:
	go: function(){
		if(!this.data.scheme && this.data.host)
			this.data.scheme = 'http';
		
		window.location.href = this.update().url;
	}

Как видно: если не указана схема, но указан хост — автоматически подставляется схема http как самая распространенная.
Далее происходит обновление ссылки и переход по оной.

Расширяем объект String


По идее, на этом можно было бы закончить. Но, мне показалось что ��ыло бы удобно работать прямо со строковыми переменными без явного создания экземпляра объекта (как бы странно это не звучало, но, в JS нет классов, как таковых).

Как обычно приведу пример одного метода:
String.prototype.scheme = function( param ){
	var url = new URL( this.valueOf() );
	
	if( typeof( param ) != 'undefined' ){
		url.scheme( param );
		result = url.url;
	} else{
		result = url.scheme();
	}
	delete url;
	
	return result;
}

В общем-то код просто передает параметры в соответствующий метод объекта URL.
Но некоторым может показаться странным тот момент, что я каждый вызов по-новой создаю и удаляю объекты URL и делаю только одно действие, причем это действие не меняет значения переменной над которой оно производится.
Вот тут то и кроется самое главное неудобство объекта String, нельзя менять значение существующей переменной. С ней вообще ничего нельзя сделать, ВСЕГДА создается новая переменная. А по-этому каждый раз создается новый объект и возвращается переменная типа String.
Цепочки конечно же поддерживаются:
url = 'example.com';

url.scheme('https').path('index.php').params({'p1':"v1", 'p2':"в2"});
// вернет: https://example.com/index.php?p1=v1&p2=%D0%B22


Вариант 2. «По Фен-Шуй»


Если предыдущий вариант, скажем так, был «красив»в использовании, то данный вариант, будет лаконичен. как с точки зрения кода, так и с точки зрения использования.

Getters/Setters


Так вот, getter/setter в данном случае будет один на всё, ну то есть совсем.
	val: function( key, param, param2 ){		
		if( this.data.hasOwnProperty( key ) ){
			if( typeof( param ) == 'undefined' ){
				return this.data[ key ] ? this.data[ key ] : false;
			} else if( typeof( param ) != 'undefined' ){
				if( key == 'params' ){
					if( typeof( param ) == 'string' ){
						if( typeof( param2 ) != 'undefined' ){
							this.data[ key ][ param ] = param2;
						} else{
							return this.data[ key ][ param ] ? this.data[ key ][ param ] : false;
						}
					} else if( typeof( param ) == 'object' ){
						for( var keys in param ){
							if( typeof( param[ keys ] ) != 'undefined' && ( param[ keys ] == '' || param[ keys ] === false ) ){
								if( this.data[ key ].hasOwnProperty( keys ) ){
									delete this.data[ key ][ keys ];
								}
							} else{
								this.data[ key ][ keys ] = param[ keys ];
							}
						}
					}
				} else{
					this.data[ key ] = param;
				}
				
				return this.update();
			}
		} else
			return 'undefined';
	}


Расширяем объект String


Идентичная ситуация и с расширением объекта String, только кода поменьше, т.к. этот метод всего лишь транспортирует параметры в URL.val();

Подведение итогов


Итак, на выходе мы имеем либу, дающую нам возможность адекватно работать с URL, причем не просто парсить, но и менять отдельные участи URL. Это уже не говоря о весьма удобном, на мой взгляд, инструменте для работы с GET-параметрами.

Плюсы и минусы подходов

Вариант 1

Плюсы:
  • Хорошая читаемость
  • Удобно применять

Минусы:
  • 8,75кб ( без сжатия и удаления разрядки )
  • 360 строк кода для в общем-то небольшого расширения функционала
  • Если можно так выразиться — громоздкость по сравнению с вариантом 2


Вариант 2

Плюсы:
  • Всего 144 строчки кода
  • Вес 4.25кб ( без сжатия и удаления разрядки )
  • Простота и лаконичность конструкций

Минусы:
  • Немножко сложно читать


Скачать исходники обоих вариантов можно тут: [ Вариант 1 || Вариант 2 ]. смысла выкладывать на гитхаб не вижу, ибо всего 1 файл.
Поддержка:
  • Да в общем-то абсолютно везде где работает JavaSript, ибо плагин написан на чистом, нативном JS, без использования magic функций, которые не поддерживаются старыми браузерами.


А за сим — откланяюсь, искренне надеюсь что мой пост принесет кому-то пользу.
Всем хорошего кода, больше сна и чтобы IE не портил жизнь.