Я считаю HTTP одной из самых продуманных технологий, которая опередила свое время. В ней заложено очень много возможностей, которые до сих пор используются очень слабо. Отчасти я виню (хоть и не мое это дело) в этом известный язык PHP, который в стремлении к доступности для начинающих скрывает механику работы протокола так сильно, что многие веб-программисты начинают, продолжают, заканчивают и становятся посредственными менеджерами так и не узнав, с чем же они все это время имели дело.

Однако с явлением Web 2.0 (каким бы одиозным этот термин ни был) многие умные люди все больше и больше находят применение потенциалу HTTP. RSS, Ajax, JSON, Atompub — все они так или иначе использует HTTP как транспорт, по пути затрагивая кеширование, авторизацию и "content negotiation" (есть уже русский термин?).

Этот самый "content negotiation" недавно нашел применение в нашем текущем проекте, и я хочу поделиться этой идейкой.

Я исповедую известный принцип "hijax", который диктует, что вся функциональность веб-приложения должна быть доступной без скриптования и ajax'а вообще — на простых формах и ссылках. Ajax на приложение навешивается уже потом, подменяя где нужно onclick'и и onsubmit'ы. В разработке по такому принципу сервер должен уметь обрабатывать по сути один и тот же запрос двояко: когда он сделан браузером по-обычному и когда он запрошен через XmlHttpRequest. Разница при этом обычно только в формате выдачи.

Простейший пример из текущего проекта. Есть "кнопка" (по сути просто стилизованный <a href>), по нажатию на которой должен всплывать некий диалог. В отсутствии ajax'а никакого диалога не появляется, а ссылка просто срабатывает обычным образом, и пользователь переходит на отдельную страницу, в которой нарисовано то же самое, что было бы нарисовано в диалоге. Конечно не точно так же, а с шапкой и всякой дополнительной шушерой по бокам, но середина — та же, что в диалоге. На сервере это обрабатывается одной и той же функцией приблизительно так:

def dialog(request):
  data = { .. } # данные диалога
  if is_ajax(request):
    return render_to_response('ajax_template.html', data)
  else:
    return render_to_response('full_template.html', data)

Функция is_ajax — то, о чем я веду речь. Нужно как-то понять по виду запроса, каким способом он пришел. Обычно это решают просто дописывание к ajax-запросам, формирующимся в javascript'е, признак типа "ajax=1". И, в общем-то, оно работает:

def is_ajax(request):
  return request.GET.get('ajax') or request.POST.get('ajax')

Однако теперь мы делаем по-другому. В HTTP есть специальный заголовок — Accept — который предназначен для того, чтобы клиент запрашивал у сервера, в каком конкретно виде он хочет ответ. Выглядит это как перечисление mime-типов, которые клиент умеет понимать, и по умолчанию в браузерах туда пишется длинная строчка со всякими "text/html", "application/xhtml+xml", "image/jpeg" и т.д.

Так вот, вызывая ajax-запрос, в этот заголовок как раз и можно написать, что javascript'овый код, ожидающий ответ, хочет получить его не в виде полноценного HTML-документа, а... в каком? В каком — это в принципе личное дело клиента и сервера, и о mime-типах они могут договориться сами. Мы сейчас в текущем проекте используем такие:

application/xmlпроизвольный XML
application/javascriptJSON
application/html+ajaxкусок HTML-документа для вставки

Это не магические значения, просто они мне кажутся достаточно понятными и предсказуемыми, кроме последнего, который я придумал за 30 секунд :-). Но главное, что они дают отличить ajax-запросы от обычных браузерных запросов:

def is_ajax(request):
  return request.META['HTTP_ACCEPT'] in ['application/xml', 'application/javascript', 'application/html+ajax']

Я пока еще не понял, лучше ли это или хуже, чем "ajax=1". Пока что мне нравится просто сам принцип использования вещи по ее прямому назначению, ну и кроме того это выглядит гораздо моднее. Однако теоретически пользу я предположить могу. Если информация о формате передается в самой ссылке, то она навязывает и контекст ее вызова. Грубо говоря, если ссылку http://server.com/service/?ajax=1 сохранить в закладки и вызвать, то она вернет какой-нибудь XML, JSON или кусок HTML, и это будет не то, что нужно. С заголовком таких проблем не возникает, потому что там тип ответа диктуется как раз контекстом вызова.

Причем же тут jQuery? А просто у ней есть замечательный метод ajaxSetup, который позволяет в одном месте сказать, чтобы во все ajax-запросы добавлялся нужный заголовок:

$(document).ready(function () {
  $.ajaxSetup({
    beforeSend: function (request) {
      request.setRequestHeader('Accept', 'application/html+ajax');
    }
  })
});

Комментарии: 43

  1. Денис Барушев

    Ajax-запрос еще можно отличить по заголовку:

    X-Requested-With: XMLHttpRequest
    

    Я пока еще не понял, лучше ли это или хуже, чем "ajax=1". Пока что мне нравится просто сам принцип использования вещи по ее прямому назначению, ну и кроме того это выглядит гораздо моднее.

    Однозначно лучше. И не только моднее, а еще и более соотвествует REST-стилю. Всячески рекомендую к прочтению по этому поводу серию переводов.

    Рассматривается имеено то, о чем ты говоришь, только в контексте Rails. Они этому учат сразу "из коробки" :)

    respond_to do |format|
      format.html # show.rhtml
      format.js   # show.rjs
      format.xml  { render :xml => @testet.to_xml }
    end
    
  2. GiNeR

    Я всё больше поражаюсь твоей маньячной педантичности. Я не прогер, но читаю с большим интересом. Красиво.

  3. dobrych

    Еще один плюс для jQuery, удобно....

  4. Илья

    Вот это круто! Использовать вещи по назначению — это прямо физиологическое удовольствие :)

  5. Kallisto

    Удобно, но в принципе... &ajax=1 в закладки поместить трудно, потому как контент вставится в текст документа, потому строка браузера не изменится.

    потому мне кажется на результате это не изменится.. но так как бы правельне.. имхо

  6. Leonya

    Тоже хотел написать, что Рельсы так работают, но меня опередили :)

  7. Michael Klishin

    Пользователям Rails это с 1.2 даровано сразу через respond_to и идею ресурсов со множеством представлений. Просто к слову, что оно приживается потихоньку в разных местах.

  8. Pashka R.

    Я исповедую известный принцип “hijax“, который диктует, что вся функциональность веб-приложения должна быть доступной без скриптования и ajax’а вообще — на простых формах и ссылках.

    Я надеюсь, что когда-нибудь webX.0 будет идеален... его будут творить люди, которые живут этим и любят своё дело :)

  9. Zork

    Мне кажется что такие заголовки как то правильней

    application/xml - произвольный XML

    text/javascript+json - JSON

    application/xml+html - кусок HTML-документа для вставки

    1. классика жанра
    2. браузер как бы обычный яваскрипт просит, но в особом формате
    3. чтобы отличить что это кусок, а не обычный запрос, то мы рассматриваем html как xml, но его определенное подмножество (военная хитрость такая)
  10. Elf

    В разработке по такому принципу сервер должен уметь обрабатывать по сути один и тот же запрос двояко: когда он сделан браузером по-обычному и когда он запрошен через XmlHttpRequest.

    Если отказаться от этой предпосылки, то 3/4 поста можно удалить :-)

  11. Alex Efros

    Если отказаться от этой предпосылки, то 3/4 поста можно удалить :-)

    А есть основания от неё отказываться? Я недавно проглядывал статистику, в среднем по инету JavaScript отключён у 5%. Для некоторых проектов это немало...

  12. Hunter

    Ну ты точно маньяк :)

  13. Elf

    А есть основания от неё отказываться? Я недавно проглядывал статистику, в среднем по инету JavaScript отключён у 5%.

    Так я не про отказ от javascript'а. Я просто не понимаю, почему одна функция должна определять весь вид страницы (раз уж к ней идет запрос и она решает - отдавать всю страницу или только фрагмент).

    Просто есть другой подход - страница = сетка. Каждый элемент ячейки - вывод какого-нить метода. Соответственно, при рендеринге страницы опрашиваются все "заинтересованные", а при обновлении аяксом вызывается только хэндлер нужной ячейки (аякс обычно так и используется - перерисовывать фрагмент страницы, логически и контекстно не связанной с остальными кусками).

    И всё, всё банально сразу становится, не надо никаких мудрствований совершенно.

    Это все меня держит пока вдали от Джанги. Хотя, если б я присмотрелся к ней поближе, мот и нашел бы способ реализации такой архитектуры...

  14. Иван Сагалаев

    application/xml+html - кусок HTML-документа для вставки

    А причем тут тогда XML? HTML и так-то не обязан быть XML'ом, а уже произвольный кусок — тем более, даже если это XHTML. Пример:

    <p>One</p>
    <p>Two</p>
    

    Это даже не well-formed XML.

    Если отказаться от этой предпосылки, то 3/4 поста можно удалить :-)

    Отказ от этой предпосылки еще и загоняет сервис в ограниченные рамки. Если сервис рисует только сетки из блоков, это означает, что он предназначен только для просмотра человеком в браузере. Я же предпочитаю писать в REST-стиле, чтобы вся функциональность была доступна для машинного доступа. Это совсем другая вселенная :-).

  15. Иван Сагалаев

    Кстати...

    Это все меня держит пока вдали от Джанги. Хотя, если б я присмотрелся к ней поближе, мот и нашел бы способ реализации такой архитектуры…

    А в чем проблема с Джанго в этом смысле? Оформляйте все куски шаблонными тегами, и сделайте две view: одна основная будет вызывать нужный шаблон-сетку, а другая — ajax'овая — будет тянуть содержимое тега отдельно.

  16. Alexander Solovyov

    Имхо is_ajax, оформленный декоратором, который будет устанавливать request.is_ajax, будет удобнее. :)

    А вообще у нас сейчас используются разные вью (и урлы) для ajax/не ajax, потому как функциональность очень часто заметно разная...

  17. Алексей Захлестин

    На самом деле, большая часть js-фреймворков использует заголовок "X-Requested-With" со значением "XMLHttpRequest" по которому можно выделять ajax-запросы. Для многих случаев этого достаточно
    Твой вариант более детальный — пожалуй попробую использовать и его :)

  18. Иван Сагалаев

    Имхо is_ajax, оформленный декоратором, который будет устанавливать request.is_ajax, будет удобнее. :)

    Тогда уж middleware, потому что оно не зависит от сути запроса.

  19. Mkdir

    Отличная статья. Спасибо!

  20. Pashka R.

    Идея по-поводу ответа на ajax запросы...

    Как вариант возвращать можно что-то типа:

    <?xml version="1.0"?>
    <html_piece>
    <![CDATA[
        <p>text 1</p>
        <p>text 2</p>
    ]]>
    </html_piece>
    

    и заголовок в ответе будет чётким: application/xml

    потому, что если возвращать просто

    <p>text 1</p>
    <p>text 2</p>
    

    то что писать в заголовке?

  21. Alexander Solovyov

    Тогда уж middleware, потому что оно не зависит от сути запроса.

    Да, действительно. :)

  22. Денис Зайцев

    В придумывании новых значений для стандартных вещей (содержимое заголовков, атрибуты тегов, etc) есть одна засада - в один прекрасный день разработчикам стандартов, а потом и браузеров может прийти идея использовать эти же значения.. с несколько другим смыслом :)

    Все никак не могу забыть как однажды у нас сломалась валидация форм, потому что в Опере 9 решили использовать атрибут pattern в элементах формы - она считала что там должна быть регулярка, а следовательно не пропускала любой текст, кроме String или Email.

  23. Иван Сагалаев

    Это, кстати, не в Опере решили, это WebForms 2 :-).

  24. KOSIASIK

    Сложно и ни о чём. Суть десятка обзацев сводится к тому, что отправляя запрос с нужным заголовком, получаем ответ нужного вида. Читаю автора давно, но этот пост расстроил из-за зря потерянного времени. Кстати, ещё три года назад пользовался именно таким способом и менно на php.

  25. dva

    Просто и по делу. Спасибо за подсказку как рулить заголовками via jQuery. Уж не первый год благодарен автору, что находит время писать подробно и о здравом, напоминать о незаслуженно забытом. Кстати, именно этой фичи мне не хватало в 1999 году - не нашлось способа воздействовать на заголовки тогдашних браузеров.

  26. kikaha

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

  27. Денис

    Хотя это было подмечено, повторюсь - прежде чем изобретать велосипед, посмотрите доки.

    X-Requested-With: XMLHttpRequest
    
  28. Иван Сагалаев

    Честно говоря, именно "X-Requested-With" мне кажется изобретением велосипеда со стороны создателей js-фреймворков, потому что заголовок "Accept" и типы "application/*" были уже до них. Впрочем это на самом деле мелочь. Если приживется и устаканится новый заголовок, значит так тому и быть.

  29. dva

    Хотя это было подмечено, повторюсь - прежде чем изобретать велосипед,
    посмотрите доки.

    X-Requested-With: XMLHttpRequest

    Статья ведь несколько шире и не только о. Но и "о" можно дополнить. Было подмечено:

    ...большая часть js-фреймворков использует заголовок “X-Requested-With” со
    значением “XMLHttpRequest” по которому можно выделять ajax-запросы.

    Настороженно смотрим на "большая часть" и топаем "посмотреть доки". Последовательно:

    http://docs.jquery.com/Ajax
    http://msdn2.microsoft.com/en-us/library/ms535874.aspx
    http://www.w3.org/TR/XMLHttpRequest

    И нигде не находим даже упоминания об X-Requested-With:. И, тем более, о его содержимом.

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

  30. alec

    Для того чтобы создавать веб-приложение не требуется знания TCP/IP, так же как не требуется знать принцип работы двигателя внутреннего сгорания чтобы водить автомобиль (хотя и то и другое не помешает). Но знание HTTP обязательно для веб-разработчика, так же как знание ПДД автомобилистом(хотя и то и другое часто не соблюдается :)). Вроде так? Дружно читаем стандарт rfc2616, либо книжку O'Reilly "HTTP: Definitive guide"

  31. Julik

    respond_to и всякий content negotiation это хорошо. Рельсы на самом деле тащат этот принцип дальше - можно писать свои собственные обработчики respond_to (например - "если клиент просит binary-картинку"). И работает это все прекрасно (если бы не эти бесконечные XML-флеймеры которые что ни день то "you should use applicacion/xml-super-duper-developer-draft-spec-ver.1.1.3.dev instead".

    Более того - из этого можно делать интересные солюшены. Например, есть метод выставления "сообщения" - боксика типа "ваш бан 230 юзеров был успешно установлен". Пользуясь content-negotiation можно прямо из (как вы его называете) middleware решить, показываем мы это сообщение как часть страницы или шлем кусок javascript который вставит нужный фрагмент.

    Просто из-за врожденной полу-CMS-ориентированности Django (да простит меня маньяк за эти слова) о content-negotiation в его команде мало кто пока задумывается :-) какой там негоциейшн - we need this newspaper online by yesterday morning.

  32. Julik

    Да забыл добавить - пережимать с этим не надо потому что есть задачи при которых хочется сделать curl http://some-site.com/latest-news-feed и получить именно RSS без указания дополнительных ключей.

  33. Arefiev

    Очень интересно про REST и ресурсы в Rails пишет http://www.novemberain.com/tags/REST
    очень похожий по изяществу подход.

  34. Mike

    Учитывать то, что присылает клиент в этом заголовке Accept - это супер идея! Вот только в чем здесь надуманная вина "в частности PHP"? Чего он скрыл то?

  35. dark-demon

    не надо путать тип данных с их представлением.

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

    application/html+ajax - такого миме-типа нет и быть не может, ибо:
    1. html-фрагмент не является html
    2. ajax - не тип файла, а указание на способ формирования реквеста
    3. для нестандартных расширений миме-типов нужно добавлять префикс vnd.

    в рфс чётко написано - сервер может выбирать представление основываясь на любых заголовках запроса. Accept служит для указания типа, accept-language - для указания языка и тд. в случае же аякса нужно запросить определённое представление, а для этого нужен отдельный заголовок.

    у меня, например, и по запросу странички и по аякс запросу выдаётся валидный html с соответствующим указанием типа text/html. мне нужно придумать новый миме-тип, чтобы отличать их друг от друга?

  36. Сергей

    Во-первых, автор необоснованно критикует язык PHP, показывая свое не знание последнего. PHP предоставляет полных доступ к заголовкам HTTP-запросов.

    Во-вторых, серверные приложения, на мой взгляд, должны быть абстрагированны от типа посылаемого запроса (ajax/не ajax).

    Представавленное решение проблемы определения типа запроса не является серебрянной пулей и требует настройки клиентского фреймворка, будь то jQuery или prototype.

  37. Николасс

    Ну почему чуть что, так сразу PHP виноват? Хороший программист способен реализовать программу хорошо на ЛЮБОМ языке. И он умеет выбирать средства, согласитесь в некоторых местах PHP оправданее даже того же Pyton'а

  38. Иван Сагалаев

    Я виню (опять же, повторюсь, отчасти) PHP не за то, что в нем что-то такое сделать нельзя. А за то, что он поощряет совсем другую — плохую — практику.

  39. Vadim Voituk

    Хотелось бы добавить что в prototype.js это делается аналогичным образом:
    Ajax.Options.requestHeaders = ['Accept', 'application/json']

  40. nmike

    Я виню (опять же, повторюсь, отчасти) PHP не за то, что в нем что-то такое сделать нельзя. А за то, что он поощряет совсем другую — плохую — практику.

    не понял. какую такую плохую и почему поощеряет? все механизмы для управления есть. я не знаю питон и "рельсы" - мож там по другому как то?

    ну не понял при чем тут PHP.

  41. Alexander Solovyov

    ну не понял при чем тут PHP.

    Вот эта штука всегда смешила в программерах на PHP. ;) Оно, конечно, обидно, но куда деваться - PHP поощряет плохой стиль программирования.

  42. Bonart

    Один недостаток у предложенного варианта таки есть - он затрудняет кеширование результата.

  43. ods

    Один недостаток у предложенного варианта таки есть - он затрудняет кеширование результата.

    Vary: accept

Добавить комментарий