Я считаю 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
Денис Барушев
12.09.07 23:47
Ajax-запрос еще можно отличить по заголовку:
Однозначно лучше. И не только моднее, а еще и более соотвествует REST-стилю. Всячески рекомендую к прочтению по этому поводу серию переводов.
Рассматривается имеено то, о чем ты говоришь, только в контексте Rails. Они этому учат сразу "из коробки" :)
GiNeR
13.09.07 00:21
Я всё больше поражаюсь твоей маньячной педантичности. Я не прогер, но читаю с большим интересом. Красиво.
dobrych
13.09.07 00:25
Еще один плюс для jQuery, удобно....
Илья
13.09.07 00:38
Вот это круто! Использовать вещи по назначению — это прямо физиологическое удовольствие :)
Kallisto
13.09.07 00:39
Удобно, но в принципе... &ajax=1 в закладки поместить трудно, потому как контент вставится в текст документа, потому строка браузера не изменится.
потому мне кажется на результате это не изменится.. но так как бы правельне.. имхо
Leonya
13.09.07 00:40
Тоже хотел написать, что Рельсы так работают, но меня опередили :)
Michael Klishin
13.09.07 00:45
Пользователям Rails это с 1.2 даровано сразу через respond_to и идею ресурсов со множеством представлений. Просто к слову, что оно приживается потихоньку в разных местах.
Pashka R.
13.09.07 01:02
Я надеюсь, что когда-нибудь webX.0 будет идеален... его будут творить люди, которые живут этим и любят своё дело :)
Zork
13.09.07 01:17
Мне кажется что такие заголовки как то правильней
application/xml - произвольный XML
text/javascript+json - JSON
application/xml+html - кусок HTML-документа для вставки
Elf
13.09.07 01:32
Если отказаться от этой предпосылки, то 3/4 поста можно удалить :-)
Alex Efros
13.09.07 03:09
А есть основания от неё отказываться? Я недавно проглядывал статистику, в среднем по инету JavaScript отключён у 5%. Для некоторых проектов это немало...
Hunter
13.09.07 03:31
Ну ты точно маньяк :)
Elf
13.09.07 05:30
Так я не про отказ от javascript'а. Я просто не понимаю, почему одна функция должна определять весь вид страницы (раз уж к ней идет запрос и она решает - отдавать всю страницу или только фрагмент).
Просто есть другой подход - страница = сетка. Каждый элемент ячейки - вывод какого-нить метода. Соответственно, при рендеринге страницы опрашиваются все "заинтересованные", а при обновлении аяксом вызывается только хэндлер нужной ячейки (аякс обычно так и используется - перерисовывать фрагмент страницы, логически и контекстно не связанной с остальными кусками).
И всё, всё банально сразу становится, не надо никаких мудрствований совершенно.
Это все меня держит пока вдали от Джанги. Хотя, если б я присмотрелся к ней поближе, мот и нашел бы способ реализации такой архитектуры...
Иван Сагалаев
13.09.07 09:07
А причем тут тогда XML? HTML и так-то не обязан быть XML'ом, а уже произвольный кусок -- тем более, даже если это XHTML. Пример:
Это даже не well-formed XML.
Отказ от этой предпосылки еще и загоняет сервис в ограниченные рамки. Если сервис рисует только сетки из блоков, это означает, что он предназначен только для просмотра человеком в браузере. Я же предпочитаю писать в REST-стиле, чтобы вся функциональность была доступна для машинного доступа. Это совсем другая вселенная :-).
Иван Сагалаев
13.09.07 09:09
Кстати...
А в чем проблема с Джанго в этом смысле? Оформляйте все куски шаблонными тегами, и сделайте две view: одна основная будет вызывать нужный шаблон-сетку, а другая -- ajax'овая -- будет тянуть содержимое тега отдельно.
Alexander Solovyov
13.09.07 11:16
Имхо
is_ajax, оформленный декоратором, который будет устанавливатьrequest.is_ajax, будет удобнее. :)А вообще у нас сейчас используются разные вью (и урлы) для ajax/не ajax, потому как функциональность очень часто заметно разная...
Алексей Захлестин
13.09.07 11:23
На самом деле, большая часть js-фреймворков использует заголовок "X-Requested-With" со значением "XMLHttpRequest" по которому можно выделять ajax-запросы. Для многих случаев этого достаточно
Твой вариант более детальный — пожалуй попробую использовать и его :)
Иван Сагалаев
13.09.07 11:29
Тогда уж middleware, потому что оно не зависит от сути запроса.
Mkdir
13.09.07 11:33
Отличная статья. Спасибо!
Pashka R.
13.09.07 12:57
Идея по-поводу ответа на ajax запросы...
Как вариант возвращать можно что-то типа:
и заголовок в ответе будет чётким: application/xml
потому, что если возвращать просто
то что писать в заголовке?
Alexander Solovyov
13.09.07 13:19
Да, действительно. :)
Денис Зайцев
13.09.07 14:00
В придумывании новых значений для стандартных вещей (содержимое заголовков, атрибуты тегов, etc) есть одна засада - в один прекрасный день разработчикам стандартов, а потом и браузеров может прийти идея использовать эти же значения.. с несколько другим смыслом :)
Все никак не могу забыть как однажды у нас сломалась валидация форм, потому что в Опере 9 решили использовать атрибут pattern в элементах формы - она считала что там должна быть регулярка, а следовательно не пропускала любой текст, кроме String или Email.
Иван Сагалаев
13.09.07 14:04
Это, кстати, не в Опере решили, это WebForms 2 :-).
KOSIASIK
13.09.07 14:11
Сложно и ни о чём. Суть десятка обзацев сводится к тому, что отправляя запрос с нужным заголовком, получаем ответ нужного вида. Читаю автора давно, но этот пост расстроил из-за зря потерянного времени. Кстати, ещё три года назад пользовался именно таким способом и менно на php.
dva
13.09.07 15:52
Просто и по делу. Спасибо за подсказку как рулить заголовками via jQuery. Уж не первый год благодарен автору, что находит время писать подробно и о здравом, напоминать о незаслуженно забытом. Кстати, именно этой фичи мне не хватало в 1999 году - не нашлось способа воздействовать на заголовки тогдашних браузеров.
kikaha
13.09.07 19:43
Иван, спасибо, самые очевидные и простые решения редко приходят в голову, это как раз такой случай, будем использовать.
Денис
13.09.07 20:00
Хотя это было подмечено, повторюсь - прежде чем изобретать велосипед, посмотрите доки.
Иван Сагалаев
13.09.07 21:51
Честно говоря, именно "X-Requested-With" мне кажется изобретением велосипеда со стороны создателей js-фреймворков, потому что заголовок "Accept" и типы "application/*" были уже до них. Впрочем это на самом деле мелочь. Если приживется и устаканится новый заголовок, значит так тому и быть.
dva
14.09.07 14:22
Статья ведь несколько шире и не только о. Но и "о" можно дополнить. Было подмечено:
Настороженно смотрим на "большая часть" и топаем "посмотреть доки". Последовательно:
И нигде не находим даже упоминания об X-Requested-With:. И, тем более, о его содержимом.
Вы продолжаете настаивать на использовании этого kludge? Тогда я продолжу стоять на документированном Accept.
alec
14.09.07 15:12
Для того чтобы создавать веб-приложение не требуется знания TCP/IP, так же как не требуется знать принцип работы двигателя внутреннего сгорания чтобы водить автомобиль (хотя и то и другое не помешает). Но знание HTTP обязательно для веб-разработчика, так же как знание ПДД автомобилистом(хотя и то и другое часто не соблюдается :)). Вроде так? Дружно читаем стандарт rfc2616, либо книжку O'Reilly "HTTP: Definitive guide"
Julik
16.09.07 01:53
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.
Julik
16.09.07 01:54
Да забыл добавить - пережимать с этим не надо потому что есть задачи при которых хочется сделать curl http://some-site.com/latest-news-feed и получить именно RSS без указания дополнительных ключей.
Arefiev
16.09.07 23:35
Очень интересно про REST и ресурсы в Rails пишет http://www.novemberain.com/tags/REST
очень похожий по изяществу подход.
Mike
17.09.07 16:58
Учитывать то, что присылает клиент в этом заголовке Accept - это супер идея! Вот только в чем здесь надуманная вина "в частности PHP"? Чего он скрыл то?
dark-demon
21.09.07 03:49
не надо путать тип данных с их представлением.
то, что некоторые представления имеют разные типы - вовсе не повод запрашивать именно тип, наивно полагая, что для заданного типа существует только одно представление, или же искуственно вводя новые типы для того, чтобы охватить всю гамму представлений.
application/html+ajax - такого миме-типа нет и быть не может, ибо:
1. html-фрагмент не является html
2. ajax - не тип файла, а указание на способ формирования реквеста
3. для нестандартных расширений миме-типов нужно добавлять префикс vnd.
в рфс чётко написано - сервер может выбирать представление основываясь на любых заголовках запроса. Accept служит для указания типа, accept-language - для указания языка и тд. в случае же аякса нужно запросить определённое представление, а для этого нужен отдельный заголовок.
у меня, например, и по запросу странички и по аякс запросу выдаётся валидный html с соответствующим указанием типа text/html. мне нужно придумать новый миме-тип, чтобы отличать их друг от друга?
Сергей
28.09.07 20:43
Во-первых, автор необоснованно критикует язык PHP, показывая свое не знание последнего. PHP предоставляет полных доступ к заголовкам HTTP-запросов.
Во-вторых, серверные приложения, на мой взгляд, должны быть абстрагированны от типа посылаемого запроса (ajax/не ajax).
Представавленное решение проблемы определения типа запроса не является серебрянной пулей и требует настройки клиентского фреймворка, будь то jQuery или prototype.
Николасс
9.10.07 17:48
Ну почему чуть что, так сразу PHP виноват? Хороший программист способен реализовать программу хорошо на ЛЮБОМ языке. И он умеет выбирать средства, согласитесь в некоторых местах PHP оправданее даже того же Pyton'а
Иван Сагалаев
9.10.07 19:13
Я виню (опять же, повторюсь, отчасти) PHP не за то, что в нем что-то такое сделать нельзя. А за то, что он поощряет совсем другую -- плохую -- практику.
Vadim Voituk
14.10.07 12:37
Хотелось бы добавить что в prototype.js это делается аналогичным образом:
Ajax.Options.requestHeaders = ['Accept', 'application/json']
nmike
15.10.07 09:37
не понял. какую такую плохую и почему поощеряет? все механизмы для управления есть. я не знаю питон и "рельсы" - мож там по другому как то?
ну не понял при чем тут PHP.
Alexander Solovyov
15.10.07 15:34
Вот эта штука всегда смешила в программерах на PHP. ;) Оно, конечно, обидно, но куда деваться - PHP поощряет плохой стиль программирования.
Bonart
10.12.07 07:17
Один недостаток у предложенного варианта таки есть - он затрудняет кеширование результата.
ods
26.12.07 15:30
Vary: accept