Разработка встраиваемого JavaScript приложения

в 6:41, , рубрики: javascript, pusher, ruby on rails, Веб-разработка, метки: , ,

Вместо вступления

Я хочу рассказать вам о некоторых трудностях, с которыми мы столкнулись при разработке встраиваемого 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–у, который в свою очередь передает его пользователю.

Разработка встраиваемого JavaScript приложения

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

Источник

Поделиться

* - обязательные к заполнению поля