Работа с JavaScript в Rails

Это руководство раскрывает встроенный в Rails функционал Ajax/JavaScript (и даже чуть больше), который позволит вам с легкостью создать насыщенные и динамические приложения Ajax!

После прочтения этого руководства вы узнаете:

  • Об основах Ajax.
  • О ненавязчивом JavaScript.
  • Как помогут встроенные хелперы Rails.
  • Как обрабатывать Ajax на стороне сервера.
  • О геме Turbolinks.

1. Введение в Ajax

Чтобы понять Ajax, нужно сперва понять, что обычно делает браузер.

Когда вы переходите на http://localhost:3000 в своем браузере, браузер (ваш 'клиент') осуществляет запрос к серверу. Он парсит отклик, затем получает все связанные файлы ресурсов, такие как файлы JavaScript, таблицы стилей и изображения. Затем он собирает страницу. Если вы нажмете на ссылку, он сделает тоже самое: получит страницу, получит файлы ресурсов, сложит их вместе, отобразит результаты. Это называется 'цикл запроса'.

JavaScript также может осуществлять запросы к серверу и парсить отклики. У него также есть возможность обновить информацию на странице. Объединив эти две силы, программист JavaScript может изготовить веб-страницу, обновляющую лишь части себя, без необходимости получения полных данных с сервера. Эту мощную технологию мы называем Ajax.

Rails поставляется по умолчанию с CoffeeScript, поэтому остальные примеры в этом руководстве будут на CoffeeScript. Все эти уроки, применимы и к чистому JavaScript.

К примеру, вот некоторый код CoffeeScript, осуществляющий запрос Ajax с использованием библиотеки jQuery:

$.ajax(url: "/test").done (html) ->
  $("#results").append html

Этот код получает данные из "/test", а затем присоединяет результат к div с id results.

Rails предоставляет небольшую встроенную поддержку для создания веб-страниц с помощью такой техники. Вам редко придется писать такой код самим. Оставшаяся часть руководства покажет, как Rails может помочь создавать сайты схожим образом, но в основе лежит эта довольно простая техника.

2. Ненавязчивый JavaScript

Rails использует технику "ненавязчивый JavaScript" для управления присоединением JavaScript к DOM. Обычно он рассматривается как лучшая практика во фронтенд-сообществе, но иногда встречаются статьи, демонстрирующие иные способы.

Вот простейший способ написания JavaScript. Его называют 'встроенный JavaScript':

<a href="#" onclick="this.style.backgroundColor='#990000'">Paint it red</a>

При нажатии, задний фон ссылки станет красным. Проблема в следующем: что будет, если у нас много JavaScript, который мы хотим запустить по щелчку?

<a href="#" onclick="this.style.backgroundColor='#009900';this.style.color='#FFFFFF';">Paint it green</a>

Некрасиво, правда? Можно вытащить определение функции из обработчика щелчка, и перевести его в CoffeeScript:

@paintIt = (element, backgroundColor, textColor) ->
  element.style.backgroundColor = backgroundColor
  if textColor?
    element.style.color = textColor

А затем на нашей странице:

<a href="#" onclick="paintIt(this, '#990000')">Paint it red</a>

Немного лучше, но как насчет нескольких ссылок, для которых нужен тот же эффект?

<a href="#" onclick="paintIt(this, '#990000')">Paint it red</a>
<a href="#" onclick="paintIt(this, '#009900', '#FFFFFF')">Paint it green</a>
<a href="#" onclick="paintIt(this, '#000099', '#FFFFFF')">Paint it blue</a>

Совсем не DRY, да? Это можно исправить, используя события. Мы добавим атрибут data-* нашим ссылкам, а затем привяжем обработчик на событие щелчка для каждой ссылки, имеющей этот атрибут:

@paintIt = (element, backgroundColor, textColor) ->
  element.style.backgroundColor = backgroundColor
  if textColor?
    element.style.color = textColor

$ ->
  $("a[data-background-color]").click (e) ->
    e.preventDefault()

    backgroundColor = $(this).data("background-color")
    textColor = $(this).data("text-color")
    paintIt(this, backgroundColor, textColor)

<a href="#" data-background-color="#990000">Paint it red</a>
<a href="#" data-background-color="#009900" data-text-color="#FFFFFF">Paint it green</a>
<a href="#" data-background-color="#000099" data-text-color="#FFFFFF">Paint it blue</a>

Это называется 'ненавязчивым' JavaScript, так как мы больше не смешиваем JavaScript с HTML. Мы должным образом разделили ответственность, сделав будущие изменения простыми. Можно с легкостью добавить поведение для любой ссылки, добавив атрибут data. Можно пропустить весь наш JavaScript через минимайзер. Этот JavaScript можно подключить на каждой странице, что означает, что он будет загружен только при загрузке первой страницы, затем будет кэширован для остальных страниц. Множество небольших преимуществ.

Команда Rails настойчиво рекомендует вам писать свой CoffeeScript (и JavaScript) в таком стиле, множество библиотек также соответствуют этому паттерну.

3. Встроенные хелперы

3.1. Remote элементы

Rails предоставляет ряд вспомогательных методов для вьюх, написанных на Ruby, помогающих вам создавать HTML. Иногда хочется добавить немного Ajax к этим элементам, и Rails подсобит в таких случаях.

Так как JavaScript ненавязчив, "Ajax-хелперы" Rails фактически состоят из двух частей: часть JavaScript и часть Ruby.

Если вы не отключили Asset Pipeline, rails-ujs представляет часть для JavaScript, а хелперы вьюх на обычном Ruby добавляют подходящие теги в DOM.

Ниже вы можете прочитать о различных событиях, которые вызываются при работе с remote элементами внутри вашего приложения.

3.1.1. form_with

form_with - это хелпер, помогающий писать формы. По умолчанию, form_with предполагает, что ваша форма будет использовать Ajax. Вы можете отказаться от этого поведения, передав :local опцию в form_with.

<%= form_with(model: @article) do |f| %>
  ...
<% end %>

Это создаст следующий HTML:

<form action="/articles" method="post" data-remote="true">
  ...
</form>

Обратите внимание на data-remote="true". Теперь форма будет подтверждена с помощью Ajax вместо обычного браузерного механизма подтверждения.

Впрочем, вы не хотите просто сидеть с заполненной формой. Возможно, вы хотите что-то сделать при успешном подтверждении. Для этого привяжите что-нибудь к событию ajax:success. При неудаче используйте ajax:error. Посмотрите:

$(document).ready ->
  $("#new_article").on("ajax:success", (e, data, status, xhr) ->
    $("#new_article").append xhr.responseText
   ).on "ajax:error", (e, xhr, status, error) ->
    $("#new_article").append "<p>ERROR</p>"

Очевидно, что хочется чего-то большего, но ведь это только начало.

link_to - это хелпер, помогающий создавать ссылки. У него есть опция :remote, которую используют следующим образом:

<%= link_to "an article", @article, remote: true %>

что создаст

<a href="/articles/1" data-remote="true">an article</a>

Можно привязаться к тем же событиям Ajax, что и в form_with. Вот пример. Предположим, имеется список публикаций, которые можно удалить одним щелчком. Нужно создать некоторый HTML, например так:

<%= link_to "Delete article", @article, remote: true, method: :delete %>

и написать некоторый CoffeeScript:

$ ->
  $("a[data-remote]").on "ajax:success", (e, data, status, xhr) ->
    alert "The article was deleted."

3.1.3. button_to

button_to - это хелпер, помогающий создавать кнопки. У него есть опция :remote, которая вызывается так:

<%= button_to "An article", @article, remote: true %>

это создаст

<form action="/articles/1" class="button_to" data-remote="true" method="post">
  <input type="submit" value="An article" />
</form>

Поскольку это всего лишь <form>, применима вся информация, что и для form_with.

3.2. Настройка remote элементов

Можно настроить поведение элементов с атрибутом data-remote без написания строк на JavaScript. Вы можете указать дополнительные data-атрибуты для достижения этой цели.

3.2.1. data-method

Нажатие на гиперссылки всегда приводит к запросу HTTP GET. Однако, если ваше приложение - RESTful, то некоторые ссылки фактически являются действиями, которые изменяют данные на сервере и должны выполняться с не-GET запросами. Этот атрибут позволяет пометить такие ссылки с помощью явного метода, такого как "post", "put" или "delete".

Суть его работы заключается в том, что после нажатия на ссылку, она создает скрытую форму в документе с атрибутом "action", который соответствует значению "href" ссылки, и методу, соответствующему значению data-method, и отправляет эту форму.

Поскольку отправка форм с помощью методов HTTP, отличных от GET и POST, поддерживается не всеми браузерами, то все остальные HTTP методы фактически отправляются через POST с использованием метода, указанного в параметре _method. Rails автоматически обнаруживает и компенсирует это.

3.2.2. data-url и data-params

Некоторые элементы вашей страницы на самом деле не ссылаются на какой-либо URL, но вам может понадобиться, чтобы они вызывали Ajax. Указание атрибута data-url вместе с data-remote вызовет Ajax для заданного URL. Вы также можете указать дополнительные параметры через атрибут data-params.

Это может быть полезно для того, чтобы вызвать действие над чекбоксами, например:

<input type="checkbox" data-remote="true"
    data-url="/update" data-params="id=10" data-method="put">

3.2.3. data-type

Также можно явно определить Ajax dataType при выполнении запросов для элементов data-remote, посредством атрибута data-type.

3.3. Подтверждения

Вы можете запросить дополнительное подтверждение пользователя, добавив атрибут data-confirm в ссылки и формы. Пользователю будет показано JavaScript диалоговое окно confirm(), содержащее текст атрибута. Если пользователь решит нажать на "отменить", действие не будет выполнено.

Добавление этого атрибута в теги ссылки вызовет диалоговое окно при нажатии на нее, и добавление атрибута в теги формы вызовет его при отправке. Например:

<%= link_to "Dangerous zone", dangerous_zone_path,
  data: { confirm: 'Are you sure?' } %>

Это создаст:

<a href="..." data-confirm="Are you sure?">Dangerous zone</a>

Атрибут также разрешено использовать для кнопок отправки формы. Это позволяет настроить предупреждающее сообщение в зависимости от кнопки, которая была нажата. В этом случае у вас не должно быть data-confirm в самой форме.

Подтверждение по умолчанию использует JavaScript диалоговое окно confirm, но вы можете настроить его, прослушивая событие confirm, которое срабатывает непосредственно перед тем, как окно подтверждения появляется у пользователя. Чтобы отменить это подтверждение по умолчанию, попросите обработчик confirm возвратить false.

3.4. Автоматическое отключение

Также возможно автоматически отключить возможность ввода, пока форма отправляется с помощью атрибута data-disable-with. Это делается для предотвращения случайного двойного щелчка пользователя, что может привести к дублированию HTTP-запросов, которые бэкенд может не обнаружить как таковой. Значение атрибута - это текст, который станет новым значением кнопки в отключенном состоянии.

Это также работает для ссылок с атрибутом data-method.

Например:

<%= form_with(model: @article.new) do |f| %>
  <%= f.submit data: { "disable-with": "Saving..." } %>
<%= end %>

Это создаст форму с:

<input data-disable-with="Saving..." type="submit">

4. Работа с событиями Ajax

Ниже перечислены различные события, которые срабатывают при обработке элементов с атрибутом data-remote:

Все обработчики, привязанные к этим событиям, всегда передают объект события в качестве первого аргумента. В приведенной ниже таблице описаны дополнительные параметры, переданные после аргумента события. Например, если дополнительные параметры указаны как xhr, settings, то для доступа к ним вы должны определить ваш обработчик с помощью function(event, xhr, settings).

Имя события Доп. параметры Срабатывают
ajax:before Перед всем ajax-бизнесом, прерывается, если остановлен.
ajax:beforeSend xhr, options Перед отправкой запроса, прерывается, если остановлен.
ajax:send xhr Когда запрос отправлен.
ajax:success xhr, status, err После завершения, если ответ был success.
ajax:error xhr, status, err После завершения, если ответ был error.
ajax:complete xhr, status После завершения запроса, независимо от результата.
ajax:aborted:file elements Если входной файл - непустой, прерывается, если остановлен.

4.1. Останавливаемые события

Если вы остановите ajax:before или ajax:beforeSend, возвратив false из обработчика метода, запрос Ajax никогда не будет выполнен. Событие ajax:before также полезно для манипулирования данными формы перед сериализацией. Событие ajax:beforeSend также полезно для добавления пользовательских заголовков запроса.

Если вы остановите событие ajax:aborted:file, поведение по умолчанию, позволяющее браузеру отправлять форму обычным способом (то есть не-AJAX представление), будет отменено и форма вообще не будет отправлена. Это полезно для реализации вашего собственного AJAX способа загрузки файлов.

5. Со стороны сервера

Ajax - это не только сторона клиента, необходимо также поработать на стороне сервера, чтобы добавить его поддержку. Часто людям нравится, когда на их запросы Ajax возвращается JSON, а не HTML. Давайте обсудим, что необходимо для этого сделать.

5.1. Простой пример

Представьте, что у вас есть ряд пользователей, которых нужно отобразить, и предоставить форму на той же странице для создания нового пользователя. Экшн index вашего контроллера выглядит так:

class UsersController < ApplicationController
  def index
    @users = User.all
    @user = User.new
  end
  # ...

Вьюха для index (app/views/users/index.html.erb) содержит:

<b>Users</b>

<ul id="users">
<%= render @users %>
</ul>

<br>

<%= form_with(model: @user) do |f| %>
  <%= f.label :name %><br>
  <%= f.text_field :name %>
  <%= f.submit %>
<% end %>

Партиал app/views/users/_user.html.erb содержит следующее:

<li><%= user.name %></li>

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

Нижняя форма вызовет экшн create в UsersController. Так как у формы опция remote установлена true, запрос будет передан через post к UsersController как запрос Ajax, ожидая JavaScript. Чтобы обслужить этот запрос, экшн create вашего контроллера должен выглядеть так:

  # app/controllers/users_controller.rb
  # ......
  def create
    @user = User.new(params[:user])

    respond_to do |format|
      if @user.save
        format.html { redirect_to @user, notice: 'User was successfully created.' }
        format.js
        format.json { render json: @user, status: :created, location: @user }
      else
        format.html { render action: "new" }
        format.json { render json: @user.errors, status: :unprocessable_entity }
      end
    end
  end

Обратите внимание на format.js в блоке respond_to, который позволяет контроллеру откликаться на ваши запросы Ajax. Далее необходим соответствующий файл вьюхи app/views/users/create.js.erb, создающий фактический код JavaScript, который будет отослан и исполнен на стороне клиента.

$("<%= escape_javascript(render @user) %>").appendTo("#users");

Rails поставляется с библиотекой Turbolinks, использующей Ajax для ускорения рендеринга страницы в большинстве приложений.

Turbolinks добавляет обработчик щелчков на всех тегах <a> на странице. Если ваш браузер поддерживает PushState, Turbolinks сделает запрос Ajax для страницы, распарсит отклик и заменит полностью <body> страницы на <body> отклика. Затем он использует PushState для изменения URL на правильный, сохраняя семантику для обновления и предоставляя красивые URL.

Единственное, что необходимо сделать для включения Turbolinks - это добавить его в свой Gemfile, и поместить //= require turbolinks в свой манифест JavaScript, обычно это app/assets/javascripts/application.js.

Если хотите отключить Turbolinks для определенных ссылок, добавьте атрибут data-turbolinks="false" к тегу:

<a href="..." data-turbolinks="false">No turbolinks here</a>.

6.2. События изменения страницы

При написании CoffeeScript, часто необходимо что-то сделать при загрузке страницы. С помощью jQuery вы писали что-то вроде этого:

$(document).ready ->
  alert "page has loaded!"

Однако, поскольку Turbolinks переопределяет обычный процесс загрузки страницы, событие, на которое полагается вышеуказанный код, не произойдет. Если у вас есть подобный код, следует его изменить на следующий:

$(document).on "turbolinks:load", ->
  alert "page has loaded!"

Подробности, включая другие возможные события, можно посмотреть в Turbolinks README.

7. Другие ресурсы

Вот несколько полезных ссылок, которые позволят вам узнать больше: