Ajax in Rails - Genecode/bbq GitHub Wiki

М. 9 Различные формы Ajax в Rails

Теория В прошлый раз мы релизовывали Ajax при помощи встроенного механизма Rails. Сегодня посмотрим, как Ajax работает внутри Rails, как это связано с JS, и как в Rails реализовать другие виды Ajax-форм. Как помним, Ajax-формы бывают разного вида в зависимости от того, что возвращает сервер:

стандартный Ajax (возвращает асинх. JS + XML) асинх. JS + JS асинх. JS + HTML асинх. JS + JSON В прошлый раз мы использовали тип Ajax AJAJ - асинх. JS + JS (т.е. наш сервер возвращал исполняемый JS – кусок JS-кода, который тут же исполняется). Сегодня сделаем 2 других типа форм:

асинх. JS + HTML асинх. JS + JSON Но сначала разберемся, как изнутри работает взаимодействие Rails и JS.

Как работает Ajax и JS внутри Rails-приложения Чтобы в Rails работала схема Unobtrusive JavaScript (ненавязчивый JS), необходим гем jquery-rails. Кроме jQuery, он подключает дополнительные расширения - обработчики дополнительных событий в JS и jQuery, связывающие действия формы, которую рендерит Rails, с JS-кодом. Посмотрим, как в Rails формируется форма, работающая через Ajax (на примере нашей формы создания ответа к вопросу):

= form_for [@question, @answer], remote: true, data: { type: :json } do |f|
  p
    = f.label :body, 'Your answer'
    .answer-errors
    = f.text_area :body
  p
    = f.fields_for :attachments do |a|
      = a.label :file
      = a.file_field :file
p= f.submit 'Create'

Мы дали хелперу формы form_for опцию remote: true, после чего запросы пошли через Ajax. Почему?

Посмотрим в веб-инспекторе, из чего состоит эта форма (начало кода): <form accept-charset="UTF-8" action="/questions/1/answers" class="new_answer" data-remote="true" ...

Видим в начале тег формы form, в середине - атрибут data-remote="true". Как только мы даем хелперу формы опцию remote: true, в html-коде формы возникает тег data-remote="true". Можно подумать, что вроде нет правила, что при добавлении такого атрибута форма автоматически должна начать отправлять данные через JS (и это не работает так в других фреймворках или с отключенным rails jquery). А дело в том, что через remote: true подключается специальный обработчик, находящийся в другом геме - jquery-ujs. Гем jquery-ujs хранится как зависимость к гему rails jquery, в репозитории rails, он прост и хорошо документирован.

Как гем jquery-ujs заставляет формы с remote: true работать через Ajax и в JS В jquery-ujs и лежит весь код по взаимодействию rails-приложения с JS/Ajax. Разберем подробнее, что там происходит, глядя в исходник гема – rails.js.

В rails.js и находится весь js-код по взаимодействию (биндингу) html-форм (и других элементов, генерируемых рельсами) с JS и вызовом/обработкой Ajax-запросов. Полистаем этот файл. Со строки 25 начинается перечисление используемых data-селекторов:

  $.rails = rails = {
    // Link elements bound by jquery-ujs
linkClickSelector: 'a[data-confirm], a[data-method], a[data-remote]:not([disabled]), a[data-disable-with], a[data-disable]',

Нам интересны те из них, что содержат слова [data-remote], как у нас в форме: например select[data-remote], button[data-remote]. Почему появление data-remote="true" заставляет форму работать через Ajax? Потому, что в файле rails.js гема jquery-ujs есть селектор, который берет форму, и, если у нее есть атрибут data-remote, начинает отправлять ее асинхронно, через Ajax. Посмотрим, как это работает. В строке 35 есть formSubmitSelector с атрибутом form (пока без remote):

    // Form elements bound by jquery-ujs
formSubmitSelector: 'form',

remote выбирается дальше, в коде, который через условие вешает на формы обработчик, делающий запросы через Ajax. Ниже есть переменная remote, которая определяется, если атрибут remote определен у формы (форме прописано remote: true). Наличие у формы опции remote: true проверяется в условии, начинающемся со строки 498 if (remote). И ниже, в том же условии, в строке 512 rails.handleRemote(form);, на формы с remote: true крепится handleRemote (обработчик события).

Если форма имеет опцию remote: true, гем jquery-ujs автоматически крепит к ней обработчик события handleRemote.

Обработчик HandleRemote готовит данные (все опции) будущего ajax-запроса для отправки, затем выполняет ajax-запрос с ними. Он начинается в строке 93. Этот довольно большой и универсальный метод, т.к. надо учесть много вариантов (он работает для кнопок, ссылок, форм, и многого другого), и, в зависимости от того, POST это запрос или GET, по-разному формирует запрос из параметров.

Так же в его обязанности входит файлинг (вызов) других событий: когда мы выполняем в Rails ajax-запрос (любого вида), возникает несколько событий, на которые мы можем повесить обработчики для выполнения какого-то кода. Например, есть ajax before, ajax before send (перед отправкой формы или перед выполнением ajax-запроса), ajax success (после успешного выполнения ajax-запроса), ajax error (после неуспешного выполнения ajax-запроса) и т.д. - на каждое из этих событий мы можем повесить свой обработчик (как - посмотрим чуть позже).

Мы разобрались, почему добавление опции remote заставляет форму отправляться через Ajax, а не напрямую: потому что у нас есть специальный код, отслеживающий формы с наличием этой опции и вешающий на них обработчик события, который меняет стандартное выполнение запроса на ajax (отправку формы с html на ajax).

Но как Ajax-запрос работает с тем, что у нас в контроллере (format html, format js)? Посмотрим в веб-инспекторе (вкладка Network, фильтр XHR, который показывает только ajax-запросы ), как работает отправка нашей формы. Заполнив форму ответа и нажав кнопку, увидим:

POST
200
Type - text/javascript

Как Rails понимают, что надо отработать именно js.erb? Как раз по типу запроса (Type), а тип запроса передается в заголовках запроса - Request Headers:

Request Headers Accept: /:q=0.5, text/javascript, application/javascript, application/ecmascript, application/x-ecmascript Именно в заголовке Request Headers передаются типы ответов, которые может принять клиент. Здесь перечислены т.н. маймтайпы (MIME Types): text/javascript, application/javascript, application/ecmascript, application/x-ecmascript. Почему их несколько? Потому что сервер может ответить разными маймтайпами (типами ответа). Эти тайпы означают одно и то же - например, что будет отдан JS - но само значение может быть отдано сервером по-разному: как text/javascript, application/javascript, application/ecmascript, application/x-ecmascript, etc.

MIME stands for "Multipurpose Internet Mail Extensions. It's a way of identifying files on the Internet according to their nature and format. For example, using the "Content-type" header value defined in a HTTP response, the browser can open the file with the proper extension/plugin. MIME Types List The Complete List of MIME Types

В запросе перечисляются все возможные маймтайпы для JS чтобы не зависеть от того, с каким маймтайпом сервер его отдаст (быть в состоянии принять любой). Кстати, строки в accept у нас появляются тоже благодаря опции remote: true, можем даже посмотреть, где это происходит в исходнике rails-ujs: строки 147-149:

            if (settings.dataType === undefined) {
              xhr.setRequestHeader('accept', '*/*;q=0.5, ' + settings.accepts.script);
}

Если в настройках dataType не определен, то установка заголовков запроса содержится в settings.accepts.script, там же лежат и маймтайпы (text/javascript, application/javascript, application/ecmascript, application/x-ecmascript).

Вкладка Headers / означает любой ответ. / означает, что при указанном remote: true наш клиент не только просто запрашивает JS, а говорит: “я предпочитаю JS, но приму любой ответ.” / это как раз и есть любой ответ. Другой вопрос, что мы можем не уметь его обрабатывать. Но, тем не менее, не будет ошибки, если сервер вернет какой-то другой (не JS) ответ. / как раз и нужен, чтобы избежать ошибок при отдаче сервером других типов ответов - чтобы если сервер вернул другой ответ , мы могли это увидеть, отследить, среагировать.

Рядом со звездочками есть интересная вещь: q=0.5: Accept: /;q=0.5

принимать все ответы, вес не-JS ответов 0.5, вес JS-ответов 1, т.к. им предпочтение. Это так называемый вес, или приоритет. У указанных 4х типов вес по умолчанию 1 (т.к. JS - предпочтительный тип), а у любого другого типа ответа вес 0.5 (менее предпочтителен, но тоже принимается). Таким образом получается, что когда мы в контроллере - например, в create что-то прописываем, то наш create.js.erb файл будет отвечать на это потому, что у нас:

есть запрос с опцией remote: true есть эта вьюха - create.js.erb в соответствующей папке Если мы сейчас попробуем запросить html, то будет ошибка, что у нас нет такой вьюхи. Но если мы во вьюхе create.js.erb добавим respond_to для html, то немного изменится поведедение: в веб-инспекторе тип запроса (Type) будет text/javascript.

Кстати, мы можем управлять запрашиваемыми типами, указывая после remote: true какой тип данных мы запрашиваем: = form_for [@question, @answer], remote: true, data { type: :html } do |f| В этом случае у нас изменится запрос. В панели Network характеристики ответа останутся те же. Но если посмотрим в Headers на Accept, то тут он поменяется на text/html: Accept: text/html, /; q=0.01

предпочитаем text/html, но возьмем и любой другой ответ, только с гораздо меньшим приоритетом (весом): 0.01. То есть если в пришедших данных будет что-то похожее на html, то в первую очередь отработает именно этот ответ. Тут мы запрашиваем html, но возвращается нам все равно js, т.к. наш контроллер умеет отвечать только с помощью create.js.erb, и отрабатывает именно этот файл.. Если бы мы могли принимать только html (в Accept был бы только html), у нас была бы ошибка, что нет такого ответа. Но у нас в заголовке Accept прописана возможность принимать любой ответ (/), поэтому сервер отрабатывает тем способом, каким получается (js.erb).

Другой вариант – не прописывать ничего дополнительного у remote: true, и тогда предпочтительным ответом будет JS. Но прописать при этом в create контроллера ответов умение отвечать только на html (например, пустым ответом):

def create
  @question = Question.find(params[:question_id])
  @answer = @question.answers.create(answer_params)

  respond_to do |format|
    format.html { render nothing: true }
  end
end

В этом случае мы получим следующее. Тип ответа в запросе поменялся на text/html (несмотря на то, что мы запросили js):

Мы сделали запрос, ответ пришел, но типа html, хотя мы запросили text/javascript (POST, 200, text/html). Hесмотря на то, что в Accept мы запросили text/js и другие варианты js-ответа:

Accept: /;q=0.5, text/javascript, application/javascript, application/ecmascript, application/-ecmascript Записи в контроллере относительно формата приоритетнее, чем то, какие есть вьюхи

Это потому, что когда мы пишем в контроллере что-то насчёт формата, это имеет приоритет над тем, какие вьюхи у нас есть. Если прописываем контроллеру какой-то рендер, то он приоритетен и все пойдет так, как прописано в контроллере. А если в контроллере ничего не пропишем, будет отвечать та вьюха, которая есть для данного экшена (create.js.erb). Пропишем в контроллере, на какие данные должен отвечать сервер:

def create
  @question = Question.find(params[:question_id])
  @answer = @question.answers.create(answer_params)

  respond_to do |format|
    format.html { render nothing: true }
    format.js
  end
end

Что у нас должно рендериться при таком контроллере?

Для html (если запрошен html) - ничего Для js - т.к. мы не указали что рендерить, будет рендериться create.js Для json - пока ничего, его добавим позже В консоли увидим POST, 200, text/javascript, Accept /;q=0.5 - т.к. мы в контроллере запросили js (format.js), отработал js. Таким образом мы и управляем типом данных, которые нам нужны.

Весь наш контроллер:

class AnswersController < ApplicationController
  def create
    @question = Question.find(params[:question_id])
    @answer = @question.answers.build(answer_params)

    respond_to do |format|
      if @answer.save
        format.html { render partial: 'questions/answers', layout: false }
        format.json { render json: @answer }
      else
        format.html { render text: @answer.errors.full_messages.join("\n"), status: :unprocessable_entity }
        format.json { render json: @answer.errors.full_messages, status: :unprocessable_entity }
      end
    end
  end

  def update
    @answer = Answer.find(params[:id])
    @answer.update(answer_params)
    @question = @answer.question
  end

  private

  def answer_params
    params.require(:answer).permit(:body, attachments_attributes: [:file])
  end
end

Реализуем тип запроса “асинхронный js и html” Теперь реализуем тип запроса “асинхронный js и html”: пусть сервер возвращает html. Есть 2 варианта:

указать явно html у remote: true в questions/show.html.slim: = form_for [@question, @answer], remote: true, data: { type: :html } do |f| убрать из контроллера строку про js, оставив только html, и тогда сервер будет отвечать только на html (убираем format.js):

#answers_controller.rb:
respond_to do |format|
  format.html { render nothing: true }
end

Можно сделать и то, и другое. Тогда форма будет запрашивать html и получать ответ от сервера в html. При этом нам надо повторить функционал файла create.js.erb - например, создав вьюху create.html.slim. Что делал файл create.js.erb? Рендерил в answers паршл questions/answers. Мы не можем выполнить такой же код в контроллере (отдать в нем тот же js через html - в этом случае js будет экранирован и не будет выполняться - отдастся как строки, а не как исполняемый js). Поэтому отрендерим тут не ничего, а паршл:

#answers_controller.rb:
respond_to do |format|
  format.html { render partail: 'question/answers', layout: false }
end

Важный момент: надо дописать layout: false, чтобы вернулся html только из этого паршла, а все футеры, хедеры, меню и другие паршлы не рендерелись.

Обработчики события, естественно, вешаются не глобальные, а для какого-то конкретного ajax-запроса от какого-то конкретного элемента. В нашем случае нам надо повесить обработчик события на форму. Посмотрим инспектором, как нызвается эта форма: class="new_answer" data-remote="true". Чтобы повесить обработчик не на любой элемент с классом new_answer, а именно на эту форму, укажем, что это элемент form с классом new_answer: $(‘form.new_answer’).

Теперь нам надо повесить на нее обработчики событий. В первую очередь, успешного выполнения запроса. Таким событием является ajax: success. Для прикрепления в элементу события можно использовать функцию bind, которая принимает 2 параметра: название события, на которое вешаем обработчик и функция, которая будет выполняться при выполнении этого события. e - объект события Data в котором могут храниться какие-то данные (но у нас они будут храниться не в нем, а в xhr) Status Xhr - ответ от сервера, в котором будут храниться данные Проверим в браузере, что получилось: запостим ответ через форму: POST, 200, text/html. Запрос был успешным, вернулся html - в ответе пришли данные (html). Мы этот код можем посмотреть в соседней вкладке Preview, там будет список ответов.

В этом списке добавился наш новый ответ, но в браузере он не появился в списке ответов при нажатии кнопки create. Если обновить страницу, новый ответ появляется, а автоматически при создании – нет. Это потому, что нет обработки этих данных. Данные-то пришли, но нет кода, который бы с ними что-то делал (добавлял, изменял и т.д.) Поэтому клиент ничего с ними и не делает. Когда был create.js.erb, в нем хранился исполняемый код, который обрабатывал данные (рендерил паршл в .answers). Сейчас этот файл не отрабатывает, а другого исполняемого кода у нас нет. Теперь данные только приходят, и все, и в html данные нет смысла слать, т.к. они не выполняются как js. Что делать? Надо добавить в js обработчики события на исполнения этих ajax-запросов:

#answers.js.coffee $('form.new_answer').bind 'ajax:success', (e, data, status, xhr) → Дальше надо сделать то же, что у нас было в create.js.erb, т.е. вызвать функцию .html для класса .answers. Ответ у нас уже приходит, осталось поместить его в .answers. Чтобы получить тело этого приходящего нам ответа из xhr, в котором приходят данные, используем ResponseText. Получается:

#answers.js.coffee $('form.new_answer').bind 'ajax:success', (e, data, status, xhr) → $('.answers').html(xhr.responseText)

Проверим в браузере, создав новый ответ. Теперь автоматическая отправка ответа в список работает.

Разберемся с выводом ошибок. Сейчас они не выводятся. Если отправить пустой ответ, будет статус 200, а в превью - список ответов.

Но нам надо сообщить юзеру, что ответ не может быть пустым. Для этого доработаем answers_controller, в котором мы еще не делали проверки, позволяющей по-разному отвечать в случае успеха и неуспеха при создании ответа. Во-первых, сделаем build, чтобы ответ не сохранялся сразу и мы могли делать save отдельно и написать условие, чтобы по-разному реагировать на то, что он вернет и выдать разные данные.

def create
  @question = Question.find(params[:question_id])
  @answer = @question.answers.build(answer_params)
И в блоке respond_to добавим условие с save.

В случае возврата html мы должны вывести ошибки. В файле .js мы перебирали ошибки и выводили их, тут мы не можем так сделать т.к. из контроллера не можем вернуть никакого исполняемого js. Поэтому тут поступим по-другому: вернем текст, представляющий из себя список ошибок, разделенных символом новой строки. Вот наш массив ошибок, как нам его разделить новой строкой? format.html { render text: @answer.errors.full.messages }

Для этого используем метод .join, который превращает элементы массива в строку и разделяет их элементом, который мы укажем (укажем "\n" – символ новой строки - обязательно в двойных кавычках, потому что такие эскейп-последовательности работают только в них): format.html { render text: @answer.errors.full.messages.join("\n") } Так у нас будет возвращаться список всех сообщений об ошибке, разделенный символами новой строки. Важный момент: если мы теперь нажмем create с пустым ответом, сообщение об ошибке появится, но при этом пропадут все ответы. Почему? Несмотря на то, что возвращается сообщение об ошибке (его видно в превью - ‘body can’t be blank’), статус у нас 200 (успех). Надо сделать так, чтобы при ошибках отрабатывал не ajax:success, а другой обработчик, т.е. написать условие в контроллере.

Ответы исчезли, т.к. у нас есть только обработчик на успех (ajax:success), а на случай ошибки нет ни обработчика, ни условия. Сейчас наш код только заменяет .answers html-ем из паршла ответов в случае success. Добавим обработчик на случай ошибки. Это можно сделать, добавив код статуса с ошибкой:

#answer_controller.rb status: :unprocessable_entity Это код 422 – если у вас ошибки валидации, он подходит лучше всего: Теперь у нас при создании нового ответа будет этот статус 422 (POST, 422, text/html), а во вкладке Response – ошибка 'Body can't be blank' (но на странице эта ошибка не появляется, т.к. обработчик есть только на успех, а тут статус 422). Добавим обработчик 2 - на ошибки: тут же, в answers.js.coffee, добавим второй bind (на том же уровне, что $ 1-го бинда – так в coffee вешается 2-й обработчик события на тот же элемент (можем так несколько вешать). Аргументы второго бинда – событие ajax:error и те же параметры, только в другой последовательности: xhr – ответ, status – код ошибки, error – текст ошибки (unprocessable entity). И так же, как мы делали в create.js, в .answer-erros добавляем ошибки. В create.js.erb у нас было: $('.answer-errors').html('<%= j message %>') А в coffee будет: $('.answer-errors').html(xhr.responseText) Итого (coffee):

$('form.new_answer').bind 'ajax:success', (e, data, status, xhr) → $('.answers').html(xhr, responseText) .bind 'ajax:error', (e, xhr, status, error) $('.answer-errors').html(xhr.responseText)

Проверяем в браузере: теперь ошибки выводятся, статус 422, и все работает, как надо – и при валидном, и при невалидном ответе. Правда, нет очистки поля. Но такое решение, как мы сейчас сделали – запрос и обработка html-я встречается довольно редко. Иногда оно бывает удобно (получить кусок html и обработать его в js), но в данном случае у нас от такого решения больше минусов, чем плюсов. В случае с create.js.erb в нем лежал весь исполняемый код, это было удобно.

Возврат JSON Гораздо чаще используется вариант, когда возвращается JSON (AJAJSON). Сделав вариант с html, мы провели подготовку по обработке json-а. Схема взаимодействия та же, и json обрабатывается в обработчиках, похожих на тот, что мы только что сделали в coffee. В show.html.slim запросим данные вместо html через JSON: = form_for [@question, @answer], remote: true, data: { type: :json } do |f| Теперь нам надо указать, что мы вообще json понимаем. Это можно сделать по-разному (например, добавить в контроллере format.json). Но тут есть отличие: указывая это, мы должны рендерить не паршлы, не html, не js, а данные. И в этом случае мы должны вернуть не список всех ответов, а только вновь созданный объект (это логично: мы в экшене create мы создаем один-единственный объект @answer, и мы должны его вернуть). Это аналогично тому, как если бы у нас не было ajax – в этом случае мы бы сделали редирект на только что созданный объект. Но тут у нас редиректы работать не будут, и мы вместо редиректа должны сделать возврат данных этого объекта в json-формате, чтобы потом их дальше обработать. Делаем это так: чтобы вернуть json, пишем render json: @answer (передаем объект нового ответа - @answer): format.json { render json: @answer } Rails автоматом вызовут метод to_json, который преобразует объект @answer в строку в формате json. Для второго условия, где возврат ошибок, тоже добавляем рендер json-а – т.к. это json, мы можем возвращать не объединенные строки, а массив ошибок full_messages: format.json { render json: @answer.errors.full_messages, status: :unprocessable_entity }

Проверяем ошибки в браузере: POST, 422, application/json, но ошибки вывелись как массив, над полем формы появилось: ["Body can't be blank"].

А если создаем новый ответ, то html заменяется json-ом нашего объекта (т.е. это наш объект в формате json). Работает не совсем правильно. Это потому, что в coffee мы берем чистый ответ от сервера и никак его не обрабатываем (сервер возвращает json, мы его и выводим просто как текст): $('.answer-errors').html(xhr.responseText).

Чтобы правильно обработать этот json, надо его сначала распарсить, и потом как-то применить. Создадим объект answer из того объекта, который возвращается сервером в случае успеха. Делаем это через функцию parse.JSON, которая предоставляется jQuery: в ней берем тело ответа (мы знаем, что это json, и его распарсиваем, и после этого у нас в answer есть js-объект, к которому мы можем обращаться). Ближайший аналог такого объекта - хэш руби.

$('form.new_answer').bind 'ajax:success', (e, data, status, xhr) -> answer = $.parseJSON(xhr.responseText) Но т.к. в отличие от вариантов, когда мы возвращали js и html, тут возвращается не список вопросов, а только 1 ответ, и мы должны не просто его заменить а добавить к списку ответов с помощью append, и добавляем мы не просто answer, а answer.body:

$('form.new_answer').bind 'ajax:success', (e, data, status, xhr) -> answer = $.parseJSON(xhr.responseText) $('.answers').append('' + answer.body + '') Мы можем написать answer.body т.к. в json-е распарсенного объекта у нас есть в том числе body. Проверяем в браузере - все работает, ответ добавляется. Теперь разберемся с выводом сообщений об ошибках, которые у нас показывались как массив. С массивом ошибок надо поступить точно так же: сначала распарсить, а потом итеративно вывести. Распарсиваем ответ: errors = $parseJSON(xhr.responseText) Перебираем все элементы массива с помощью итератора jQuery .each:

Итерируем массив errors, параметры – index, value (index нам не нужен, но он должен присутствовать), и апендаем value:

errors = $.parseJSON(xhr.responseText)
$.each errors, (index, value) ->
  $('.answer-errors').append(value)
Итого:

$ ->
  $('.edit-answer-link').click (e) ->
    e.preventDefault();
    $(this).hide();
    answer_id = $(this).data('answerId')
    $('form#edit-answer-' + answer_id).show()

  $('form.new_answer').bind 'ajax:success', (e, data, status, xhr) ->
    answer = $.parseJSON(xhr.responseText)
    $('.answers').append('<p>' + answer.body + '</p>')
  .bind 'ajax:error', (e, xhr, status, error) ->
    errors = $.parseJSON(xhr.responseText)
    $.each errors, (index, value) ->
$('.answer-errors').append(value)

Все, теперь ошибки выводятся как надо.

Послесловие: При работе через js и создание файлов js.erb нам не приходится писать обработчики событий, как мы это делали сейчас для работы с json. И тут мы просто добавляем тело ответа к списку ответов: $('.answers').append('' + answer.body + '')

Но у нас были еще и формы для создания и редактирования ответов, и чтобы повторить это поведение, нам понадобится добавить не просто тег

, но и другие теги форм. Либо сделать так, чтобы не выводились сразу все формы, а выводились динамически при нажатии на кнопку edit, добавляясь к нужному элементу.

Т.е при работе через json могут возникать какие-то сложности и решения. Получается, что рельсовый способ из коробки – самый простой, легкий, и он соблюдает структуру распределения вьюх, контроллеров и т.д. так как она принята в Rails , поэтому он довольно популярный.

C точки зрения взаимодействия сервера и клиента, с точки зрения ajax-запросов конечно лучше возвращать json. Но при работе с json надо быть готовым к тому, что придется выносить обработчики событий в js-файлы и, возможно, придется больше писать и это будет менее удобно. В любом случае, надо уметь работать через json (html не так часто возвращают через ajax, поэтому он менее актуален).

И такой момент - когда работаете через json, и js-кода становится довольно много, то js-код по обработке (код обработчиков), если он не разбит по файлам, быстро становится неудобоваримым – все в каше в нескольких файлах, легко запутаться. Когда код обработки ajax-ответов начал становится сложным, грязным и запутанным, это стало одной из причин появления современных js-фреймворков – желание структурировать сложный js-код по обработке ajax. Они полезны, когда на сайте много ajax-запросов. Но не на всех сайтах много ajax и не везде они нужны. Так что в большинстве случаев вам хватит работы с возвратом js или json. Но если у вас например отзывчивый интерфейс, который достигается за счет множественных ajax-запросов, вам помогут js-MVC-фреймворки (react, angular, etc.)

А суть того, как работают с этим Rails, вам уже понятна: они запрашивают json, вы им отвечаете. Плюс позже мы пройдем написание REST API, и когда на клиенте у нас ипользуются MVC-js-фреймворки, Rails используются просто как сервер API, то есть там нет стандартных видов, идет возврат json, а все что касается обработки этого json ложится на фреймворк.