понедельник, 17 декабря 2007 г.

AJAX, «кирилические символы», кодировки, prototype.js, jQuery: раз и навсегда

AJAX, — это технология. Одной из часто используемых техник этой технологии является посылка запросов при помощи объекта класса XMLHttpRequest.

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

В документации на XMLHttpRequest сказано, что браузер должен поддерживать следующие типы HTTP-запросов:

GET, POST, HEAD, PUT, DELETE, OPTIONS

На сегодняшний день джаваскриптом через объект класса XMLHttpRequest можно отправить только запросы типа GET и POST.

Итак, рассмотрим 2 этих запроса:


1) Запрос типа GET:

Вся информация скрипту на сервере может передаваться только через URL и через заголовки.

Например,

GET http://moy-rebenok/ajax.php?f=324

Host: moy-rebenok

User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; ru; rv:1.8.1.11) Gecko/20071127

Firefox/2.0.0.11

Accept:

text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/pn

g,*/*;q=0.5

Accept-Language: ru-ru,ru;q=0.8,en-us;q=0.5,en;q=0.3

Accept-Encoding: gzip,deflate

Accept-Charset: windows-1251,utf-8;q=0.7,*;q=0.7

Keep-Alive: 300

Connection: keep-alive

Referer: http://moy-rebenok/ajax.html


На сервере, в ajax.php можно будет использовать конструкцию

$_GET['f'], чтобы получить значение переменной f.

Почему встает проблема с русскими буквами? Потому что, как вы знаете, русские буквы в URL использовать нельзя, их необходимо как-то передать при помощи доступных латинских букв, цифр и знаков, допустимых в URL после знака '?'.

Люди договорились, что будут делать это при помощи escape-последовательностей.

escape последовательность слова "привет" в кодировке windows-1251:

%EF%F0%E8%E2%E5%F2

escape последовательность слова "привет" в кодировке UTF-8:

%D0%BF%D1%80%D0%B8%D0%B2%D0%B5%D1%82

escape последовательность слова "привет" в кодировке KOI8-R:

%CE%CF%D5%C1%C5%D0


(Знак '%', потом код символа).


Таким образом передать русские буквы можно, например, так:

GET http://moy-rebenok/ajax.php?f=%EF%F0%E8%E2%E5%F2

Host: ...


или так:


GET http://moy-rebenok/ajax.php?f=%D0%BF%D1%80%D0%B8%D0%B2%D0%B5%D1%82

Host: ...


Никто вас в этом не ограничивает.

Кстати, для GET запроса не нужно указывать заголовок Content-Type.
Т.к. никакого контента нет. Есть только запрос по определенному адресу.
Все переменные на сервер передаются через URL.
Как же смастерить необходимую escape последовательность в нужной кодировке?
Мастерить можно хоть руками, хоть как, но естественно в JavaScript.
Опять же, никто вас не ограничивает.

Но для удобства обычно используют одну из 3 функций, которые уже определены в JavaScript:

а) escape()

б) encodeURI()

в) encodeURIComponent()


По порядку:


а) escape()

Латинские буквы, цифры, символы @*/+. оставляет как есть, всё остальное кодирует так:

%xx, либо так: %uxxxx.

Причем, xxxx во втором случае, — это код символа не в UTF-8, а в Unicode

(Разница между Unicode и UTF-8).

Использовать эту функцию не надо, т.к. результат выполнения зависит от браузера, функция не является стандартизированной W3C, возникла в лихие 90-е.

К тому же, как-то нормально (по крайней мере, быстро) обработать строку в таком винигретчатом формате на сервере сложно.

Функцию escape() использует библиотека нашего соотечественника JsHttpRequest.
Не потому что библиотека плохая, а потому что создана для работы со всеми браузерами
(в том числе и с самыми древними).


б) encodeURI()

Латинские буквы, цифры, символы !@#$&*()=:/;?+'. оставляет как есть, всё остальное

кодирует

escape-последовательностями в кодировке UTF-8.

Одобрено W3C.


в) encodeURIComponent():

Латинские буквы, цифры, символы !*()'. оставляет как есть, всё остальное кодирует

escape-последовательностями в кодировке UTF-8.

Одобрено W3C.

Используется jQuery, prototype.js при запросе методом GET.


Возможно вы слышали от кого-то: "XMLHttpRequest работает только с UTF-8".
Теперь знаете, что это не совсем правда.

Когда используется GET-запрос, то кодировка переданных данных вообще нигде не прописывается(!).

Ещё раз повторю, 'Content-type', в котором мы можем указать charset не используется в GET запросах.

Но, т.к. в JavaScript есть 2 удобные функции для перевода любой строки в строку с escape-последовательностями в UTF-8, то все их используют, и работают с UTF-8.

Именно поэтому в jQuery даже нельзя никак указать charset при отправке запроса.

Именно поэтому в Prototype.js, даже когда указываешь encoding='windows-1251', и используешь GET запрос, то передается всё равно UTF-8.

Просто потому что в кодах этих библиотек используется функция encodeURIComponent().

Что ж. В этом нет совершенно ничего плохого. Всё, что надо сделать, чтобы теперь работать в PHP в нормальной кодировке использовать iconv:

$f = iconv('UTF-8', 'windows-1251', $_GET['f']);

Кстати, мы можем это сделать именно потому, что $_GET работает так, что он понимает

escape-последовательности. Спасибо создателям PHP.

Т.е. когда приходит GET запрос PHP смотрит на URL, создает для нас массив $_GET, а мы
уже с ним что хотим, то и делаем. Но это вроде понятно должно быть.


2) POST-запросы.

Здесь уже всё интереснее.

Вот приходит это запрос на сервер. Обработчик PHP смотрит на Content-type, и в зависимости от него заполняет массив $_POST и/или переменную $HTTP_RAW_POST_DATA.

$_POST он заполняет в том случае, когда в Content-type указано multipart/form-data или

x-www-form-urlencoded.

Что-же это за Content-type такой?

А контент-тайп это очень удобный. Он позволяет передать php скрипту несколько переменных.

Что по сути такое POST запрос?

Это заголовки, а за ними контент. Контент вообще произвольный. Т.е. просто байты, байты, байты.

Но ведь из JavaScript обычно требуется передать не просто байты, байты, байты, а несколько пар ключ=значение, ключ=значение, ...

Как в GET запросе.

Вот люди и договорились о таком удобном типе, как x-www-form-urlencoded

Для того, чтобы передать f=123 и gt=null необходимо передать контент:

f=123&gt=null

Знакомо неправда ли? Конечно знакомо, и тип не зря называется x-www-form-urlencoded.

Всё то же самое, что и при GET запросе.

И как же формируется контент в библиотеках jQuery и prototype.js?

Верно, при помощи всё той же функции encodeURIComponent(), а значит и escape-последовательности будут в кодировке UTF-8. (Независимо от того, что в prototype.js вы установите encoding).

Всё. Осталась ещё одна возможность. Ведь можно передавать не x-www-form-urlencoded (т.е. не параметры), а обычный текстовый или бинарный контент, который потом можно будет прочитать через $HTTP_RAW_POST_DATA.

Для этого устанавливаем Content-type text/xml или application/octet-stream, там же устанавливаем charset="windows-1251".

Засовываем в функцию send() строку нужной кодировки. (Prototype.js оборачивает этот вызов конструкцией new Ajax.Request(...)).

И что потом... А он (объект класса XMLHttpRequest) переводит эту строку в UTF-8, в какой бы кодировке она не была. Так написано в документации W3C. И он реально это делает.

Выводы:

1. Напрямую через XMLHttpRequest можно передавать только строки в кодировке UTF-8.

2. Можно передавать строки как бы "в любых других кодировках", если нелатинские символы при этом за-escape-ены.

3. В JavaScript существует 3 функции, которые escape-ят нелатинские символы:

escape(), encodeURI() и encodeURIComponent().

Первая переводит в кривой Unicode. Вторые две в UTF-8.

Можно написать свои функции, которые будут генерировать escape-последовательности любой кодировки. Можно, но не нужно. Т.к. наоборот надо радоваться, что есть такие вот функции, которые переводят текст любой кодировки в UTF-8. Это черезвычайно прекрасный факт. Схема при которой все xhtml страницы работают на windows-1251, ajax с сервера клиенту кидает windows-1251, а ajax с клиента серверу кидает UTF-8 абсолютна приемлема и используется на большинстве ресурсов.

Просто не надо забывать использовать iconv как было описано ниже. А для того, чтобы сервер отдавал яваскрипту JSON (или что там у вас) в правильной кодировке (т.е. в такой же кодировке, в которой отдаются все xhtml страницы) просто в начале вашего ajax.php пропишите заголовок:

header('Content-type: text/html; charset=windows-1251');

И всё будет ок.


На последок немного субъективного мнения:

Используйте jQuery, любите людей, дарите подарки.

2 комментария:

Unknown комментирует...

Дебы мну взять такую пхп функцию чтобы из любой кодировки в ютф-8, без предварительного указания этой самой кодировки

Bocman комментирует...

Почти(!) слепой метод (для Ajax).

JS -> encodeURI('данные') -> PHP -> urldecode($_POST['data'])

PHP -> urlencode('данные') -> JS -> decodeURI(responseText);

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