Пользователь
0,0
рейтинг
26 июля 2009 в 14:09

Разработка → Парсим URL

Хочу поделиться одной полезной утилиткой, написанной на pure JavaScript, — URL. По сути это небольшой парсер URL'ов, работающий почти как 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, с последующим редиректом.

Можно придумать ещё.

Проблема


В чём же, собственно, проблема? Проблема в том, что:
  1. Мы не можем использовать объект window.location, т.к. он перезагружает текущую страницу при малейших изменениях
  2. Мы не можем создать ещё один такой же объект через конструктор Location — атата! запрещено браузероводами!
  3. Сам объект довольно нетривиален в поведении
  4. Ну и я не нашёл никакой готовой реализации :)

Я упомянул про нетривиальность поведения. Вот она в чём:

Рисунок: взаимосвязь частей URL

При изменении любой из частей URL должны обновляться другие.

Разбор на части


По сути я буду создавать подобие window.location, поэтому и обозначения тащу оттуда. Разберём пример:

Рисунок: разбор частей URL

Без комментариев :)

Как ни крутись, без 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/. Там пока-что пустынно, но я только начинаю)
Ростислав @Kottenator
карма
211,0
рейтинг 0,0

Похожие публикации

Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (81)

  • +3
    Пишите комменты/отзывы, товарищи. Особенно когда минусуете ;-)
    • +4
      статья хорошая.

      воможно минусы ставят за большие простыни исходников. Стоило в начале представить пример, который бы описывал о чем речь пойдет, а потом описать реализацию. И да, полные исходники в теле статьи не нужны и вредны. Если бы вы выделили только значимые куски кода и описали что он делат, то читать статью было бы интереснее и легче. Весь остальной код можно прикрепить ссылкой, кому надо пусть разбирается с оригиналами.

      * кроме того, очень плохо, когда в статье отсутствуют ожидаемые изображения
      • +1
        Спасибо за отзыв, сейчас вытащу пример наверх. А рисуки — надо перезалить, но я не знаю — куда лучше :(
  • 0
    Если рисунок не отобразился — прямая ссылка.


    404 =\ Сами пробовали открыть?
    • 0
      Эх, вечно проблемы с этими рисунками. Спасибо, перезалью их куда-нибудь на другой сервер. Кстати, не подскажете достойный?
      • 0
        Я пикассу использовал. Нет проблем с большой нагрузкой.
        • +1
          Перезалил рисунки на ImageShack.
      • 0
        fotki.yandex.ru
      • +2
        Приятне, красиве, удобне. Пикамате
  • –1
    var pattern = "^(([^:/\\?#]+):)?(//(([^:/\\?#]*)(?::([^/\\?#]*))?))?([^\\?#]*)(\\?([^#]*))?(#(.*))?$";
    var rx = new RegExp(pattern);
    var parts = rx.exec(url);


    Сокращаем до одной строки:
    var parts = /^(([^:/\\?#]+):)?(//(([^:/\\?#]*)(?::([^/\\?#]*))?))?([^\\?#]*)(\\?([^#]*))?(#(.*))?$/.exec(url)

    Создание гетеров, сетеров… Вы вообще о чем? есть адекватное решение навроде:

    this.holder = [];
    var val = function(key, val) {
    if (!val) return this.holder[key];
    this.holder[key] = val;
    }

    Джаваскрипты они вообще не о том, чтобы такие интерфейсы у классов писать. Если постараться — ваш код сожмется раза в 2 если не больше.
    • +1
      Сократить RegExp до одной строки — да, можно. Но это не суть.

      А вот геттеры и сеттеры — суть. Тут вы либо не поняли мою идею, либо не дочитали статью :) Соль — в псевдо-аттрибутах. И решения для IE я пока в глаза не видел.
      • +1
        Видимо действительно не уловил сути. Пример жизненный приведите пожалуйста. Ато так много кода, а вокруг чего вся кутерьма, да еще и с магическими методами — не до конца ясно.

        Насчет универсального сеттера комментарием выше — извините, быстро по коду пробежал, не увидел у вас метод protocol.
        • +1
          Да ничего, понимаю, что много букв, но не люблю, когда часть — спрятана от глаз. Примеры — вынес из ката наверх. Ну и могу прямо здесь привести:
          // Пусть текущий 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 // https://my.site.com/absolute/path.php?a=8#some-hash
          u.host = 'another.site.com:8080'
          u.href // https://another.site.com:8080/absolute/path.php?a=8#some-hash
          u.port // 8080
          // и так далее, и тому подобное

          * This source code was highlighted with Source Code Highlighter.
          • 0
            Работает в FF3+ (может и в 2+, не пробовал) и в IE6+. Можно ещё докрутить для других.
          • +1
            Нет, я не о том. Буквы я худо-бедно прочитал.

            Я о жизненном применении этой библиотечки. Вам часто приходится джаваскриптом модифицировать и изменять ссылки? Как эти решения могут помочь сторонним разработчикам?
            • +2
              Скажу честно, данная тулза возникла именно из практических нужд.
              И я видел уже несколько кустарных разработок подобного назначения в больших JS-проектах, таких, как TinyMCE. В RTE часто имеешь дело со ссылками на ресурсы. И эти ссылки нужно обрабатывать в real-time.

              Конкретно мне надо было распарсить текущий URL и изменить/добавить новый параметр в search, с последующим редиректом.

              Можно придумать ещё.
              • 0
                Спасибо за разъяснение. Вы бы об этом упомянули вначале статьи.
                • 0
                  Резонно. Внесу в статью
  • +3
    " — Как ни крутись, без RegExp не обойтись"
    Претендует на народную мудрость:-)
  • 0
    А чем вам не угодили обычные a.hash('asd') a.hash()?
    Зачем эти танцы с бубнами?
    • 0
      Собственно поэтому версия 2.0 альфа :) Просто решил поделиться решением для псевдо-аттрибутов для IE. В продакшене я бы использовал именно a.hash('asd') и a.hash().
      • 0
        наивно полагать, что никто не додумался до этого раньше ;-)
        способ довольно грязный, ибо в узлах и так много всякого мусора
        к тому же полиморфные акцессоры позволяют использовать цепочки:
        var uri= $.url( '/xxx' ).hash( 666 ).post( 8080 ).href()

        ещё, хранение данных в замыканиях — сильно усложняет отладку.
        лучше тогда уж использовать проперти типа таких: plugins.jquery.com/files/jquery-property.js_1.txt
        • 0
          Цепочки — легко реализовать. ранение данных в замыканиях — хорошо тем, что с удалением объекта эти данные соберёт garbage collector, в отличии от использования сторонних хранилищ этих данных.

          Да, метод грязный (для IE), но другого я не нашёл :(
          • 0
            > Цепочки — легко реализовать.

            пример?

            > ранение данных в замыканиях — хорошо тем, что с удалением объекта эти данные соберёт garbage collector, в отличии от использования сторонних хранилищ этих данных.

            глупость какая-то

            • +1
              1) Цепочки: в моей реализации заменить
              setPort: function(val) {
              //…
              return data.port;
              }
              на setPort: function(val) {
              //…
              return this;
              }
              И вызывать этот метод в нужном контексте (setPort.call(necessaryScope, val))

              2) Не вижу ничего глупого. В предложенном вами решени plugins.jquery.com/files/jquery-property.js_1.txt — по сути хранит данные вне целевого объекта, и когда этот объект исчезнет, данные его останутся в этом «внешнем хранилище» навсегда.
              • 0
                1. ага, ещё зафигачить эти методы в инстанс и получить в нём оба интерфейса…

                2. почему внешнее хранилище не будет уничтожено? о_0''
  • 0
    search хорошо бы еще разбирать на пары key: value и соответственно менять их по отдельности по необходимости.
    А так библиотека полезная получилась бы, например при изменении каких-то фильтров, менять ссылки на связанные страницы где эти же фильтры используются.
    • +1
      function URL( href ){
      var url= document.createElement( 'a' )
      url.href= href || document.location.href
      return url
      }

      var link= new URL( '/xxx' )
      link.hash= 666
      alert( link )
      • 0
        Да, интересно, не подумал :)
        Но мой способ (v.1 final) — даёт отвязку от DOM'а. И применим также в server-side JS. Ну и для Actionscript.
        • 0
          у них нету дома? о_0''
    • 0
      гомэн, промахнулся ^_^''
  • +4
    Мне почему то в голову приходит другое простое решение:

    var u = document.createElement('A');
    u.href = '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 // https://my.site.com/absolute/path.php?a=8#some-hash
    u.host = 'another.site.com:8080'
    u.href // https://another.site.com:8080/absolute/path.php?a=8#some-hash
    u.port // 8080
    // и так далее, и тому подобное


    * This source code was highlighted with Source Code Highlighter.

    Может я конечно чего-то не понимаю, и ваш велосипед на самом деле чем то лучше :)
    • –2
      Да, признаю — это неплохой вариант. Но моя реализация лучше в двух моментах:
      1) Отвязанность от DOM-модели. Возможность использовать в server-side JS, или в Actionscript
      2) Наглядность. Видно, как этот велосипед устроен внутри.
      • 0
        а смысл создавать велосипед, который копирует нативный фукнционал?
        server-side JS, конечно, да, не поспоришь, но вещь в наших краях редкая.
        может, подумать в сторону расширяемости какой-нибудь, ну там сравнение ссылок, еще чего-нибудь придумать?
        • 0
          u.href // my.site.com/somepath/relative/path/index.html
          u.catalogues // [«somepath», «relative», «path»]

          еще можно mailto проработать, к нему можно в get-параметрах тему и текст сообщения передавать

          и есть псевдопротоколы, вроде magnit или javascript, для них наверняка можно что-нибудь полезное придумать
          • 0
            Спасибо, есть куда стремиться :)
        • 0
          Да, сравнение реализовать не очень сложно. Да и ещё чего-нибудь можно придумать. То, чего ещё нет в нативном функционале, но что нужно.
      • +4
        Вы уж меня извините, но:
        1. Думаю, что в «в server-side JS, или в Actionscript» наверняка есть что-то встроенное. Отвязанность от DOM? Это в реализации для IE, где вы создаете элемент DIV? Вариант для Firefox будет работать только для браузеров на базе gecko, так что о server-side и actionscript говорить не приходится.
        2. Наглядность только в том случае, если Вы хотите показать как это написать самому. Включать в свое приложение дополнительный код, который работает только в двух браузерах, причем по разному — когда можно обойтись более простым решением — несколько нерационально.

        Ну и по коду, не очень хорошая реализация. Я бы остановился на второй реализации, только бы выправил с точки зрения алгоритмической части. Позвольте несколько советов.
          this.search = function(val) {
                if (typeof val != "undefined") {
                    search = val;
                }
                return search;
            }
        

        Если вы хотите проверить передан параментр или нет, то лучше это делать следующим образом:
          this.search = function(val) {
                if (arguments.length) {
                    search = val;
                }
                return search;
            }
        

        Следующий таинственный фрагмент:
        this.href(this.protocol() + '//' + this.host() + this.pathname() + this.search() + this.hash());
        

        Почему не
        this.href = protocol + '//' + host + pathname + search + hash;
        

        Ведь методы сделают тоже самое + у вас не будет проблем с рекурсией.
        Стремимся к простосте — вместо:
        if (val.indexOf("/") != 0) { // relative url
           var _p = (pathname || window.location.pathname).split("/");
           _p[_p.length - 1] = val;
           val = _p.join("/");
        }
        

        например так
        if (val.charAt(0) != '/') { // relative url
          val = pathname.replace(/(\/|)$/, '/' + val)
        }
        

        Про window.location.pathname лучше забыть. Ибо
        // находимся на http://habrahabr.ru/blogs/javascript/65407/
        var u = new URL('http://domain.ru');
        u.pathname('foo/bar/index.html');
        u.href // ожидаете http://domain.ru/foo/bar/index.html получите http://domain.ru/blogs/javascript/65407/foo/bar/index.html
        


        Ну и далее далее…
        • 0
          1) Насчёт if (arguments.length) { — согласен, так лучше.
          2) «Следующий таинственный фрагмент:» — вы что-то путаете. Первый фрагмент — для одной реализации. Второй фрагмент — для второй реализации.
          3) «val.indexOf(»/") != 0" == «val.charAt(0) != '/'» — одно и то же.
          4) Использовать «pathname.replace(/(\/|)$/, '/' + val)» более расточительно, чем мой вариант со split + join, хоть он и более громоздкий. Ну и мой вариант более читабелен.
          5) «Про window.location.pathname лучше забыть» — вы наверное имеете ввиду relative pathname? Т.к. в целом про pathname ни в коем случае забывать нельзя :) Но если моя реализация действительно работает так, как вы описали, то это конечно бага. Зафикшу.
          • 0
            Извиняюсь, насчёт №2 — тут я перепутал. Вы предлагаете использовать this.href = protocol + '//' + host + pathname + search + hash; но тут 2 момента:
            1) Внутри ф-ции updateURL (из которой вы и привели фрагмент кода) нет никаких сведений ни про какие host, pathname, search и hash, т.к. они находятся внутри замыкания конструктора.
            2) this.href = /* всё, что вы написали */ — привдёт к переопределению метода this.href.

            А чтоб оно работало, надо реализовывать всё совсем по-другому :-)
            • 0
              Да, this я забыл убрать.
              Насчет замыкания — только сейчас заметил что вынесена функция. Что не совсем логично, так как она используется (вызывается) только внутри класса. Зачем она вынесена? Только потому что используется всеми экземплярами? Может стоит поместить внутрь конструктора? Вне конструктора она не нужна — и тогда сможете вызывать ее updateUrl() вместо updateUrl.call(this)
              • 0
                Да, я вынес updateUrl из объекта и из конструктора потому, что её требуется всего 1 экземпляр, и он должен быть спрятан от глаз в замыкании.
                • 0
                  Несколько странный подход… от кого прячем? :)
                  • 0
                    Ну вот представьте — у вас объект URL. updateUrl(...) мог бы находится в this, тогда у наблюдательного пользователя (скажем, использующего console.dir) возникнет вопрос — что оно тут делает? С чем его есть? Ответ — не с чем, это служебный метод. Поэтому его надо прятать с глаз долой.
                    Как это сделать? Можно спрятать в конструктор. Тогда будет создаваться по новенькой ф-ции updateUrl с каждым новым объектом. Но это неоправдано, нужна только одна ф-ция updateUrl.
                    Значит надо вынести и из конструктора, и из прототипа. Значит это должна быть внешняя ф-ция, но спрятанная в одноразовом замыкании (function(){/**/})()

                    Вот так я размышлял, когда писал код.
          • 0
            2. второй фрагмент переписаная строка из вашей функции updateURL().
            4. чем же более расточительно? поиск и замена вместо split (то же поиск, но плюс разбиение на массив) + join. В вашем оформление он не очень то и читабельней.
            5. да я имел ввиду что в случае работы с некоторым URL не стоит приплетать значения из window.location в случае если что-то опущено в адресе с котором работает объект, иначе можно получить неожиданные сюрпризы.

            ЗЫ Все вышеизложеное было мое ИМХО. Не хотелось бы заниматься буквоедством и холиварами :)
    • 0
      Нашёл большой минус подхода «var u = document.createElement('A');» — неадекватное поведение в IE6-8. Так не пойдёт.
      • 0
        а конкретней?
        • 0
          var u = document.createElement('A');
          u.href = 'relative/path/index.html';
          alert(u.href); // relative/path/index.html, а не my.site.com/somepath/relative/path/index.html

          Одинаково неправильно в IE6, IE7, IE8
          • 0
            ну и что?
            • 0
              Вы издеваетесь, или троллите меня? :)
              Вы предложили нерабочий (но зато native) вариант взамен моего рабочего, вот что. Потому я и сказал, что так не пойдёт.
              • 0
                в каком месте он не рабочий? зачем тебе пренепременно абсолютизированные ссылки?
                • 0
                  Ссылки, к примеру, нужны именно абсолютные, когда я в РТЕ редактирую статью и вставляю ссылку на ресурс с этого же хоста (бывают такие нужды).

                  Да и разнобой принципов работы в разных браузерах (в FF — absolute, в IE — relative) — неприемлемо.
                  • 0
                    это повод пропатчить PTE, чтобы он научился абсолютизировать относительные урлы, если он ещё не умеет этого делать.

                    неприемлемо почему? о_0
                    для задачи «распарсить текущий URL и изменить/добавить новый параметр в search, с последующим редиректом» и для большинства других — вполне годится.
  • 0
    Спасибо! Хорошо, доступно, полезно!
  • 0
    Полезно для небольших вещей, но, ИМХО, не окупает килобайтов кода.

    a = new URL('http://user:pass@host/') делает бяку (хотя такие вещи попадаются в жизни реже, чем необходимость продублировать window.location)

    В AS2/AS3 все равно без отвертки не войдет (хотя бы потому что используете сокращенную форму регулярок).

    Для AS3 нашел такую вещь:
    manfred.dschini.org/2008/05/12/as3-url-class/
    • 0
      Надо будет почитать, подкрутить :)
  • +1
    Забыли ненавязчиво о пароле и юзере. Смотрим RFC 1738 Uniform Resource Locators (URL) — www.ietf.org/rfc/rfc1738.txt
    Для IE можно использовать элемент COMMENT (msdn) чтобы не засорять левыми дивами.
    • 0
      Спасибо, содержательно. Покопаюсь, как будет время :)
  • 0
    Хорошая статья. Но есть замечание к вопросу разбора URL. Все Вами написанное можно было бы записать чуть-чуть короче (за исключением регулярного выражения), только это не покрывает проблем, развитых в статье:

    String.prototype.parseUrl = function()
    {
    	var matches = this.match(arguments.callee.re);
    
    	if ( ! matches ) {
    		return null;
    	}
    
    	var result = {
    		'scheme': matches[1] || '',
    		'subscheme': matches[2] || '',
    		'user': matches[3] || '',
    		'pass': matches[4] || '',
    		'host': matches[5],
    		'port': matches[6] || '',
    		'path': matches[7] || '',
    		'query': matches[8] || '',
    		'fragment': matches[9] || ''};
    
    	return result;
    };
    
    String.prototype.parseUrl.re = /^(?:([a-z]+):(?:([a-z]*):)?\/\/)?(?:([^:@]*)(?::([^:@]*))?@)?((?:[a-z0-9_-]+\.)+[a-z]{2,}|localhost|(?:(?:[01]?\d\d?|2[0-4]\d|25[0-5])\.){3}(?:(?:[01]?\d\d?|2[0-4]\d|25[0-5])))(?::(\d+))?(?:([^:\?\#]+))?(?:\?([^\#]+))?(?:\#([^\s]+))?$/i;
    
    • 0
      Конечно же у этого решения тоже можно найти недостатки.
      • 0
        Если ставить задачу, как разовый парсинг URL, то да, ваш вариант отлично подходит (за исключением засорения String.prototype). И я вижу вы потрудились над RegExp'ом ( устрашающе выглядит с первого взгляда :-) )
        • 0
          Здесь два варианта — засоряем глобальную или локальную область :). Регекс написан был давно, то ли на PHP, то ли на Perl и лишь перенесен в Javascript. Страшый же он от того что, пытается покрыть максимально возможные варианты (но не все) — protocol://user:pass@host:port/path?query#hash (fragment — небольшое разночтение с Вашим hash). Под host подразумеваются доменные имена, IP-адреса и localhost.
  • –1
    Автор, вы перестарались, первоначальная задача была «распарсить текущий URL и изменить/добавить новый параметр в search, с последующим редиректом.», для этого достаточно 1 регулярного выражения. А если там что-то более сложное, лучше делать это на стороне сервера, чем писать простыню тормозящего загрузку страницы яваскрипта.

    И забейте вы на эту имитацию protected methods через .call(this), пишите нормально.

    * Вот как же я не люблю, когда видимо пришедшие с других языков типа Явы в веб-программирование, пытаются и тут делать монстры-фреймворки на все случаи жизни.
    • 0
      Это никакой не монстр-фреймворк, и ниоткуда он сюда не перенесён. К тому же я уверен в его востребованности, во всяком случае для меня.
    • 0
      А можно пример «нормального» кода? Объясните, пожалуйста, почему подобная реализация столько плоха? (или ссылки на объяснения приведите)
      • 0
        Реализация плоха тем, что содержит много кода, что отрицательно сказывается на скорости загрузки и производительности сайта на клиентской стороне.

        Как я понял, из смутного объяснения автора, ему надо было получать полный УРЛ ресурса из относительного (видимо прибавить http:// и адрес сайта в начало если их там не было), делается это примерно так:

        url = (url.substring(0, 1) === '/')? (url = 'http://' + host + url): url; // добавляем адрес сайта к отнсительному УРЛ (начинается со слеша)

        if (!/^(\w+):\/\//.test(url)) { url = 'http://' + url); } // Если УРЛ не начинается с протокола, приписываем http://

        Если надо, наоборот, парсить УРЛ, то тут придется поизвращаться с регекспами, типа того: var matches = /^(\w+:\/\/)?([a-z0-9_.])((/[^?])(.*))?$/i.exec(url)  - выделяет протокол, адрес узла, путь и строку параметров (простейший пример).
        • 0
          Вообще я спрашивал относительно 2 и 3-го абзацев вашего коммента:
          И забейте вы на эту имитацию protected methods через .call(this), пишите нормально.

          * Вот как же я не люблю, когда видимо пришедшие с других языков типа Явы в веб-программирование, пытаются и тут делать монстры-фреймворки на все случаи жизни.
          • 0
            Можно например начинать имена с подчеркивания, и соответсвенно не обращаться к методам, начинающимся с подчеркивания, снаружи. Правда, из-за этого половину кода занимают подчеркивания, но лучше же чем писать .apply(this)? Если в языке нет этой опции — по моему не стоит ее пытаться имитировать.

            А то мне это напоминает вещи вроде «javascript на php» — попытка сделать библиотеку, чтобы писать javascript используя php-функции, изврат же.
  • 0
    Ничего принципиально нового не увидел. Все уже давно существует, например тут mootools.net/docs/more/Native/URI
    • 0
      А как же getter'ы для IE? Неужели не зацепило? :-)
      • –1
        Как я понял там setter-ы а не getter-ы. В результате получаем уебанский костыль — div, который должен работать как объект класса URL.
        Ради этого не стоит заморачиваться. Лучше забить на ie6, ie7 или использовать .get() и .set(). Фрэймворки упрощают не только написание логики приложения но и вспомогательных классов(плагинов).
        про onpropertychange не знал, но врятли когданить буду использовать
        • +1
          Да, это костыль. И я бы и сам не стал его использовать в продакшне. Но это псевдо-атрибуты, и теоретически штука интересная.
          В IE8, кстати, нормальных геттеров/сеттеров так и не сделали — только для DOM-объектов (опять же), но уже не через onproperychange.
    • +1
      Насчёт варианта с Mootools — во-первых, мне не нужен Mootools для этой штуки, а я так понимаю, что мне придётся его включать в проект. Во-вторых, Mootools загрязняет нативные объекты (в отличии от jQuery), что ИМХО тоже не хорошо.
  • 0
    Стоит еще учесть, что хвост адреса может быть немного не стандартный, например в SWFAdress библиотеке не раз видел вариант типа

    ...html&a=1&b=2#/f/o/l/d/e/r/s?c=3&d=4

    и браузеры это съедают с легкостью
    • 0
      var u=new URL('test.html&a=1&b=2#/f/o/l/d/e/r/s?c=3&d=4')
      u.hash // #/f/o/l/d/e/r/s?c=3&d=4

      Тут всё ок, всё, что правее первого # == hash
      • 0
        Ну вот я и говорю, что не плохо бы и его парсить, тогда уже организовать это в библиотечку и раздавать/продавать всем желающим, тот же SWFAdress парсит весь хэш, но весьма неказистым образом.
        • 0
          Хорошая идея, спасибо. Надо подумать над этим. Наверное, таки докручу.
  • 0
    Автор, Вы неподражаемы!
    Прежде всего пара замечаний:
    Ошибка в Вашем же схематическом изображении — HOST является контейнером для hostname и port — так зачем же Вы их стрелочками-то аттачите в HREF?
    При изменении любой из частей URL должны обновляться другие. — так и не постиг, как именно «другие» должны обновляться??? ИМХО связь (да и то — лишь теоретическая, бо никто не мешает Вам использовать порты и протоколы по своему усмотрению) в URL есть лишь в парах http => 80 и https => 443 — Вы об этом?
    И один вопрос —
    Конкретно мне надо было распарсить текущий URL и изменить/добавить новый параметр в search, с последующим редиректом.
    Это конкретно все, что нужно было сделать?

    var url_replace = function (new_text,del_old) {
    var _stuff,_result,_test_url = window.location.href;
    _stuff=((_stuff=test_url.match(/\?([^#]+)/))&&_stuff[1]); // при отсутствии search полУчите null
    // теперь работайте с Вашим search
    if (del_old&&_stuff) _result=test_url.replace(/\?([^#]+)/,'?'+new_text); //если нужно перезаписать search
    else _result=test_url.replace(/(#)|$/,(!_stuff?'?':'&')+new_text+'$1'); // если нужно дополнить search
    return _result;
    }


    * This source code was highlighted with Source Code Highlighter.

    Вызывайте url_replace('foo=bar') для дополнения или url_replace('foo=bar',1) для замены строки search
    • 0
      Нет, я не о портах 80 и 443. Я про обновление href при, скажем, изменении port, и про обновление href & hostname & port при изменении host. И т.д. Изменяется один аттрибут => должны обновиться ещё парочку.

      И я понимаю, что есть решения проще и меньше по объёму кода, но они не универсальны и не поддерживают вышеупомянутое обновление частей URL
      • 0
        Агрегируйтесь! :)
        Меняя host — его и меняйте, какая вам разница до его составляющих?
        PS. ИМХО решение задач в общем виде — убийство времени.
        PPS. Меня там тоже немного покоцали, если решите променять универсальность на скорость и объем — ВЕЛКАМ!

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