Я считаю 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/javascript | JSON |
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
Ajax-запрос еще можно отличить по заголовку:
Однозначно лучше. И не только моднее, а еще и более соотвествует REST-стилю. Всячески рекомендую к прочтению по этому поводу серию переводов.
Рассматривается имеено то, о чем ты говоришь, только в контексте Rails. Они этому учат сразу "из коробки" :)
Я всё больше поражаюсь твоей маньячной педантичности. Я не прогер, но читаю с большим интересом. Красиво.
Еще один плюс для jQuery, удобно....
Вот это круто! Использовать вещи по назначению — это прямо физиологическое удовольствие :)
Удобно, но в принципе... &ajax=1 в закладки поместить трудно, потому как контент вставится в текст документа, потому строка браузера не изменится.
потому мне кажется на результате это не изменится.. но так как бы правельне.. имхо
Тоже хотел написать, что Рельсы так работают, но меня опередили :)
Пользователям Rails это с 1.2 даровано сразу через respond_to и идею ресурсов со множеством представлений. Просто к слову, что оно приживается потихоньку в разных местах.
Я надеюсь, что когда-нибудь webX.0 будет идеален... его будут творить люди, которые живут этим и любят своё дело :)
Мне кажется что такие заголовки как то правильней
application/xml - произвольный XML
text/javascript+json - JSON
application/xml+html - кусок HTML-документа для вставки
Если отказаться от этой предпосылки, то 3/4 поста можно удалить :-)
А есть основания от неё отказываться? Я недавно проглядывал статистику, в среднем по инету JavaScript отключён у 5%. Для некоторых проектов это немало...
Ну ты точно маньяк :)
Так я не про отказ от javascript'а. Я просто не понимаю, почему одна функция должна определять весь вид страницы (раз уж к ней идет запрос и она решает - отдавать всю страницу или только фрагмент).
Просто есть другой подход - страница = сетка. Каждый элемент ячейки - вывод какого-нить метода. Соответственно, при рендеринге страницы опрашиваются все "заинтересованные", а при обновлении аяксом вызывается только хэндлер нужной ячейки (аякс обычно так и используется - перерисовывать фрагмент страницы, логически и контекстно не связанной с остальными кусками).
И всё, всё банально сразу становится, не надо никаких мудрствований совершенно.
Это все меня держит пока вдали от Джанги. Хотя, если б я присмотрелся к ней поближе, мот и нашел бы способ реализации такой архитектуры...
А причем тут тогда XML? HTML и так-то не обязан быть XML'ом, а уже произвольный кусок — тем более, даже если это XHTML. Пример:
Это даже не well-formed XML.
Отказ от этой предпосылки еще и загоняет сервис в ограниченные рамки. Если сервис рисует только сетки из блоков, это означает, что он предназначен только для просмотра человеком в браузере. Я же предпочитаю писать в REST-стиле, чтобы вся функциональность была доступна для машинного доступа. Это совсем другая вселенная :-).
Кстати...
А в чем проблема с Джанго в этом смысле? Оформляйте все куски шаблонными тегами, и сделайте две view: одна основная будет вызывать нужный шаблон-сетку, а другая — ajax'овая — будет тянуть содержимое тега отдельно.
Имхо
is_ajax
, оформленный декоратором, который будет устанавливатьrequest.is_ajax
, будет удобнее. :)А вообще у нас сейчас используются разные вью (и урлы) для ajax/не ajax, потому как функциональность очень часто заметно разная...
На самом деле, большая часть js-фреймворков использует заголовок "X-Requested-With" со значением "XMLHttpRequest" по которому можно выделять ajax-запросы. Для многих случаев этого достаточно
Твой вариант более детальный — пожалуй попробую использовать и его :)
Тогда уж middleware, потому что оно не зависит от сути запроса.
Отличная статья. Спасибо!
Идея по-поводу ответа на ajax запросы...
Как вариант возвращать можно что-то типа:
и заголовок в ответе будет чётким: application/xml
потому, что если возвращать просто
то что писать в заголовке?
Да, действительно. :)
В придумывании новых значений для стандартных вещей (содержимое заголовков, атрибуты тегов, etc) есть одна засада - в один прекрасный день разработчикам стандартов, а потом и браузеров может прийти идея использовать эти же значения.. с несколько другим смыслом :)
Все никак не могу забыть как однажды у нас сломалась валидация форм, потому что в Опере 9 решили использовать атрибут pattern в элементах формы - она считала что там должна быть регулярка, а следовательно не пропускала любой текст, кроме String или Email.
Это, кстати, не в Опере решили, это WebForms 2 :-).
Сложно и ни о чём. Суть десятка обзацев сводится к тому, что отправляя запрос с нужным заголовком, получаем ответ нужного вида. Читаю автора давно, но этот пост расстроил из-за зря потерянного времени. Кстати, ещё три года назад пользовался именно таким способом и менно на php.
Просто и по делу. Спасибо за подсказку как рулить заголовками via jQuery. Уж не первый год благодарен автору, что находит время писать подробно и о здравом, напоминать о незаслуженно забытом. Кстати, именно этой фичи мне не хватало в 1999 году - не нашлось способа воздействовать на заголовки тогдашних браузеров.
Иван, спасибо, самые очевидные и простые решения редко приходят в голову, это как раз такой случай, будем использовать.
Хотя это было подмечено, повторюсь - прежде чем изобретать велосипед, посмотрите доки.
Честно говоря, именно "X-Requested-With" мне кажется изобретением велосипеда со стороны создателей js-фреймворков, потому что заголовок "Accept" и типы "application/*" были уже до них. Впрочем это на самом деле мелочь. Если приживется и устаканится новый заголовок, значит так тому и быть.
Статья ведь несколько шире и не только о. Но и "о" можно дополнить. Было подмечено:
Настороженно смотрим на "большая часть" и топаем "посмотреть доки". Последовательно:
И нигде не находим даже упоминания об X-Requested-With:. И, тем более, о его содержимом.
Вы продолжаете настаивать на использовании этого kludge? Тогда я продолжу стоять на документированном Accept.
Для того чтобы создавать веб-приложение не требуется знания TCP/IP, так же как не требуется знать принцип работы двигателя внутреннего сгорания чтобы водить автомобиль (хотя и то и другое не помешает). Но знание HTTP обязательно для веб-разработчика, так же как знание ПДД автомобилистом(хотя и то и другое часто не соблюдается :)). Вроде так? Дружно читаем стандарт rfc2616, либо книжку O'Reilly "HTTP: Definitive guide"
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.
Да забыл добавить - пережимать с этим не надо потому что есть задачи при которых хочется сделать curl http://some-site.com/latest-news-feed и получить именно RSS без указания дополнительных ключей.
Очень интересно про REST и ресурсы в Rails пишет http://www.novemberain.com/tags/REST
очень похожий по изяществу подход.
Учитывать то, что присылает клиент в этом заголовке Accept - это супер идея! Вот только в чем здесь надуманная вина "в частности PHP"? Чего он скрыл то?
не надо путать тип данных с их представлением.
то, что некоторые представления имеют разные типы - вовсе не повод запрашивать именно тип, наивно полагая, что для заданного типа существует только одно представление, или же искуственно вводя новые типы для того, чтобы охватить всю гамму представлений.
application/html+ajax - такого миме-типа нет и быть не может, ибо:
1. html-фрагмент не является html
2. ajax - не тип файла, а указание на способ формирования реквеста
3. для нестандартных расширений миме-типов нужно добавлять префикс vnd.
в рфс чётко написано - сервер может выбирать представление основываясь на любых заголовках запроса. Accept служит для указания типа, accept-language - для указания языка и тд. в случае же аякса нужно запросить определённое представление, а для этого нужен отдельный заголовок.
у меня, например, и по запросу странички и по аякс запросу выдаётся валидный html с соответствующим указанием типа text/html. мне нужно придумать новый миме-тип, чтобы отличать их друг от друга?
Во-первых, автор необоснованно критикует язык PHP, показывая свое не знание последнего. PHP предоставляет полных доступ к заголовкам HTTP-запросов.
Во-вторых, серверные приложения, на мой взгляд, должны быть абстрагированны от типа посылаемого запроса (ajax/не ajax).
Представавленное решение проблемы определения типа запроса не является серебрянной пулей и требует настройки клиентского фреймворка, будь то jQuery или prototype.
Ну почему чуть что, так сразу PHP виноват? Хороший программист способен реализовать программу хорошо на ЛЮБОМ языке. И он умеет выбирать средства, согласитесь в некоторых местах PHP оправданее даже того же Pyton'а
Я виню (опять же, повторюсь, отчасти) PHP не за то, что в нем что-то такое сделать нельзя. А за то, что он поощряет совсем другую — плохую — практику.
Хотелось бы добавить что в prototype.js это делается аналогичным образом:
Ajax.Options.requestHeaders = ['Accept', 'application/json']
не понял. какую такую плохую и почему поощеряет? все механизмы для управления есть. я не знаю питон и "рельсы" - мож там по другому как то?
ну не понял при чем тут PHP.
Вот эта штука всегда смешила в программерах на PHP. ;) Оно, конечно, обидно, но куда деваться - PHP поощряет плохой стиль программирования.
Один недостаток у предложенного варианта таки есть - он затрудняет кеширование результата.
Vary: accept