Простой разбор URL с помощью изоморфного Javascript

http://www.sitepoint.com/url-parsing-isomorphic-javascript/
  • Перевод
Во многих веб-приложениях требуется разбор URL, такой как извлечение доменного имени, имплиментация REST API или поиск пути к файлам изображений. Типичная структура URL показана на рисунке ниже:



Вы можете разобрать URL строку на компоненты используя регулярные выражения, но это неоправданное и сложное решение…

Серверный разбор


Node.js (как и его форк io.js) предоставляет URL API:
// Server-side JavaScript
var urlapi = require('url'),
    url = urlapi.parse('http://site.com:81/path/page?a=1&b=2#hash');
 
console.log(
    url.href + '\n' +           // the full URL
    url.protocol + '\n' +       // http:
    url.hostname + '\n' +       // site.com
    url.port + '\n' +           // 81
    url.pathname + '\n' +       // /path/page
    url.search + '\n' +         // ?a=1&b=2
    url.hash                    // #hash
);

Как вы видите в примере выше, метод parse() возвращает объект, содержащий все отдельные части URL, такие как протокол, порт, адрес хоста и т.п.

Разбор на клиенте


В браузере, к сожалению, нет похожего метода, зато браузер умеет хорошо манипулировать DOM элементами, которые реализуют простой интерфейс Location:
// Клиентский JavaScript
// найдем первую ссылку в DOM
var url = document.getElementsByTagName('a')[0];
 
console.log(
    url.href + '\n' +           // the full URL
    url.protocol + '\n' +       // http:
    url.hostname + '\n' +       // site.com
    url.port + '\n' +           // 81
    url.pathname + '\n' +       // /path/page
    url.search + '\n' +         // ?a=1&b=2
    url.hash                    // #hash
);

Если у нас есть URL строка, мы может разобрать ее в памяти используя якорный элемент (a) без использования регулярных выражений:
// Клиентский JavaScript
// создадим скрытую ссылку
var url = document.createElement('a');
url.href = 'http://site.com:81/path/page?a=1&b=2#hash';
 
console.log(url.hostname); // site.com


Изоморфный разбор URL


Aurelio De Rosa обсуждал недавно изоморфные JavaScript приложения. Вкратце, это прогрессивное улучшение, задающее новый уровень, когда приложения могут одинаково хорошо выполняться как на клиенте, так и на сервере. Пользователь с современным браузером будет использовать одностраничное (single-page) приложение. Старые браузеры и поисковые роботы будут использовать альтернативные механизмы, обрабатывающие запросы на сервере. В теории, приложения смогут реализовать разные уровни клиент-серверной обработки, в зависимости от ширины канала и возможностей клиента.
Изоморфный Javascript обсуждается уже много лет и этот вопрос носит комплексный характер. Некоторые проекты пошли дальше, чем просто создание совместно используемых представлений, ведь на самом деле не так много ситуаций, когда прогрессивное улучшение не будет работать также хорошо (если не лучше, ведь большинство «изоморфных» Javascript фреимворков не существуют без клиентской обработки).Тем не менее, существует возможность создавать платформонезависимые микробиблиотеки, способные помочь сделать первый шаг на пути к изоморфной концепции.
Давайте обсудим, как мы можем создать такую библиотеку для разбора URL в файле lib.js. Для начала нужно определить где запущен наш код:
// Это Node.js?
var isNode = (typeof module === 'object' && module.exports);

Это не самый лучший способ определить платформу, так как у вас может быть определен клиентский объект с методом module.exports, но я не знаю метода лучше. Другие разработчики проверяют объект window:
// Это Node.js?
var isNode = typeof window === 'undefined';

Давайте завершим наш код в файле lib.js
// running on Node.js?
var isNode = (typeof module === 'object' && module.exports);
 
(function(lib) {
 
    "use strict";
 
    // require Node URL API
    var url = (isNode ? require('url') : null);
 
    // parse URL
    lib.URLparse = function(str) {
 
        if (isNode) {
            return url.parse(str);
        }
        else {
            url = document.createElement('a');
            url.href = str;
            return url;
        }
 
    }
 
})(isNode ? module.exports : this.lib = {});

В этом примере для ясности я использовал переменную isNode. В любом случае вы можете заменить эту проверку, поместив ее прямо в последние скобки этого кода.
На сервере URLParse экспортируется как стандартный Common.JS модуль. Пример:
// include lib.js module
var lib = require('./lib.js');
 
var url = lib.URLparse('http://site.com:81/path/page?a=1&b=2#hash');
console.log(
    url.href + '\n' +           // the full URL
    url.protocol + '\n' +       // http:
    url.hostname + '\n' +       // site.com
    url.port + '\n' +           // 81
    url.pathname + '\n' +       // /path/page
    url.search + '\n' +         // ?a=1&b=2
    url.hash                    // #hash
);

На клиенте URLParse объявлен как метод глобального объекта lib:
<script src="./lib.js"></script>
<script>
var url = lib.URLparse('http://site.com:81/path/page?a=1&b=2#hash');
console.log(
    url.href + '\n' +           // the full URL
    url.protocol + '\n' +       // http:
    url.hostname + '\n' +       // site.com
    url.port + '\n' +           // 81
    url.pathname + '\n' +       // /path/page
    url.search + '\n' +         // ?a=1&b=2
    url.hash                    // #hash
);
</script>

Отличий в использовании на сервере и клиенте, кроме способа подключения к коду, данная библиотека не имеет.

По правде сказать, это простой код и метод URLParse запускается(почти всегда) отдельно на сервере и клиенте. Но мы создали постоянное API, которое показывает как можно писать Javascript код, способный запускаться где угодно. Мы можем расширить библиотеку другим клиентскими и серверными функциями, такими как, валидация данных, разбор куков, обработка дат, форматирование валют и т.д.
Но я не уверен в полной изоморфизации приложений в виду того, что часто требуется разная логика для обработки на клиенте и сервере. В любом случае, платфомонезависимые библиотеки могут уменьшить ваши затраты на создание разного кода, который делает одни и те же вещи.
Поделиться публикацией

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

Комментарии 30

    +2
    А как быть, если мы используем browserify, webpack? Без полифилов для нодовых модулей, но с module.exports?
      0
      var isNode = typeof process != 'undefined' && {}.toString.call(process) == '[object process]';
      
        0
        На самом деле это неправильно) Нас не интересует нода это или нет, нас интересует, можем ли мы разобрать ссылку при помощи DOM.

        Как-то так:
        if (typeof document == 'object' && document.createElement) {
          url = document.createElement('a');
          url.href = str;
          return url;
        } else {
          return url.parse(str);
        }
        
          +1
          На самом деле, нет никакой разницы, Node.js это или нет, так как browserify предоставляет браузерные версии стандартных модулей, в том числе и url, так что и смысла извращаться нет. Я привёл этот код только потому, как в статье предлагается 2 неверных способа определить, нода это или нет. Ваш вариант не более правильный, потому как в окружении без dom также может отсутствовать и модуль url.
            +6
            Мой вариант может неправильный практически, но идеологически именно такой вариант и должен использоваться — хотите использовать сомнительную фичу, то проверьте её существование, а не платформу.
              0
              На ноде модуль url есть из коробки и определение платформы достаточный фичадетект для его использования без нежелательных последствий. В браузере же его подключение добавит в сборку лишний здоровый модуль (привет варианту из статьи). А результат и моего, и вашего вариантов один.
                0
                В статье показан workaround, как можно использовать функционал парсинга урлов, но этот функционал ни разу не является надежным. Т.к. на серверной стороне зависит от специфичного для Node.js окружения, а на клиентской стороне от специфичной фичи браузера. Такой функционал нельзя распространять в виде npm-модуля, в виде кода своего приложения сколько угодно.
                  0
                  Эмм… А какое отношение данный тред имеет к какому-бы ни было распространению данного велосипеда? :)
      0
      url = document.createElement('a');
      url.href = str;
      return url;
      Это не очень хороший способ вытащить данные о URL. document.createElement — довольно медленная операция, и если вам надо распарсить 10000 ссылок в цикле, то это может ощутимо подвесить браузер.
      Я понимаю, что это только пример, но кто-то может захотеть использовать его в реальной жизни. Нехорошо писать такие примеры.
        +4
        Не так уж и медленно — на 100 тысяч фаерфокс (на amd fx 6100 3.3GHz) тратит меньше секунды (800 мс)
        jsfiddle.net/fh21gu0a/

        p.s. По мне так отличнейший способ не изобретать своего велосипеда. Мало того, если вдруг будут внесены какие то изменения в стандарт url, то браузер поддержку обновит, а вот ваш сайт лет через 5-10 никто обновлять не будет!
          +11
          Можно еще попробовать создать элемент вне цикла.
            0
            Отличная идея, это ускорило код на треть, при этом примерно 80% времени в моем примере тратится на получение .hash.
            В общем нет никаких тормозов.
          0
          А зачем вообще делать document.createElement в цикле?
            –3
            Отвечу на это примером из жизни. Пару лет назад, когда я был моложе и зеленее, для работы с большими таблицами на клиенте я воспользовался одной библиотекой, название которой уже не помню. Но что мне запомнилось, так это то, что она начинала нещадно тормозить при выводе текста, которому надо было убирать тэги. Данных было много, порядка 5000 строк по 10 ячеек в строке, и это был единственный юзкейс при котором тормоза становились неприемлемыми.
            Расчехлив профилировщик я обнаружил, что очистка от тэгов производилась как-то так
            var span = document.createElement("span");
            span.innerHTML = str;
            var filtered_str = span.innerText;
            
            Это и было самым ресурсоёмким местом. С тех пор я помню, что создание элементов DOM — не самая быстрая операция, можно и побыстрее.
            Вы спрашиваете, зачем это делать в цикле. Незачем, если это пример. Но в реальной жизни кто-нибудь может засунуть это в функцию
              0
              Ну так вопрос как бы и намекал на то, что создавать элемент в цикле — плохая идея.
            0
            1) если вам нужно распарсить 100500 URL-адресов в браузере, то вам свинья не товарищ
            2) никто для такой узкой задачи не запрещает сделать document.createElement лишь раз
            +3
            В браузере, к сожалению, нет похожего метода

            url.spec.whatwg.org/#api
            developer.mozilla.org/en-US/docs/Web/API/URL/URL
            github.com/arv/DOM-URL-Polyfill
              +6
              Примерно раз в год, кто-то изобретает этот велосипед, и спешит поделиться с хабром.
                +1
                ftp://user#1:2@[ff:fe::1]:3128/foo/bar?q#hash
                


                Съест?
                  0
                  Прошу прощения за, возможно, глупый вопрос, а что такое "#1:2"?
                    +5
                    Как меня поправили ниже, '#' не возможен. Заменим его на другую строку:

                    ftp://user!&1:$&*;=3@[ff:fe::1]:3128/foo/bar?q#hash
                    


                    Штука до '@' — это authority.
                      0
                      Да, браузер все корректно распарсил (что логично)
                    +1
                    > user#1
                    В данном случае все что после # будет фрагментом, по rfc3986 символ # невозможен нигде кроме как во фрагменте.
                      +1
                      authority = [ userinfo "@" ] host [ ":" port ]
                      userinfo = *( unreserved / pct-encoded / sub-delims / ":" )
                      sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="

                      Да, вы правы. Меняю задачу:

                      ftp://user!&1:$&*;=3@[ff:fe::1]:3128/foo/bar?q#hash
                      
                        +1
                         ❯ node -p 'require("url").parse("ftp://user!&1:$&*;=3@[ff:fe::1]:3128/foo/bar?q#hash")'
                        { protocol: 'ftp:',
                          slashes: true,
                          auth: 'user!&1:$&*;=3',
                          host: '[ff:fe::1]:3128',
                          port: '3128',
                          hostname: 'ff:fe::1',
                          hash: '#hash',
                          search: '?q',
                          query: 'q',
                          pathname: '/foo/bar',
                          path: '/foo/bar?q',
                          href: 'ftp://user!%261:%24%26*%3B%3D3@[ff:fe::1]:3128/foo/bar?q#hash' }
                        
                          0
                          Круто. Хотя точно должно быть 'ftp:', а не 'ftp'?
                            +1
                            Встреченные мною парсеры урлов консистентно отдавали протокол с двоеточием в конце, и в Node.js, и в браузере.
                          0
                          github.com/petkaantonov/urlparser — вот кстати еще более быстрый парсер урлов, без регулярки. Сейчас это вроде висит как PR в io.js с тегом «на повестке дня».
                      +1
                      К сожалению, изменять href у ссылки — не самый кроссбраузерный способ. В IE отсутствие 80 дефолтного для http порта трактуется иначе, чем в других браузерах. Вот тут чуть подробнее.
                        0
                        Писал свой с поддержкой сложного query
                        Демка: runkit.com/pxyup/5c336aed02ce8e00124ee50d

                        github.com/PxyUp/uri-parse-lib

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

                        Самое читаемое