
Доброго времени суток, уважаемые хабравчане!
Возникла передо мной сегодня задача генерации 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
- Вручную ставим свойство
URL.urlEncode=false;
- Вызываем метод
URL.update();
test = new URL({"path":"index.php", "params":{"param1":"value1", "param2":"значение параметра&"}}, false);
test.url;
//index.php?param1=value1¶m2=значение параметра&
test2 = new URL({"path":"index.php", "params":{"param1":"value1", "param2":"значение параметра&"}});
test2.url;
//index.php?param1=value1¶m2=%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¶m2=значение параметра&
Ну и чтобы было удобно — метод для перехода по сгенерированной ссылке:
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 не портил жизнь.