Вместо вступления
Я хочу рассказать вам о некоторых трудностях, с которыми мы столкнулись при разработке встраиваемого JavaScript приложения, виджета комментариев "Комментатр".
В этой статье я опишу некоторые проблемы и тонкости разработки подобных приложений, а также предложу варианты их решения.
В качестве backend–решения мы используем приложение на Ruby on Rails, поэтому некоторые фрагменты этой статьи будут специфичны для Rails–окружения.
Комментатр состоит из двух проектов: API и виджета, который устанавливается на сайт клиента. Речь пойдет об их взаимодействии между собой и виджета с сайтом клиента. В основном общение виджета и API происходит посредством JSONP, который, как известно, поддерживает только GET–запросы. В связи с этим возникает первая сложность.
Добавление комментария
Так как комментарий может быть достаточно объемным, мы не можем использовать для его отправки метод GET, например, из-за ограничений по длине запроса в Internet Explorer, соответственно, про JSONP можем тоже смело забыть. Мы не можем отправить POST–запрос на домен приложения с сайта клиента через AJAX из–за ограничений, которые накладывает Same Origin Policy, но мы можем отправить форму методом POST в невидимый фрейм.
Результат добавления комментария можно получить разными способами, например, с помощью easyXDM.
Но мы пойдем другим путем и будем использовать Pusher. Мы уже используем Pusher для моментального добавления комментариев на открытые у всех пользователей страницы, а также для отображения его в интерфейсе для модераторов. Гораздо удобнее использовать один инструмент для выполнения нескольких задач, тем более что Pusher с ними прекрасно справляется.
В итоге у нас получается следующая схема: пользователь отправляет комментарий к нам на сервер, мы его обрабатываем и передаем результат добавления Pusher–у, который в свою очередь передает его пользователю.
Pusher и Unicorn
В качестве веб–сервера мы используем Unicorn. Отправка события Pusher–у занимает некоторое время, которое не так заметно в случае отправки всего одного события. Однако при отправке нескольких событий время отклика значительно увеличивается и становится неприемлемым. Unicorn не относится к семейству evented–серверов, значит, мы не можем сделать отправку сообытий асинхронной.
Вариант решения этой проблемы, предложенный разработчиками Pusher, — это запуск EventMachine в треде, которой делегируется отправка сообщений, рядом с каждым процессом Unicorn.
module Pusher
module Async
class << self
def spawn
Thread.new { EM.run } unless EM.reactor_running?
end
def respawn
EM.stop if EM.reactor_running?
spawn
end
end
end
class Request
alias :send_async_without_next_tick :send_async
def send_async
df = EM::DefaultDeferrable.new
EM.next_tick do
send_async_without_next_tick
.callback{ |response| df.succeed(response) }
.errback{ |error| df.fail(error) }
end
df
end
end
end
Об этом решении написана небольшая статья разработчиками Gauges, а также есть готовое решение.
Запуск дополнительного треда с EventMachine для каждого воркера не кажется мне хорошей идеей.
Второй вариант — запуск рядом с нашим веб–сервером с Unicorn еще одного маленького веб–сервера с Thin, который без проблем справляетя со своей единственной задачей — проксированием запросов к Pusher таким образом, что отправка прокси–серверу запроса занимает время, стремящееся к нулю, а уже переправка им запроса в Pusher — дело второе.
Мы какое-то время использовали такое решение, но впоследствии от него отказались, и остановились на третьем варианте — передавать отправку сообщений delayed job процессору, в нашем случае, выбрав для этого Sidekiq. Он моментально берет в обработку задачи, которые появляются в очереди, и способен очень быстро их выполнить.
Вставка виджета с кодировкой UTF-8 на страницу с другой кодировкой
Теперь, когда проблема с кросс–доменными запросами решена, нас ждет следующая — кодировки. Если просто вставить джаваскрипт с UTF-8 на страницу с windows-1251, то все символы UTF-8 в нем будут отображаться некорректно. Чтобы решить эту проблему, необходимо добавить тэгу <script>
аттрибут charset
со значением UTF-8
.
<script src="http://example.com/script_in_unicode.js" charset="UTF-8"></script>
В целях уменьшения количества дополнительных запросов для виджета, мы «упаковываем» его стили внутрь скрипта, а после установки виджета копируем их внутрь тэга <style>
, который создаем сразу после загрузки. При использовании юникод–символов в стилях, например, для создания разделителей, необходимо добавить в начало стиля медиа–запрос @charset
.
Это необходимо сделать даже в случае, если мы уже получили код стилей в корректной кодировке, иначе стили будут обработаны в кодировке принимающей страницы.
<style type="text/css">
@charset "UTF-8";
.breadcrumbs li:before {
content: "→";
}
.breadcrumbs li:first-child:before {
content: "";
}
</style>
Третье и, возможно, самое важное, что необходимо сделать, — это указать форме добавления комментария, что ее содержимое нужно отправлять на сервер именно в UTF-8, в противном случае нам придется угадывать кодировку каждой страницы клиента уже на сервере, а это занятие неблагодарное.
<form action="http://example.com/comments" method="post" target="hidden-iframe" enctype="application/x-www-form-urlencoded;charset=UTF-8" accept-charset="UTF-8">
<textarea name="body"></textarea>
</form>
HTTP–заголовки
И последнее, о чем хочется рассказать в этой статье, — это внимание к заголовкам, которые отдает веб–сервер для динамических страниц и статики.
Для того, чтобы успешно отдавать страницу авторизации пользователя во фрейме, необходимо, чтобы веб–сервер не отдавал заголовки X-Frame-Options
, X-Content-Type-Options
и X-XSS-Protection
, но обязательно отдавал Content-Type
с указанной кодировкой.
С отдачей статики тоже есть свои тонкости. Firefox, например, не будет загружать шрифты с вашего сайта, пока вы не добавите заголовок Access-Control-Allow-Origin: *
Резюмируя
Разумеется, это далеко не полный список нюансов, с которыми столкнется разработчик подобных решений, но, я надеюсь, этот материал окажется полезным.
Автор: chez