- PVSM.RU - https://www.pvsm.ru -

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

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

Я хочу рассказать вам о некоторых трудностях, с которыми мы столкнулись при разработке встраиваемого JavaScript приложения, виджета комментариев "Комментатр [1]".
В этой статье я опишу некоторые проблемы и тонкости разработки подобных приложений, а также предложу варианты их решения.
В качестве backend–решения мы используем приложение на Ruby on Rails [2], поэтому некоторые фрагменты этой статьи будут специфичны для Rails–окружения.

Комментатр [3] состоит из двух проектов: API и виджета, который устанавливается на сайт клиента. Речь пойдет об их взаимодействии между собой и виджета с сайтом клиента. В основном общение виджета и API происходит посредством JSONP [4], который, как известно, поддерживает только GET–запросы. В связи с этим возникает первая сложность.

Добавление комментария

Так как комментарий может быть достаточно объемным, мы не можем использовать для его отправки метод GET, например, из-за ограничений по длине запроса в Internet Explorer, соответственно, про JSONP можем тоже смело забыть. Мы не можем отправить POST–запрос на домен приложения с сайта клиента через AJAX из–за ограничений, которые накладывает Same Origin Policy [5], но мы можем отправить форму методом POST в невидимый фрейм.
Результат добавления комментария можно получить разными способами, например, с помощью easyXDM [6].
Но мы пойдем другим путем и будем использовать Pusher [7]. Мы уже используем Pusher для моментального добавления комментариев на открытые у всех пользователей страницы, а также для отображения его в интерфейсе для модераторов. Гораздо удобнее использовать один инструмент для выполнения нескольких задач, тем более что Pusher с ними прекрасно справляется.
В итоге у нас получается следующая схема: пользователь отправляет комментарий к нам на сервер, мы его обрабатываем и передаем результат добавления Pusher–у, который в свою очередь передает его пользователю.

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

Pusher и Unicorn

В качестве веб–сервера мы используем Unicorn [8]. Отправка события Pusher–у занимает некоторое время, которое не так заметно в случае отправки всего одного события. Однако при отправке нескольких событий время отклика значительно увеличивается и становится неприемлемым. Unicorn не относится к семейству evented–серверов, значит, мы не можем сделать отправку сообытий асинхронной.

Вариант решения этой проблемы, предложенный разработчиками Pusher, — это запуск EventMachine [9] в треде, которой делегируется отправка сообщений, рядом с каждым процессом 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

Об этом решении написана небольшая статья [10] разработчиками Gauges [11], а также есть готовое решение [12].
Запуск дополнительного треда с EventMachine для каждого воркера не кажется мне хорошей идеей.

Второй вариант — запуск рядом с нашим веб–сервером с Unicorn еще одного маленького веб–сервера с Thin, который без проблем справляетя со своей единственной задачей — проксированием запросов к Pusher таким образом, что отправка прокси–серверу запроса занимает время, стремящееся к нулю, а уже переправка им запроса в Pusher — дело второе.

Мы какое-то время использовали такое решение [13], но впоследствии от него отказались, и остановились на третьем варианте — передавать отправку сообщений delayed job процессору, в нашем случае, выбрав для этого Sidekiq [14]. Он моментально берет в обработку задачи, которые появляются в очереди, и способен очень быстро их выполнить.

Вставка виджета с кодировкой 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

Источник [15]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/javascript/31806

Ссылки в тексте:

[1] Комментатр: http://commentatr.net/demo/one

[2] Ruby on Rails: http://rubyonrails.org/

[3] Комментатр: http://commentatr.net

[4] JSONP: http://ru.wikipedia.org/wiki/JSONP

[5] Same Origin Policy: http://ru.wikipedia.org/wiki/%D0%9F%D1%80%D0%B0%D0%B2%D0%B8%D0%BB%D0%BE_%D0%BE%D0%B3%D1%80%D0%B0%D0%BD%D0%B8%D1%87%D0%B5%D0%BD%D0%B8%D1%8F_%D0%B4%D0%BE%D0%BC%D0%B5%D0%BD%D0%B0

[6] easyXDM: https://github.com/oyvindkinsey/easyXDM

[7] Pusher: http://pusher.com/

[8] Unicorn: http://unicorn.bogomips.org/

[9] EventMachine: https://github.com/eventmachine/eventmachine

[10] небольшая статья: http://www.railstips.org/blog/archives/2011/05/04/eventmachine-and-passenger/

[11] Gauges: http://get.gaug.es/

[12] готовое решение: https://github.com/jdudek/pusher-async

[13] такое решение: https://github.com/commentatr/messenger

[14] Sidekiq: http://mperham.github.io/sidekiq/

[15] Источник: http://habrahabr.ru/post/175753/