Макеты и рендеринг в Rails

Это руководство раскрывает основные возможности макетов Action Controller и Action View.

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

  • Как использовать различные методы рендеринга, встроенные в Rails.
  • Как создавать макеты с несколькими разделами содержимого.
  • Как использовать частичные шаблоны для соблюдения принципа DRY в ваших вью.
  • Как использовать вложенные макеты (подшаблоны).

1. Обзор: как кусочки складываются вместе

Это руководство сосредотачивается на взаимодействии между контроллером и вью (представлением) в треугольнике модель-представление-контроллер (MVC). Как вы знаете, контроллер ответственен за управление целым процессом обслуживания запросов в Rails, хотя обычно любой серьезный код переносится в модель. Но когда приходит время послать отклик обратно пользователю, контроллер передает все вью. Именно этой передаче посвящено данное руководство.

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

2. Создание откликов

С точки зрения контроллера есть три способа создать отклик HTTP:

  • Вызвать render для создания полного отклика, возвращаемого браузеру
  • Вызвать redirect_to для передачи браузеру кода переадресации HTTP
  • Вызвать head для создания отклика, включающего только заголовки HTTP, возвращаемого браузеру

2.1. Рендеринг по умолчанию: соглашения по конфигурации в действии

Вы уже слышали, что Rails содействует принципу "соглашения по конфигурации". Рендеринг по умолчанию - прекрасный пример этого. По умолчанию контроллеры в Rails автоматически рендерят вью с именами, соответствующими валидным маршрутам. Например, если есть такой код в вашем классе BooksController:

class BooksController < ApplicationController
end

И следующее в файле маршрутов:

resources :books

И у вас имеется файл вью app/views/books/index.html.erb:

<h1>Books are coming soon!</h1>

Rails автоматически отрендерит app/views/books/index.html.erb при переходе на адрес /books, и вы увидите на экране надпись "Books are coming soon!"

Однако это сообщение минимально полезно, поэтому вскоре вы создадите модель Book и добавите экшн index в BooksController:

class BooksController < ApplicationController
  def index
    @books = Book.all
  end
end

Снова отметьте, что у нас соглашения превыше конфигурации в том, что отсутствует избыточный рендер в конце этого экшна index. Правило в том, что не нужно что-то избыточно рендерить в конце экшна контроллера, rails будет искать шаблон action_name.html.erb по пути вью контроллера и отрендерит его, поэтому в нашем случае Rails отрендерит файл app/views/books/index.html.erb.

Итак, в нашей вью мы хотим отобразить свойства всех книг, это делается с помощью шаблона ERB, подобного следующему:

<h1>Listing Books</h1>

<table>
  <thead>
    <tr>
      <th>Title</th>
      <th>Content</th>
      <th colspan="3"></th>
    </tr>
  </thead>

  <tbody>
    <% @books.each do |book| %>
      <tr>
        <td><%= book.title %></td>
        <td><%= book.content %></td>
        <td><%= link_to "Show", book %></td>
        <td><%= link_to "Edit", edit_book_path(book) %></td>
        <td><%= link_to "Destroy", book, data: { turbo_method: :delete, turbo_confirm: "Are you sure?" } %></td>
      </tr>
    <% end %>
  </tbody>
</table>

<br>

<%= link_to "New book", new_book_path %>

Фактически рендеринг осуществляется вложенными классами модуля ActionView::Template::Handlers. Мы не будем углубляться в этот процесс, но важно знать, что расширение файла вью контролирует выбор обработчика шаблона.

2.2. Использование render

Во многих случаях метод ActionController::Renderer#render выполняет большую работу по рендерингу содержимого Вашего приложения для использования в браузере. Имеются различные способы настройки возможностей render. Вы можете рендерить вью по умолчанию для шаблона Rails, или определенный шаблон, или файл, или встроенный код, или совсем ничего. Можно рендерить текст, JSON или XML. Также можно определить тип содержимого или статус HTTP отрендеренного отклика.

Если хотите увидеть точные результаты вызова render без необходимости проверять это в браузере, можете вызвать render_to_string. Этот метод принимает те же самые опции, что и render, но возвращает строку вместо отправки отклика обратно браузеру.

2.2.1. Рендеринг вью экшна

Если хотите отрендерить вью, соответствующую другому шаблону этого же контроллера, можно использовать render с именем вью:

def update
  @book = Book.find(params[:id])
  if @book.update(book_params)
    redirect_to(@book)
  else
    render "edit"
  end
end

Если вызов update проваливается, вызов экшна update в этом контроллере отрендерит шаблон edit.html.erb, принадлежащий тому же контроллеру.

Если хотите, можете использовать символ вместо строки для определения экшна для рендеринга:

def update
  @book = Book.find(params[:id])
  if @book.update(book_params)
    redirect_to(@book)
  else
    render :edit, status: :unprocessable_entity
  end
end
2.2.2. Рендеринг шаблона экшна из другого контроллера

Что, если вы хотите отрендерить шаблон из абсолютно другого контроллера? Это можно также сделать с render, который принимает полный путь шаблона для рендеринга (относительно app/views). Например, если запускаем код в AdminProductsController который находится в app/controllers/admin, можете отрендерить результат экшна в шаблон в app/views/products следующим образом:

render "products/show"

Rails знает, что эта вью принадлежит другому контроллеру, поскольку содержит символ слэша в строке. Если хотите быть точными, можете использовать опцию :template (которая требовалась в Rails 2.2 и более ранних):

render template: "products/show"
2.2.3. Оборачивание

Вышеописанные два метода рендеринга (рендеринг шаблона другого экшна в контроллере и рендеринг шаблона другого экшна в другом контроллере) на самом деле являются вариантами одной и той же операции.

Фактически в классе BooksController, в экшне update, в котором мы хотим отрендерить шаблон edit, если книга не была успешно обновлена, все нижеследующие вызовы отрендерят шаблон edit.html.erb в директории views/books:

render :edit
render action: :edit
render "edit"
render action: "edit"
render "books/edit"
render template: "books/edit"

Какой из них вы будете использовать - это вопрос стиля и соглашений, но практическое правило заключается в использовании простейшего, который больше подходит по стилю написания вашего кода.

2.2.4. Использование render с :inline

Метод render вполне может обойтись без вью, если вы используете опцию :inline для поддержки ERB, как части вызова метода. Это вполне валидно:

render inline: "<% products.each do |p| %><p><%= p.name %></p><% end %>"

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

По умолчанию встроенный рендеринг использует ERB. Можете принудить использовать вместо этого Builder с помощью опции :type:

render inline: "xml.p {'Horrid coding practice!'}", type: :builder
2.2.5. Рендеринг текста

Вы можете послать простой текст - совсем без разметки - обратно браузеру с использованием опции :plain в render:

render plain: "OK"

Рендеринг чистого текста наиболее полезен, когда вы делаете Ajax-отклик или отвечаете на запросы веб-сервиса, ожидающего что-то иное, чем HTML.

По умолчанию при использовании опции :plain текст рендерится без использования текущего макета. Если хотите, чтобы Rails вложил текст в текущий макет, необходимо добавить опцию layout: true и использовать расширение .text.erb для файла макета.

2.2.6. Рендеринг HTML

Вы можете вернуть HTML, используя опцию :html метода render:

render html: helpers.tag.strong('Not Found')

Это полезно когда вы хотите отрендерить небольшой кусочек HTML-кода. Однако, если у вас достаточно сложная разметка, стоит рассмотреть выделение её в файл шаблона.

Когда используется опция html:, HTML объекты будут экранироваться, если строка не состоит из API, поддерживающих html_safe.

2.2.7. Рендеринг JSON

JSON - это формат данных JavaScript, используемый многими библиотеками Ajax. Rails имеет встроенную поддержку для преобразования объектов в JSON и рендеринга этого JSON обратно браузеру:

render json: @product

Не нужно вызывать to_json в объекте, который хотите рендерить. Если используется опция :json, render автоматически вызовет to_json за вас.

2.2.8. Рендеринг XML

Rails также имеет встроенную поддержку для преобразования объектов в XML и рендеринга этого XML обратно вызывающему:

render xml: @product

Не нужно вызывать to_xml в объекте, который хотите рендерить. Если используется опция :xml, render автоматически вызовет to_xml за вас.

2.2.9. Рендеринг внешнего JavaScript

Rails может рендерить чистый JavaScript:

render js: "alert('Hello Rails');"

Это пошлет указанную строку в браузер с типом MIME text/javascript.

2.2.10. Рендеринг необработанного содержимого

Вы можете вернуть необработанный текст, без установки типа содержимого, используя опцию :body, метода render:

render body: "raw"

Эта опция должна использоваться, только если не важен тип содержимого отклика. Использование :plain или :html уместнее в большинстве случаев.

Возвращенным откликом от этой опции будет text/plain (если не будет переопределен), так как это тип содержимого по умолчанию у отклика Action Dispatch.

2.2.11. Рендеринг необработанного файла

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

render file: "#{Rails.root}/public/404.html", layout: false

Это отрендерит необработанный файл (ERB или другие обработчики не поддерживаются). По умолчанию файл рендерится с использованием текущего макета.

Использование опции :file в комбинации с данными, введенными пользователем, может привести к проблемам безопасности, так как злоумышленник может использовать этот экшн для доступа к чувствительным, с точки зрения безопасности, файлам вашей файловой системы.

send_file часто является более быстрым и лучшим вариантом, если макет не требуется.

2.2.12. Рендеринг объектов

Rails может рендерить объекты, отвечающие на :render_in.

render MyRenderable.new

Это вызывает render_in на представленном объекте в контексте текущей вью.

2.2.13. Опции для render

Вызов метода render как правило принимает шесть опций:

  • :content_type
  • :layout
  • :location
  • :status
  • :formats
  • :variants
2.2.13.1. Опция :content_type

По умолчанию Rails будет обрабатывать результаты операции рендеринга с типом содержимого MIME text/html (или application/json, если используется опция :json, или application/xml для опции :xml). Иногда бывает так, что нужно изменить это, и тогда необходимо настроить опцию :content_type:

render template: "feed", content_type: "application/rss"
2.2.13.2. Опция :layout

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

Опция :layout нужна, чтобы сообщить Rails о необходимости использовать определенный файл как макет для текущего экшна:

render layout: "special_layout"

Также можно сообщить Rails, что требуется рендерить вообще без макета:

render layout: false
2.2.13.3. Опция :location

Опцию :location можно использовать, чтобы установить заголовок HTTP Location:

render xml: photo, location: photo_url(photo)
2.2.13.4. Опция :status

Rails автоматически сгенерирует отклик с правильным кодом статуса HTML (в большинстве случаев равный 200 OK). Опцию :status можно использовать, чтобы изменить это:

render status: 500
render status: :forbidden

Rails понимает как числовые коды статуса, так и соответствующие символы, показанные ниже.

Класс отклика Код статуса HTTP Символ
Informational 100 :continue
101 :switching_protocols
102 :processing
Success 200 :ok
201 :created
202 :accepted
203 :non_authoritative_information
204 :no_content
205 :reset_content
206 :partial_content
207 :multi_status
208 :already_reported
226 :im_used
Redirection 300 :multiple_choices
301 :moved_permanently
302 :found
303 :see_other
304 :not_modified
305 :use_proxy
307 :temporary_redirect
308 :permanent_redirect
Client Error 400 :bad_request
401 :unauthorized
402 :payment_required
403 :forbidden
404 :not_found
405 :method_not_allowed
406 :not_acceptable
407 :proxy_authentication_required
408 :request_timeout
409 :conflict
410 :gone
411 :length_required
412 :precondition_failed
413 :payload_too_large
414 :uri_too_long
415 :unsupported_media_type
416 :range_not_satisfiable
417 :expectation_failed
421 :misdirected_request
422 :unprocessable_entity
423 :locked
424 :failed_dependency
426 :upgrade_required
428 :precondition_required
429 :too_many_requests
431 :request_header_fields_too_large
451 :unavailable_for_legal_reasons
Server Error 500 :internal_server_error
501 :not_implemented
502 :bad_gateway
503 :service_unavailable
504 :gateway_timeout
505 :http_version_not_supported
506 :variant_also_negotiates
507 :insufficient_storage
508 :loop_detected
510 :not_extended
511 :network_authentication_required

Если попытаться отрендерить содержимое наряду с кодом статуса без содержимого (100-199, 204, 205 или 304), он будет исключён из отклика.

2.2.13.5. Опция :formats

Rails использует формат, определённый в запросе (или :html по умолчанию). Вы можете изменить его, передав в опцию :formats символ или массив:

render formats: :xml
render formats: [:json, :xml]

Если шаблон с указанным форматом не существует, вызывается ошибка ActionView::MissingTemplate.

2.2.13.6. Опция :variants

Она сообщает Rails искать варианты шаблона того же формата. Можно указать список вариантов, передав опции :variants символ или массив.

Пример использования.

# called in HomeController#index
render variants: [:mobile, :desktop]

С таком набором вариантов, Rails будет искать следующий набор шаблонов и использовать первый из существующих.

  • app/views/home/index.html+mobile.erb
  • app/views/home/index.html+desktop.erb
  • app/views/home/index.html.erb

Если шаблон с указанным форматом не существует, будет вызвана ошибка ActionView::MissingTemplate.

Вместо указания варианта на вызове render, его также можно установить на объекте request в экшне контроллера.

def index
  request.variant = determine_variant
end

private

def determine_variant
  variant = nil
  # некоторый код для определения варианта(ов) для использования
  variant = :mobile if session[:use_mobile]

  variant
end
2.2.13.7. Поиск макетов

Чтобы найти текущий макет, Rails сначала смотрит файл в app/views/layouts с именем, таким же, как имя контроллера. Например, рендеринг экшнов из класса PhotosController будет использовать /app/views/layouts/photos.html.erb (или app/views/layouts/photos.builder). Если такого макета нет, Rails будет использовать /app/views/layouts/application.html.erb или /app/views/layouts/application.builder. Если макет .erb отсутствует, Rails будет использовать макет .builder, если таковой имеется. Rails также предоставляет несколько способов для более точного назначения определенных макетов отдельным контроллерам и экшнам.

2.2.13.8. Определение макетов для контроллеров

Вы можете переопределить дефолтные соглашения по макетам в контроллере, используя объявление layout. Например:

class ProductsController < ApplicationController
  layout "inventory"
  #...
end

С этим объявлением все вью, отрендеренные ProductsController, будут использовать app/views/layouts/inventory.html.erb как макет.

Чтобы привязать определенный макет к приложению в целом, используйте объявление layout в классе ApplicationController:

class ApplicationController < ActionController::Base
  layout "main"
  #...
end

С этим объявлением каждая из вью во всем приложении будет использовать app/views/layouts/main.html.erb как макет.

2.2.13.9. Выбор макетов во время выполнения

Можно использовать символ для отсрочки выбора макета до тех пор, пока не будет обработан запрос:

class ProductsController < ApplicationController
  layout :products_layout

  def show
    @product = Product.find(params[:id])
  end

  private
    def products_layout
      @current_user.special? ? "special" : "products"
    end

end

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

Можно даже использовать встроенный метод, такой как Proc, для определения макета. Например, если передать объект Proc, то блоку, которому вы передаете Proc, будет предоставлен экземпляр controller, поэтому макет может быть определен, основываясь на текущем запросе:

class ProductsController < ApplicationController
  layout Proc.new { |controller| controller.request.xhr? ? "popup" : "application" }
end
2.2.13.10. Условные макеты

Макеты, определенные на уровне контроллера, поддерживают опции :only и :except. Эти опции принимают либо имя метода, либо массив имен методов, соответствующих именам методов в контроллере:

class ProductsController < ApplicationController
  layout "product", except: [:index, :rss]
end

С таким объявлением макет product будет использован везде, кроме методов rss и index.

2.2.13.11. Наследование макета

Объявление макета ниже по иерархии и более специфическое объявление макета всегда переопределяет более общие. Например:

  • application_controller.rb

    class ApplicationController < ActionController::Base
      layout "main"
    end
    
  • articles_controller.rb

    class ArticlesController < ApplicationController
    end
    
  • special_articles_controller.rb

    class SpecialArticlesController < PostsController
      layout "special"
    end
    
  • old_articles_controller.rb

    class OldArticlesController < SpecialPostsController
      layout false
    
      def show
        @article = Article.find(params[:id])
      end
    
      def index
        @old_articles = Article.older
        render layout: "old"
      end
      # ...
    end
    

В этом приложении:

  • В целом, вью будут рендериться в макет main
  • ArticlesController#index будет использовать макет main
  • SpecialArticlesController#index будет использовать макет special
  • OldArticlesController#show не будет использовать макет совсем
  • OldArticlesController#index будет использовать макет old
2.2.13.12. Наследование шаблона

Следуя логике наследования макета, если шаблон или партиал не найдены по обычному пути, контроллер будет искать шаблон или партиал для рендеринга по цепочке наследования. Например:

# app/controllers/application_controller
class ApplicationController < ActionController::Base
end
# app/controllers/admin_controller
class AdminController < ApplicationController
end
# app/controllers/admin/products_controller
class Admin::ProductsController < AdminController
  def index
  end
end

Порядок поиска экшна admin/products#index будет такой:

  • app/views/admin/products/
  • app/views/admin/
  • app/views/application/

Это делает app/views/application/ хорошим местом для общих партиалов, которые затем могут быть отрендерены в ERB следующим образом:

<%# app/views/admin/products/index.html.erb %>
<%= render @products || "empty_list" %>

<%# app/views/application/_empty_list.html.erb %>
There are no items in this list <em>yet</em>.
2.2.14. Избегание ошибок двойного рендеринга

Рано или поздно, большинство разработчиков на Rails увидят сообщение об ошибке "Can only render or redirect once per action". Хоть такое и раздражает, это относительно просто правится. Обычно такое происходит в связи с фундаментальным непониманием метода работы render.

Например, вот некоторый код, который вызовет эту ошибку:

def show
  @book = Book.find(params[:id])
  if @book.special?
    render action: "special_show"
  end
  render action: "regular_show"
end

Если @book.special? вычисляется как true, Rails начинает процесс рендеринга, выгружая переменную @book во вью special_show. Но это не остановит от выполнения остальной код в экшне show, и когда Rails достигнет конца экшна, он начнет рендерить вью show - и выдаст ошибку. Решение простое: убедитесь, что у вас есть только один вызов render или redirect за один проход. Еще может помочь такая вещь, как return. Вот исправленная версия метода:

def show
  @book = Book.find(params[:id])
  if @book.special?
    render action: "special_show"
    return
  end
  render action: "regular_show"
end

Отметьте, что неявный рендер, выполняемый ActionController, определяет, был ли вызван render поэтому следующий код будет работать без проблем:

def show
  @book = Book.find(params[:id])
  if @book.special?
    render action: "special_show"
  end
end

Это отрендерит книгу (book) с special?, заданным с помощью шаблона special_show, в то время как остальные книги будут рендериться с дефолтным шаблоном show.

2.3. Использование redirect_to

Другой способ управлять возвратом отклика на HTTP-запрос - с помощью redirect_to. Как вы видели, render сообщает Rails, какую вью (или иной ассет) использовать при построении отклика. Метод redirect_to делает нечто совершенно отличное: он говорит браузеру послать новый запрос по другому URL. Например, можно перенаправить из любого места, где сейчас выполняется код, к экшну index фотографий вашего приложения с помощью этого вызова:

redirect_to photos_url

Можно использовать redirect_back, чтобы вернуть пользователя на страницу с которой он только что пришел. Это место расположения вытаскивается из заголовка HTTP_REFERER, который не обязательно будет установлен браузером, поэтому нужно предоставить fallback_location для использования в таком случае.

redirect_back(fallback_location: root_path)

redirect_to и redirect_back не прерывают и не возвращают из выполняемого метода немедленно, а просто устанавливают отклики HTTP. Выражения, следующие после них в методе, будут выполнены. При необходимости можно прервать явным return или любым другим механизмом прерывания.

2.3.1. Получение различного кода статуса перенаправления

Rails использует код статуса HTTP 302, временное перенаправление, при вызове redirect_to. Если хотите использовать иной код статуса, возможно 301, постоянное перенаправление, можете использовать опцию :status:

redirect_to photos_path, status: 301

Подобно опции :status для render, :status для redirect_to принимает и числовые, и символьные обозначения заголовка.

2.3.2. Различие между render и redirect_to

Иногда неопытные разработчики думают о redirect_to как о разновидности команды goto, перемещающую выполнение из одного места в другое в вашем коде Rails. Это не верно. Ваш код останавливается и ждет нового запроса от браузера. Просто получается так, что вы говорите браузеру, какой запрос он должен сделать следующим, возвращая код статуса HTTP 302.

Рассмотрим эти экшны, чтобы увидеть разницу:

def index
  @books = Book.all
end

def show
  @book = Book.find_by(id: params[:id])
  if @book.nil?
    render action: "index"
  end
end

С кодом в такой форме, вероятно, будет проблема, если переменная @book равна nil. Помните, render :action не запускает какой-либо код в указанном экшне, и таким образом ничего не будет присвоено переменной @books, которую, возможно, потребует вью index. Один из способов исправить это - использовать перенаправление вместо рендеринга:

def index
  @books = Book.all
end

def show
  @book = Book.find_by(id: params[:id])
  if @book.nil?
    redirect_to action: :index
  end
end

С помощью этого кода браузер сделает новый запрос для индексной страницы, код в методе index запустится, и все будет хорошо.

Единственный недостаток этого кода в том, что он требует круговорот через браузер: браузер запрашивает экшн show с помощью /books/1, и контроллер обнаруживает, что книг нет, поэтому отсылает отклик-перенаправление 301 браузеру, сообщающий перейти на /books/, браузер выполняет и посылает новый запрос контроллеру, теперь запрашивая экшн index, затем контроллер получает все книги в базе данных и рендерит шаблон index, отсылает его обратно браузеру, который затем показывает его на экране.

Пока это небольшое приложение, такая добавленная задержка не может быть проблемой, но иногда стоит подумать о том, является ли время отклика проблемой. Можем продемонстрировать один из способов управления этим с помощью хитрого примера:

def index
  @books = Book.all
end

def show
  @book = Book.find_by(id: params[:id])
  if @book.nil?
    @books = Book.all
    flash.now[:alert] = "Your book was not found"
    render "index"
  end
end

Это обнаружит, что нет книг с определенным ID, заполнит переменную экземпляра @books всеми книгами в модели, и затем напрямую отрендерит шаблон index.html.erb, возвратив его браузеру с предупреждающим сообщением в flash, сообщающим пользователю, что произошло.

2.4. Использование head для создания отклика, содержащего только заголовок

Метод head может использоваться для отправки браузеру откликов, содержащих только заголовки. Метод head принимает число или символ (смотрите таблицу соответствия), представляющие код статуса HTTP. Аргумент опций интерпретируется как хэш заголовков имен и значений. Например, можно возвратить только заголовок ошибки:

head :bad_request

Это создаст следующий заголовок:

HTTP/1.1 400 Bad Request
Connection: close
Date: Sun, 24 Jan 2010 12:15:53 GMT
Transfer-Encoding: chunked
Content-Type: text/html; charset=utf-8
X-Runtime: 0.013483
Set-Cookie: _blog_session=...snip...; path=/; HttpOnly
Cache-Control: no-cache

Или можете использовать другие заголовки HTTP для передачи другой информации:

head :created, location: photo_path(@photo)

Что создаст:

HTTP/1.1 201 Created
Connection: close
Date: Sun, 24 Jan 2010 12:16:44 GMT
Transfer-Encoding: chunked
Location: /photos/1
Content-Type: text/html; charset=utf-8
X-Runtime: 0.083496
Set-Cookie: _blog_session=...snip...; path=/; HttpOnly
Cache-Control: no-cache

3. Структурирование макетов

Когда Rails рендерит вью как отклик, он делает это путем объединения вью с текущим макетом, используя правила для нахождения текущего макета, которые были рассмотрены ранее. В макетах у вас есть доступ к трём инструментам для объединения различных кусочков результата для формирования общего отклика:

  • Теги ассетов
  • yield и content_for
  • Партиалы

3.1. Хелперы ассетных тегов

Хелперы ассетных тегов предоставляют методы для генерации HTML, связывающие вью с лентами новостей, JavaScript, таблицами стилей, изображениями, видео и аудио. В Rails доступно шесть хелперов ассетных тегов:

Эти теги можно использовать в макетах или других вью, хотя auto_discovery_link_tag, javascript_include_tag и stylesheet_link_tag как правило используются в разделе <head> макета.

Хелперы ассетных тегов не проверяют существование ассетов по заданному месту расположения; они просто предполагают, что вы знаете, что делаете, и генерируют ссылку.

Хелпер auto_discovery_link_tag создает HTML-код, который большинство браузеров и агрегаторов новостей могут использовать для определения наличия каналов RSS, Atom или JSON лент. Он принимает тип ссылки (:rss, :atom или :json), хэш опций, которые передаются через url_for, и хэш опций для тега:

<%= auto_discovery_link_tag(:rss, {action: "feed"},
  {title: "RSS Feed"}) %>

Вот три опции тега, доступные для auto_discovery_link_tag:

  • :rel определяет значение rel в ссылке. Значение по умолчанию "alternate"
  • :type определяет явный тип MIME. Rails генерирует подходящий тип MIME автоматически
  • :title определяет заголовок ссылки. Значение по умолчанию это значение :type в верхнем регистре, например, "ATOM" или "RSS".
3.1.2. Присоединение файлов JavaScript с помощью javascript_include_tag

Хелпер javascript_include_tag возвращает HTML-тег script для каждого предоставленного источника.

При использовании Rails с включенным Asset Pipeline, этот хелпер сгенерирует ссылку на /assets/javascripts/, а не на public/javascripts, которая использовалась в более ранних версиях Rails. Затем эта ссылка обслуживается файлопроводом (asset pipeline).

Файл JavaScript в приложении Rails или Rails Engine размещается в одном из трех мест расположения: app/assets, lib/assets или vendor/assets. Эти места расположения детально описаны в разделе про организацию ресурсов в руководстве по Asset Pipeline.

Можно определить полный путь относительно корня документа или URL, по желанию. Например, сослаться на файл JavaScript, находящийся в директории с именем javascripts в одной из app/assets, lib/assets или vendor/assets, можно так:

<%= javascript_include_tag "main" %>

Rails тогда выдаст такой тег script:

<script src='/assets/main.js'></script>

Затем запрос к этому ассету будет обслужен гемом Sprockets.

Чтобы включить несколько файлов, таких как app/assets/javascripts/main.js и app/assets/javascripts/columns.js за один раз:

<%= javascript_include_tag "main", "columns" %>

Чтобы включить app/assets/javascripts/main.js и app/assets/javascripts/photos/columns.js:

<%= javascript_include_tag "main", "/photos/columns" %>

Чтобы включить http://example.com/main.js:

<%= javascript_include_tag "http://example.com/main.js" %>

Хелпер stylesheet_link_tag возвращает HTML-тег <link> для каждого предоставленного источника.

При использовании Rails с включенным "Asset Pipeline", этот хелпер сгенерирует ссылку на /assets/stylesheets/. Эта ссылка будет затем обработана гемом Sprockets. Файл таблицы стилей может быть размещен в одном из трех мест расположения: app/assets, lib/assets или vendor/assets.

Можно определить полный путь относительно корня документа или URL. Например, на файл таблицы стилей в директории stylesheets, размещенной в одной из app/assets, lib/assets или vendor/assets, можно сослаться так:

<%= stylesheet_link_tag "main" %>

Чтобы включить app/assets/stylesheets/main.css и app/assets/stylesheets/columns.css:

<%= stylesheet_link_tag "main", "columns" %>

Чтобы включить app/assets/stylesheets/main.css и app/assets/stylesheets/photos/columns.css:

<%= stylesheet_link_tag "main", "/photos/columns" %>

Чтобы включить http://example.com/main.css:

<%= stylesheet_link_tag "http://example.com/main.css" %>

По умолчанию stylesheet_link_tag создает ссылки с rel="stylesheet". Можно переопределить любое из этих дефолтных значений, указав соответствующую опцию (:rel):

<%= stylesheet_link_tag "main_print", media: "print" %>
3.1.4. Присоединение изображений с помощью image_tag

Хелпер image_tag создает HTML-тег <img /> для определенного файла. По умолчанию файлы загружаются из public/images.

Обратите внимание, что нужно указывать расширение изображения.

<%= image_tag "header.png" %>

Вы можете предоставить путь к изображению, если желаете:

<%= image_tag "icons/delete.gif" %>

Вы можете предоставить хэш дополнительных опций HTML:

<%= image_tag "icons/delete.gif", {height: 45} %>

Или альтернативный текст, если пользователь отключил показ изображений в браузере. Если вы не определили явно тег alt, по умолчанию будет указано имя файла с большой буквы и без расширения. Например, эти два тега изображения возвратят одинаковый код:

<%= image_tag "home.gif" %>
<%= image_tag "home.gif", alt: "Home" %>

Можете указать специальный тег size в формате "{width}x{height}":

<%= image_tag "home.gif", size: "50x20" %>

В дополнение к вышеописанным специальным тегам, можно предоставить итоговый хэш стандартных опций HTML, таких как :class или :id, или :name:

<%= image_tag "home.gif", alt: "Go Home",
                          id: "HomeImage",
                          class: "nav_bar" %>
3.1.5. Присоединение видео с помощью video_tag

Хелпер video_tag создает тег HTML5 <video> для определенного файла. По умолчанию файлы загружаются из public/videos.

<%= video_tag "movie.ogg" %>

Создаст

<video src="/videos/movie.ogg" />

Подобно image_tag, можно предоставить путь или абсолютный, или относительный к директории public/videos. Дополнительно можно определить опцию size: "#{width}x#{height}", как и в image_tag. Теги видео также могут иметь любые опции HTML, определенные в конце (id, class и др.).

Тег видео также поддерживает все HTML-опции <video> через хэш HTML-опций, включая:

  • poster: "image_name.png", предоставляет изображение, которое будет отображаться вместо видео прежде, чем оно начнет проигрываться.
  • autoplay: true, запускает проигрывание видео при загрузке страницы.
  • loop: true, запускает видео сначала, как только оно достигает конца.
  • controls: true, предоставляет пользователю поддерживаемую браузером панель управления для взаимодействия с видео.
  • autobuffer: true, файл видео предварительно загружается для пользователя при загрузке страницы.

Также можно определить несколько видео для проигрывания, передав массив видео в video_tag:

<%= video_tag ["trailer.ogg", "movie.ogg"] %>

Это создаст:

<video>
  <source src="/videos/trailer.ogg" />
  <source src="/videos/movie.ogg" />
</video>
3.1.6. Присоединение аудиофайлов с помощью audio_tag

Хелпер audio_tag создает тег HTML5 <audio> для определенного файла. По умолчанию файлы загружаются из public/audios.

<%= audio_tag "music.mp3" %>

Если хотите, можете предоставить путь к аудио файлу:

<%= audio_tag "music/first_song.mp3" %>

Также можно предоставить хэш дополнительных опций, таких как :id, :class и т.д.

Подобно video_tag, audio_tag имеет специальные опции:

  • autoplay: true, начинает воспроизведение аудио при загрузке страницы
  • controls: true, предоставляет пользователю поддерживаемую браузером панель управления для взаимодействия с аудио.
  • autobuffer: true, файл аудио предварительно загружается для пользователя при загрузке страницы.

3.2. Понимание yield

В контексте макета, yield определяет раздел, где должно быть вставлено содержимое из вью. Самый простой способ его использования - это иметь один yield там, куда вставится все содержимое вью, которая в настоящий момент рендерится:

<html>
  <head>
  </head>
  <body>
  <%= yield %>
  </body>
</html>

Также можете создать макет с несколькими разделами yield:

<html>
  <head>
  <%= yield :head %>
  </head>
  <body>
  <%= yield %>
  </body>
</html>

Основное тело вью всегда рендериться в неименованный yield. Чтобы рендерить содержимое в именованный yield, используйте метод content_for.

3.3. Использование метода content_for

Метод content_for позволяет вставлять содержимое в именованный блок yield в макете. Например, эта вью будет работать с макетом, который вы только что видели:

<% content_for :head do %>
  <title>A simple page</title>
<% end %>

<p>Hello, Rails!</p>

Результат рендеринга этой страницы в макет будет таким HTML:

<html>
  <head>
  <title>A simple page</title>
  </head>
  <body>
  <p>Hello, Rails!</p>
  </body>
</html>

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

3.4. Использование партиалов

Частичные шаблоны - также называемые "партиалы" - являются еще одним подходом к разделению процесса рендеринга на более управляемые кусочки. С партиалами можно перемещать код для рендеринга определенных частей отклика в свои отдельные файлы.

3.4.1. Именование партиалов

Чтобы отрендерить партиал как часть вью, используем метод render внутри вью:

<%= render "menu" %>

Это отрендерит файл, названный _menu.html.erb в этом месте в пределах рендерящейся вью. Обратите внимание на начальный символ подчеркивания: файлы партиалов начинаются со знака подчеркивания, чтобы отличать их от обычных вью, несмотря на то, что в вызове они указаны без подчеркивания. Это справедливо даже тогда, когда партиалы вызываются из другой папки:

<%= render "shared/menu" %>

Этот код вытянет партиал из app/views/shared/_menu.html.erb.

3.4.2. Использование партиалов для упрощения вью

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

<%= render "shared/ad_banner" %>

<h1>Products</h1>

<p>Here are a few of our fine products:</p>
...

<%= render "shared/footer" %>

Здесь партиалы _ad_banner.html.erb и _footer.html.erb могут содержать контент, общий для многих страниц приложения. Нет необходимости видеть код этих разделов, чтобы сконцентрироваться на определенной странице.

Как видно из предыдущих разделов данного руководства, yield является очень мощным инструментом для очистки ваших макетов. Имейте в виду, что это чистый Ruby, так что можно использовать его практически везде. Например, его можно использовать для соблюдения принципа DRY при определении макета формы для нескольких похожих ресурсов:

  • users/index.html.erb

    <%= render "shared/search_filters", search: @q do |form| %>
      <p>
        Name contains: <%= form.text_field :name_contains %>
      </p>
    <% end %>
    
  • roles/index.html.erb

    <%= render "shared/search_filters", search: @q do |form| %>
      <p>
        Title contains: <%= form.text_field :title_contains %>
      </p>
    <% end %>
    
  • shared/_search_filters.html.erb

    <%= form_for(search) do |form| %>
      <h1>Search form:</h1>
      <fieldset>
        <%= yield form %>
      </fieldset>
      <p>
        <%= form.submit "Search" %>
      </p>
    <% end %>
    

Для содержимого, общего для всех страниц приложения, можно использовать партиалы прямо в макетах.

3.4.3. Макеты партиала

Партиал может использовать свой собственный файл макета, подобно тому, как вью может использовать макет. Например, можете вызвать подобный партиал:

<%= render partial: "link_area", layout: "graybar" %>

Это найдет партиал с именем _link_area.html.erb и отрендерит его, используя макет _graybar.html.erb. Отметьте, что макеты для партиалов также начинаются с подчеркивания, как и обычные партиалы, и размещаются в той же папке с партиалами, которым они принадлежат (не в основной папке layouts).

Также отметьте, что явное указание partial необходимо, когда передаются дополнительные опции, такие как layout

3.4.4. Передача локальных переменных

В партиалы также можно передавать локальные переменные, что делает их более мощными и гибкими. Например, можете использовать такую технику для уменьшения дублирования между страницами new и edit, сохранив немного различающееся содержимое:

  • new.html.erb

    <h1>New zone</h1>
    <%= render partial: "form", locals: {zone: @zone} %>
    
  • edit.html.erb

    <h1>Editing zone</h1>
    <%= render partial: "form", locals: {zone: @zone} %>
    
  • _form.html.erb

    <%= form_for(zone) do |form| %>
      <p>
        <b>Zone name</b><br>
        <%= form.text_field :name %>
      </p>
      <p>
        <%= form.submit %>
      </p>
    <% end %>
    

Хотя тот же самый партиал будет отрендерен в обоих вью, Action View отправит хелпер, который возвратит "Create Zone" для экшна new и "Update Zone" для экшна edit.

Для передачи локальной переменной в партиал только в особых случаях, используйте local_assigns.

  • index.html.erb

    <%= render user.articles %>
    
  • show.html.erb

    <%= render article, full: true %>
    
  • _article.html.erb

    <h2><%= article.title %></h2>
    
    <% if local_assigns[:full] %>
      <%= simple_format article.body %>
    <% else %>
      <%= truncate article.body %>
    <% end %>
    

Таким образом, можно использовать партиал без необходимости объявления всех локальных переменных.

Каждый партиал также имеет локальную переменную с именем, как у партиала (без начального символа подчеркивания). Можете передать объект в эту локальную переменную через опцию :object:

<%= render partial: "customer", object: @new_customer %>

В партиале customer переменная customer будет указывать на @new_customer из родительской вью.

Если есть экземпляр модели для рендеринга в партиале, можно использовать сокращенный синтаксис:

<%= render @customer %>

Предположим, что переменная экземпляра @customer содержит экземпляр модели Customer. Эта переменная будет использовать _customer.html.erb для рендеринга модели и передаст локальную переменную customer в партиал, на который будет ссылаться на переменная экземпляра @customer в родительской вью.

3.4.5. Рендеринг коллекций

Партиалы часто используют для рендеринга коллекций. Когда коллекция передается в партиал с помощью опции :collection, партиал будет вставлен один раз для каждого члена коллекции:

  • index.html.erb

    <h1>Products</h1>
    <%= render partial: "product", collection: @products %>
    
  • _product.html.erb

    <p>Product Name: <%= product.name %></p>
    

Когда партиал вызывается с коллекцией во множественном числе, то каждый отдельный экземпляр партиала имеет доступ к члену коллекции, подлежащей рендерингу, через переменную с именем партиала. В нашем случает партиал _product, и в партиале _product можете обращаться к product для получения экземпляра, который рендерится.

Имеется также сокращенная запись для этого. Предположив, что @products является коллекцией экземпляров Product, можно просто написать так в index.html.erb и получить аналогичный результат:

<h1>Products</h1>
<%= render @products %>

Rails определяет имя партиала, изучая имя модели в коллекции. Фактически, можно даже создать гетерогенную коллекцию и рендерить ее таким образом, и Rails подберет подходящий партиал для каждого члена коллекции:

  • index.html.erb

    <h1>Contacts</h1>
    <%= render [customer1, employee1, customer2, employee2] %>
    
  • customers/_customer.html.erb

    <p>Customer: <%= customer.name %></p>
    
  • employees/_employee.html.erb

    <p>Employee: <%= employee.name %></p>
    

В этом случае Rails использует партиалы customer или employee по мере необходимости для каждого члена коллекции.

В случае, если коллекция пустая, render возвратит nil, поэтому очень просто предоставить альтернативное содержимое.

<h1>Products</h1>
<%= render(@products) || "There are no products available." %>
3.4.6. Локальные переменные

Чтобы использовать пользовательские имена локальных переменных в партиале, определите опцию :as в вызове партиала:

<%= render partial: "product", collection: @products, as: :item %>

С этим изменением можете получить доступ к экземпляру коллекции @products через локальную переменную item в партиале.

Также можно передавать произвольные локальные переменные в любой партиал, который рендерится с помощью опции locals: {}:

<%= render partial: "product", collection: @products,
           as: :item, locals: {title: "Products Page"} %>

В этом случае, партиал имеет доступ к локальной переменной title со значением "Products Page".

3.4.7. Переменные счетчика

Rails также создает переменную счетчика, доступную в партиале, вызываемом коллекцией. Переменная именуется заголовком партиала с добавленным _counter. Например, при рендеринге коллекции @products партиал _product.html.erb может получить доступ к переменной product_counter. Переменная индексирует количество раз, которое партиал был отрендерен во внешнюю вью, начиная со значения 0 при первом рендере.

# index.html.erb
<%= render partial: "product", collection: @products %>
# _product.html.erb
<%= product_counter %> # 0 для первого product, 1 для второго product...

Это также работает, когда имя партиала было изменено с помощью опции as:. Таким образом, если есть as: :item, переменная счетчика будет item_counter.

3.4.8. Разделяющие шаблоны

Также можно определить второй партиал, который будет отрендерен между экземплярами главного партиала, используя опцию :spacer_template:

<%= render partial: @products, spacer_template: "product_ruler" %>

Rails отрендерит партиал _product_ruler (без переданных в него данных) между каждой парой партиалов _product.

3.4.9. Макеты коллекции партиала

При рендеринге коллекций также возможно использовать опцию :layout:

<%= render partial: "product", collection: @products, layout: "special_layout" %>

Макет будет отрендерен вместе с партиалом для каждого элемента коллекции. Переменные текущего объекта и object_counter также будут доступны в макете, как это происходит в партиале.

3.5. Использование вложенных макетов

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

Предположим, имеется макет ApplicationController:

  • app/views/layouts/application.html.erb

    <html>
    <head>
      <title><%= @page_title or "Page Title" %></title>
      <%= stylesheet_link_tag "layout" %>
      <style><%= yield :stylesheets %></style>
    </head>
    <body>
      <div id="top_menu">Top menu items here</div>
      <div id="menu">Menu items here</div>
      <div id="content"><%= content_for?(:content) ? yield(:content) : yield %></div>
    </body>
    </html>
    

На страницах, сгенерированных NewsController, допустим, нужно спрятать верхнее меню и добавить правое меню:

  • app/views/layouts/news.html.erb

    <% content_for :stylesheets do %>
      #top_menu {display: none}
      #right_menu {float: right; background-color: yellow; color: black}
    <% end %>
    <% content_for :content do %>
      <div id="right_menu">Right menu items here</div>
      <%= content_for?(:news_content) ? yield(:news_content) : yield %>
    <% end %>
    <%= render template: "layouts/application" %>
    

Вот и все. Вью News будут использовать новый макет, прячущий верхнее меню и добавляющий новое правое меню в "content" div.

Существует несколько способов получения похожих результатов с различными подшаблонными схемами, используя эту технику. Отметьте, что нет ограничений на уровень вложенности. Можно использовать метод ActionView::render через render template: 'layouts/news', чтобы создать новый макет на основе макета News. Если есть уверенность, что не понадобятся подшаблоны для макета News, можно заменить строку content_for?(:news_content) ? yield(:news_content) : yield простым yield.