Хочу поделиться одной полезной утилиткой, написанной на pure JavaScript, — URL. По сути это небольшой парсер URL'ов, работающий почти как
А заодно скажу пару слов про getters & setters в JavaScript.
UPD1: по просьбам трудящихся, вынесу сюда примеры:
Работает в FF3+ (может и в 2+, не пробовал) и в IE6+ ( и это — моё ноу-хау :-) ).
Разобрана в статье также полностью кросс-браузерная реализация, но в использовании — немного более громоздкая:
Да, и я привожу свой листинг полностью, извиняйте, так надо.
UPD2: кратко объясню цели моей библиотеки:
Данная тулза возникла именно из практических нужд.
И я видел уже несколько кустарных разработок подобного назначения в больших JS-проектах, таких, как TinyMCE. В RTE часто имеешь дело со ссылками на ресурсы. И эти ссылки нужно обрабатывать в real-time.
Конкретно мне надо было распарсить текущий URL и изменить/добавить новый параметр в search, с последующим редиректом.
Можно придумать ещё.
В чём же, собственно, проблема? Проблема в том, что:
Я упомянул про нетривиальность поведения. Вот она в чём:
При изменении любой из частей URL должны обновляться другие.
По сути я буду создавать подобие
Без комментариев :)
Основную работу выполнять будет, конечно же, Regular Expression:
Теперь более подробно:
Как нетрудно догадаться, этот RegExp будет работать не только в JavaScript, но и в сотне других языков. Пользуйтесь на здоровье! ;)
Всё бы ничего, но мы обязуем пользователя постоянно вызывать
К сожалению, в данной реализации от этого не уйти. Но можно ведь всё сделать иначе :)
А сейчас я предложу уже приемлемый вариант. Нам нужны getters & setters. Самый очевидный путь — для каждого параметра создать методы (н-р)
Сделаем это in more JavaScript way. Будет один метод
Настоящие же данные мы спрячем в замыкании.
В целом — код есть self-documented, поэтому объясню лишь ключевые моменты:
Ну и сразу к примерам. Ведь главное — посмотреть эту штуку в действии.
Вот так. Всё работает.
Вообщем-то это вполне рабочая версия. Назовём её version 1.0 final.
А теперь перейдём к version 2.0 alpha, или в игру вступают tru getter'ы и setter'ы.
Приведу код, а потом рассмотрю интересные моменты.
Рассмотрим создание getter/setter'ов:
Работает в FF3+, IE6+. Можно докрутить для Safari/Chrome. Насчёт Opera — не уверен. Необходимо RTFM.
Надеюсь, я сделал что-то полезное и не впустую потратил день своей жизни на написание этой статьи :-)
P.S.: да, я думаю написать отдельную статейку, посвящённую getter'ам и setter'ам в разных браузерах. Не Firefox'ом одним живы (небольшой пи-ар: чтобы не нагружать Habrahabr моим потоком мыслей — милости прошу на мой блог — http://web-by-kott.blogspot.com/. Там пока-что пустынно, но я только начинаю)
window.location
, но не перезагружающий страницу браузера при манипуляциях.А заодно скажу пару слов про getters & setters в JavaScript.
UPD1: по просьбам трудящихся, вынесу сюда примеры:
// Пусть текущий URL = 'http://my.site.com/somepath/'
var u = new URL('relative/path/index.html')
u.href // my.site.com/somepath/relative/path/index.html
u.href = '/absolute/path.php?a=8#some-hash'
u.href // my.site.com/absolute/path.php?a=8#some-hash
u.hash // #some-hash
u.protocol = 'https:'
u.href // my.site.com/absolute/path.php?a=8#some-hash
u.host = 'another.site.com:8080'
u.href // another.site.com:8080/absolute/path.php?a=8#some-hash
u.port // 8080
// и так далее, и тому подобное
* This source code was highlighted with Source Code Highlighter.
Работает в FF3+ (может и в 2+, не пробовал) и в IE6+ ( и это — моё ноу-хау :-) ).
Разобрана в статье также полностью кросс-браузерная реализация, но в использовании — немного более громоздкая:
// Пусть текущий URL = 'http://my.site.com/somepath/'
var u = new URL('relative/path/index.html')
u.href() // my.site.com/somepath/relative/path/index.html
u.href('/absolute/path.php?a=8#some-hash')
u.href() // my.site.com/absolute/path.php?a=8#some-hash
// и т.д.
* This source code was highlighted with Source Code Highlighter.
Да, и я привожу свой листинг полностью, извиняйте, так надо.
UPD2: кратко объясню цели моей библиотеки:
Данная тулза возникла именно из практических нужд.
И я видел уже несколько кустарных разработок подобного назначения в больших JS-проектах, таких, как TinyMCE. В RTE часто имеешь дело со ссылками на ресурсы. И эти ссылки нужно обрабатывать в real-time.
Конкретно мне надо было распарсить текущий URL и изменить/добавить новый параметр в search, с последующим редиректом.
Можно придумать ещё.
Проблема
В чём же, собственно, проблема? Проблема в том, что:
- Мы не можем использовать объект
window.location
, т.к. он перезагружает текущую страницу при малейших изменениях - Мы не можем создать ещё один такой же объект через конструктор
Location
— атата! запрещено браузероводами! - Сам объект довольно нетривиален в поведении
- Ну и я не нашёл никакой готовой реализации :)
Я упомянул про нетривиальность поведения. Вот она в чём:
При изменении любой из частей URL должны обновляться другие.
Разбор на части
По сути я буду создавать подобие
window.location
, поэтому и обозначения тащу оттуда. Разберём пример:Без комментариев :)
Как ни крутись, без RegExp не обойтись
Основную работу выполнять будет, конечно же, Regular Expression:
var pattern = "^(([^:/\\?#]+):)?(//(([^:/\\?#]*)(?::([^/\\?#]*))?))?([^\\?#]*)(\\?([^#]*))?(#(.*))?$";
* This source code was highlighted with Source Code Highlighter.
Теперь более подробно:
var pattern =
// Match #0. URL целиком (#0 - это HREF, в терминах window.location).
// Например, #0 == "https://example.com:8080/some/path/index.html?p=1&q=2&r=3#some-hash"
"^" +
// Match #1 & #2. SCHEME (#1 - это PROTOCOL, в терминах window.location).
// Например, #1 == "https:", #2 == "https"
"(([^:/\\?#]+):)?" +
// Match #3-#6. AUTHORITY (#4 = HOST, #5 = HOSTNAME и #6 = PORT, в терминах window.location)
// Например, #3 == "//example.com:8080", #4 == "example.com:8080", #5 == "example.com", #6 == "8080"
"(" +
"//(([^:/\\?#]*)(?::([^/\\?#]*))?)" +
")?" +
// Match #7. PATH (#7 = PATHNAME, в терминах window.location).
// Например, #7 == "/some/path/index.html"
"([^\\?#]*)" +
// Match #8 & #9. QUERY (#8 = SEARCH, в терминах window.location).
// Например, #8 == "?p=1&q=2&r=3", #9 == "p=1&q=2&r=3"
"(\\?([^#]*))?" +
// Match #10 & #11. FRAGMENT (#10 = HASH, в терминах window.location).
// Например, #10 == "#some-hash", #11 == "some-hash"
"(#(.*))?" + "$";
* This source code was highlighted with Source Code Highlighter.
Как нетрудно догадаться, этот RegExp будет работать не только в JavaScript, но и в сотне других языков. Пользуйтесь на здоровье! ;)
Попытка №1
function URL(url) {
url = url || "";
this.parse(url);
}
URL.prototype = {
// Если меняем this.href, не забываем вызывать после этого this.parse()
href: "",
// Если меняем что-то из следующего, не забываем вызывать после этого this.update()
protocol: "",
host: "",
hostname: "",
port: "",
pathname: "",
search: "",
hash: "",
parse: function(url) {
url = url || this.href;
var pattern = "^(([^:/\\?#]+):)?(//(([^:/\\?#]*)(?::([^/\\?#]*))?))?([^\\?#]*)(\\?([^#]*))?(#(.*))?$";
var rx = new RegExp(pattern);
var parts = rx.exec(url);
this.href = parts[0] || "";
this.protocol = parts[1] || "";
this.host = parts[4] || "";
this.hostname = parts[5] || "";
this.port = parts[6] || "";
this.pathname = parts[7] || "/";
this.search = parts[8] || "";
this.hash = parts[10] || "";
this.update();
},
update: function() {
// Плюшка для protocol - если не указан, берём текущий
if (!this.protocol)
this.protocol = window.location.protocol;
// Плюшки для relative pathname/URL - если задаём relative, то "добавляется" к текущему
this.pathname = this.pathname.replace(/^\s*/g, '');
if (!this.host && this.pathname && !/^\//.test(this.pathname)) {
// Если честно, это не лучший вариант. Но тут лучше я не придумал.
var _p = window.location.pathname.split('/');
_p[_p.length - 1] = this.pathname;
this.pathname = _p.join('/');
};
// Плюшка для hostname - если не указан, берём текущий
if (!this.hostname)
this.hostname = window.location.hostname;
this.host = this.hostname + (("" + this.port) ? ":" + this.port : "");
this.href = this.protocol + '//' + this.host + this.pathname + this.search + this.hash;
},
/**
* Есть такой метод у window.location. Переход по заданому URL.
*/
assign: function(url) {
this.parse(url);
window.location.assign(this.href);
},
/**
* Есть такой метод у window.location. Переход по заданому URL, но без внесения в history
*/
replace: function(url) {
this.parse(url);
window.location.replace(this.href);
}
}
* This source code was highlighted with Source Code Highlighter.
В деталях
- Как видим, присутствуют привычные для
window.location
аттрибутыhref, port, hash
и т.д. - Присутствуют также привычные для
window.location
методыassign(...), replace(...)
- Метод
parse(...)
делает главную работу — парсит URL на составные части. - И метод
update(...)
— обновляет все части, если одна из них была изменена.
Всё бы ничего, но мы обязуем пользователя постоянно вызывать
update(...)
и parse(...)
после изменения любого кусочка URL (например, port). Это ужасно. Ведь пользователь может забыть это сделать, и тогда всё летит в тар-тарары.К сожалению, в данной реализации от этого не уйти. Но можно ведь всё сделать иначе :)
Попытка №2
А сейчас я предложу уже приемлемый вариант. Нам нужны getters & setters. Самый очевидный путь — для каждого параметра создать методы (н-р)
getProtocol() & setProtocol(newProtocol)
. Но мне такой подход не нравится из-за своей громоздкости.Сделаем это in more JavaScript way. Будет один метод
protocol(...)
и если мы вызываем его без параметров, то это getter, а если с одним параметром, — то setter.Настоящие же данные мы спрячем в замыкании.
var URL;
// Прячем всю реализацию в замыкание. Так надо, т.к. мы прячем служебные функции parseURL и updateURL.
(function() {
URL = function(url) {
// Собственно, данные. Для каждого нового объекта URL - свои, естественно.
var href, protocol, host, hostname, port, pathname, search, hash;
// Минус данного подхода - нам приходится определять методы в конструкторе, а не в прототипе.
// Get/set href - при set вызываем parseURL.call(this),
// т.е. внешняя функция parseURL обрабатывает объект типа URL - this.
this.href = function(val) {
if (typeof val != "undefined") {
href = val;
parseURL.call(this);
}
return href;
}
// Get/set protocol
// Подобно set href, set protocol вызывает updateURL.call(this), который обновляет все параметры.
this.protocol = function(val) {
if (typeof val != "undefined") {
// Плюшка - если protocol не задан, берём из window.location
if (!val)
val = protocol || window.location.protocol;
protocol = val;
updateURL.call(this);
}
return protocol;
}
// Get/set host
// Здесь особенность в том, что host, hostname и port - связаны между собой.
// Поэтому надо делать дополнительную работу при set host.
this.host = function(val) {
if (typeof val != "undefined") {
val = val || '';
var v = val.split(':');
var h = v[0], p = v[1] || '';
host = val;
hostname = h;
port = p;
updateURL.call(this);
}
return host;
}
// Get/set hostname
// Опять учитываем связку host, hostname и port.
this.hostname = function(val) {
if (typeof val != "undefined") {
if (!val)
val = hostname || window.location.hostname;
hostname = val;
host = val + (("" + port) ? ":" + port : "");
updateURL.call(this);
}
return hostname;
}
// Get/set port
// Опять учитываем связку host, hostname и port.
this.port = function(val) {
if (typeof val != "undefined") {
port = val;
host = hostname + (("" + port) ? ":" + port : "");
updateURL.call(this);
}
return port;
}
// Get/set pathname
// С pathname интересно. Я сделал возможность использования
// relative pathname, т.е. если мы будем set'ить pathname,
// и новое значение не будет начинаться с '/', то дополнится текущее.
this.pathname = function(val) {
if (typeof val != "undefined") {
if (val.indexOf("/") != 0) { // relative url
var _p = (pathname || window.location.pathname).split("/");
_p[_p.length - 1] = val;
val = _p.join("/");
}
pathname = val;
updateURL.call(this);
}
return pathname;
}
// Get/set search
this.search = function(val) {
if (typeof val != "undefined") {
search = val;
}
return search;
}
// Get/set hash
this.hash = function(val) {
if (typeof val != "undefined") {
hash = val;
}
return hash;
}
url = url || "";
parseURL.call(this, url);
}
URL.prototype = {
/**
* Есть такой метод у window.location. Переход по заданому URL.
*/
assign: function(url) {
parseURL.call(this, url);
window.location.assign(this.href());
},
/**
* Есть такой метод у window.location. Переход по заданому URL, но без внесения в history
*/
replace: function(url) {
parseURL.call(this, url);
window.location.replace(this.href());
}
}
// Служебная функция, которая разбирает URL на кусочки.
// В предидущей реализации эта ф-ция была методом объекта URL.
// Теперь я её вынес, т.к. пользователь больше никогда не будет её вызывать.
function parseURL(url) {
if (this._innerUse)
return;
url = url || this.href();
var pattern = "^(([^:/\\?#]+):)?(//(([^:/\\?#]*)(?::([^/\\?#]*))?))?([^\\?#]*)(\\?([^#]*))?(#(.*))?$";
var rx = new RegExp(pattern);
var parts = rx.exec(url);
// Prevent infinite recursion
this._innerUse = true;
this.href(parts[0] || "");
this.protocol(parts[1] || "");
//this.host(parts[4] || "");
this.hostname(parts[5] || "");
this.port(parts[6] || "");
this.pathname(parts[7] || "/");
this.search(parts[8] || "");
this.hash(parts[10] || "");
delete this._innerUse;
updateURL.call(this);
}
// Служебная функция, которая обновляет URL при изменении кусочка.
// В предидущей реализации эта ф-ция тоже была методом объекта URL.
// Теперь я её вынес, т.к. пользователь больше никогда не будет её вызывать.
// Заметим, что эта фуекция сильно похудела, её части разошлись по setter'ам.
function updateURL() {
if (this._innerUse)
return;
// Prevent infinite recursion
this._innerUse = true;
this.href(this.protocol() + '//' + this.host() + this.pathname() + this.search() + this.hash());
delete this._innerUse;
}
})()
* This source code was highlighted with Source Code Highlighter.
В целом — код есть self-documented, поэтому объясню лишь ключевые моменты:
- Прототип оскуднел на 2 метода
parse(...)
иupdate(...)
, которые были вынесены, соответственно, в функцииparseURL(...)
иupdateURL(...)
- Также из прототипа ушли все данные (
href, port, host
и т.д.), и поселились в замыкании, созданном конструктором. А работа с ними теперь идёт через getters & setters
Примеры
Ну и сразу к примерам. Ведь главное — посмотреть эту штуку в действии.
// Пусть текущий URL = 'http://my.site.com/somepath/'
var u = new URL('relative/path/index.html')
u.href() // my.site.com/somepath/relative/path/index.html
u.href('/absolute/path.php?a=8#some-hash')
u.href() // my.site.com/absolute/path.php?a=8#some-hash
u.hash() // #some-hash
u.protocol('https:')
u.href() // my.site.com/absolute/path.php?a=8#some-hash
u.host('another.site.com:8080')
u.href() // another.site.com:8080/absolute/path.php?a=8#some-hash
u.port() // 8080
// и так далее, и тому подобное
* This source code was highlighted with Source Code Highlighter.
Вот так. Всё работает.
Вообщем-то это вполне рабочая версия. Назовём её version 1.0 final.
А теперь перейдём к version 2.0 alpha, или в игру вступают tru getter'ы и setter'ы.
Попытка №3
Приведу код, а потом рассмотрю интересные моменты.
var URL;
(function() {
var isIE = window.navigator.userAgent.indexOf('MSIE') != -1;
URL = function(url) {
var data = {href: '', protocol: '', host: '', hostname: '', port: '', pathname: '', search: '', hash: ''};
var gs = {
getHref: function() {
return data.href;
},
setHref: function(val) {
data.href = val;
parseURL.call(this);
return data.href;
},
getProtocol: function() {
return data.protocol;
},
setProtocol: function(val) {
if (!val)
val = data.protocol || window.location.protocol; // update || init
data.protocol = val;
updateURL.call(this);
return data.protocol;
},
getHost: function() {
return data.host;
},
setHost: function(val) {
val = val || '';
var v = val.split(':');
var h = v[0], p = v[1] || '';
data.host = val;
data.hostname = h;
data.port = p;
updateURL.call(this);
return data.host;
},
getHostname: function() {
return data.hostname;
},
setHostname: function(val) {
if (!val)
val = data.hostname || window.location.hostname; // update || init
data.hostname = val;
data.host = val + (("" + data.port) ? ":" + data.port : "");
updateURL.call(this);
return data.hostname;
},
getPort: function() {
return data.port;
},
setPort: function(val) {
data.port = val;
data.host = data.hostname + (("" + data.port) ? ":" + data.port : "");
updateURL.call(this);
return data.port;
},
getPathname: function() {
return data.pathname;
},
setPathname: function(val) {
if (val.indexOf("/") != 0) { // relative url
var _p = (data.pathname || window.location.pathname).split("/");
_p[_p.length - 1] = val;
val = _p.join("/");
}
data.pathname = val;
updateURL.call(this);
return data.pathname;
},
getSearch: function() {
return data.search;
},
setSearch: function(val) {
return data.search = val;
},
getHash: function() {
return data.hash;
},
setHash: function(val) {
return data.hash = val;
}
};
if (isIE) { // IE5.5+
var el=document.createElement('div');
el.style.display='none';
document.body.appendChild(el);
el.assign = URL.prototype.assign;
el.replace = URL.prototype.replace;
var keys = ["href", "protocol", "host", "hostname", "port", "pathname", "search", "hash"];
el.onpropertychange=function(){
var pn = event.propertyName;
var pv = event.srcElement[event.propertyName];
if (this._holdOnMSIE || pn == '_holdOnMSIE')
return pv;
this._holdOnMSIE = true;
for (var i = 0, l = keys.length; i < l; i++)
el[keys[i]] = data[keys[i]];
this._holdOnMSIE = false;
for (var i = 0, l = keys.length; i < l; i++) {
var key = keys[i];
if (pn == key) {
var sKey = 'set' + key.substr(0, 1).toUpperCase() + key.substr(1);
return gs[sKey].call(el, pv);
}
}
}
url = url || "";
parseURL.call(el, url);
return el;
} else if (URL.prototype.__defineSetter__) { // FF
var keys = ["href", "protocol", "host", "hostname", "port", "pathname", "search", "hash"];
for (var i = 0, l = keys.length; i < l; i++) {
(function(i) {
var key = keys[i];
var gKey = 'get' + key.substr(0, 1).toUpperCase() + key.substr(1);
var sKey = 'set' + key.substr(0, 1).toUpperCase() + key.substr(1);
URL.prototype.__defineGetter__(key, gs[gKey]);
URL.prototype.__defineSetter__(key, gs[sKey]);
})(i);
}
url = url || "";
parseURL.call(this, url);
}
}
URL.prototype = {
assign: function(url) {
parseURL.call(this, url);
window.location.assign(this.href);
},
replace: function(url) {
parseURL.call(this, url);
window.location.replace(this.href);
}
}
function parseURL(url) {
if (this._innerUse)
return;
url = url || this.href;
var pattern = "^(([^:/\\?#]+):)?(//(([^:/\\?#]*)(?::([^/\\?#]*))?))?([^\\?#]*)(\\?([^#]*))?(#(.*))?$";
var rx = new RegExp(pattern);
var parts = rx.exec(url);
// Prevent infinite recursion
this._innerUse = true;
this.href = parts[0] || "";
this.protocol = parts[1] || "";
//this.host = parts[4] || "";
this.hostname = parts[5] || "";
this.port = parts[6] || "";
this.pathname = parts[7] || "/";
this.search = parts[8] || "";
this.hash = parts[10] || "";
if (!isIE)
delete this._innerUse;
else
this._innerUse = false;
updateURL.call(this);
}
function updateURL() {
if (this._innerUse)
return;
// Prevent infinite recursion
this._innerUse = true;
this.href = this.protocol + '//' + this.host + this.pathname + this.search + this.hash;
if (!isIE)
delete this._innerUse;
else
this._innerUse = false;
}
})()
* This source code was highlighted with Source Code Highlighter.
Рассмотрим создание getter/setter'ов:
- Случай для Firefox:
var keys = ["href", "protocol", "host", "hostname", "port", "pathname", "search", "hash"];
for (var i = 0, l = keys.length; i < l; i++) {
(function(i) {
var key = keys[i];
var gKey = 'get' + key.substr(0, 1).toUpperCase() + key.substr(1);
var sKey = 'set' + key.substr(0, 1).toUpperCase() + key.substr(1);
URL.prototype.__defineGetter__(key, gs[gKey]);
URL.prototype.__defineSetter__(key, gs[sKey]);
})(i);
}
* This source code was highlighted with Source Code Highlighter.
Используем магическиеURL.prototype.__defineGetter__
иURL.prototype.__defineSetter__
. Вследствии у нас появятся псевдо-аттрибутыurl.href, url.path
и т.д., изменяя которые на самом деле будут вызываться функции-обработчики.
- Случай для Internet Explorer: а вот тут начинаются танцы с бубном. Версии IE < 8 вообще не имеют механизмов getter/setter. Однако есть чудесное событие —
onpropertchange
. Ничего не остаётся, как воспользоваться. Однако возникает осложнение — это событие присутствует только у DOM-элементов, да и то лишь тогда, когда эти элементы уже включены в DOM-модель. Что же, так и поступим:
var el = document.createElement('div');
el.style.display = 'none';
document.body.appendChild(el);
// ...
el.onpropertychange = function(){
var pn = event.propertyName; // имя изменённого параметра
var pv = event.srcElement[event.propertyName]; // его новое значение
// ...
}
// ...
return el; // обязательно вернуть el. Т.е. по сути new URL(...)
// вернёт не объект типа URL, а элемент DIV.
// Признаюсь, это не очень хорошо, т.к. к примеру
// теряется связь instanceof. Но что тут поделаешь, это IE, детка :)
* This source code was highlighted with Source Code Highlighter.
Примеры №2
// Пусть текущий URL = 'http://my.site.com/somepath/'
var u = new URL('relative/path/index.html')
u.href // my.site.com/somepath/relative/path/index.html
u.href = '/absolute/path.php?a=8#some-hash'
u.href // my.site.com/absolute/path.php?a=8#some-hash
u.hash // #some-hash
u.protocol = 'https:'
u.href // my.site.com/absolute/path.php?a=8#some-hash
u.host = 'another.site.com:8080'
u.href // another.site.com:8080/absolute/path.php?a=8#some-hash
u.port // 8080
// и так далее, и тому подобное
* This source code was highlighted with Source Code Highlighter.
Работает в FF3+, IE6+. Можно докрутить для Safari/Chrome. Насчёт Opera — не уверен. Необходимо RTFM.
Вот так
Надеюсь, я сделал что-то полезное и не впустую потратил день своей жизни на написание этой статьи :-)
P.S.: да, я думаю написать отдельную статейку, посвящённую getter'ам и setter'ам в разных браузерах. Не Firefox'ом одним живы (небольшой пи-ар: чтобы не нагружать Habrahabr моим потоком мыслей — милости прошу на мой блог — http://web-by-kott.blogspot.com/. Там пока-что пустынно, но я только начинаю)