URL.js или дружим JavaScript с обработкой ссылок

в 11:23, , рубрики: development, front-end, javascript, library, parse, plugin, url, web-разработка

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 не портил жизнь.

Автор: xobotyi

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js