Макеты и рендеринг в Rails
Это руководство раскрывает основные возможности макетов Action Controller и Action View.
После прочтения этого руководства, вы узнаете:
- Как использовать различные методы рендеринга, встроенные в Rails.
- Как создавать макеты с несколькими разделами содержимого.
- Как использовать частичные шаблоны для соблюдения принципа DRY в ваших вьюхах.
- Как использовать вложенные макеты (подшаблоны).
Обзор: как кусочки складываются вместе
Это руководство сосредотачивается на взаимодействии между контроллером и вьюхой (представлением) в треугольнике модель-представление-контроллер (MVC). Как вы знаете, контроллер ответственен за управление целым процессом обслуживания запросов в Rails, хотя обычно любой серьезный код переносится в модель. Но когда приходит время послать ответ обратно пользователю, контроллер передает все вьюхе. Именно эта передача является предметом данного руководства.
В общих чертах все связано с решением, что же должно быть послано как ответ, и вызовом подходящего метода для создания этого ответа. Если ответом является полноценная вьюха, Rails также проводит дополнительную работу по упаковыванию вьюхи в макет и, возможно, по вставке частичных вьюх. В общем, все эти этапы вы увидите сами в следующих разделах.
Создание откликов
С точки зрения контроллера есть три способа создать отклик HTTP:
- Вызвать
render
для создания полного отклика, возвращаемого браузеру - Вызвать
redirect_to
для передачи браузеру кода переадресации HTTP - Вызвать
head
для создания отклика, включающего только заголовки HTTP, возвращаемого браузеру
Рендеринг по умолчанию: соглашения по конфигурации в действии
Вы уже слышали, что 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, method: :delete, data: { confirm: "Are you sure?" } %></td>
</tr>
<% end %>
</tbody>
</table>
<br>
<%= link_to "New book", new_book_path %>
NOTE: Фактически рендеринг осуществляется подклассами ActionView::TemplateHandlers
. Мы не будем углубляться в этот процесс, но важно знать, что расширение файла вьюхи контролирует выбор обработчика шаблона. Начиная с Rails 2, стандартные расширения это .erb
для ERB (HTML со встроенным Ruby) и .builder
для Builder (генератор XML).
Использование render
Во многих случаях метод ActionController::Base#render
выполняет большую работу по рендерингу содержимого Вашего приложения для использования в браузере. Имеются различные способы настройки возможностей render
. Вы можете рендерить вьюху по умолчанию для шаблона Rails, или определенный шаблон, или файл, или встроенный код, или совсем ничего. Можно рендерить текст, JSON или XML. Также можно определить тип содержимого или статус HTTP отрендеренного отклика.
TIP: Если хотите увидеть точные результаты вызова render
без необходимости проверять это в браузере, можете вызвать render_to_string
. Этот метод принимает те же самые опции, что и render
, но возвращает строку вместо отправки отклика обратно браузеру.
Рендеринг вьюхи экшна
Если хотите отрендерить вьюху, соответствующую другому шаблону этого же контроллера, можно использовать 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
end
end
Рендеринг шаблона экшна из другого контроллера
Что, если вы хотите отрендерить шаблон из абсолютно другого контроллера? Это можно также сделать с render
, который принимает полный путь шаблона для рендера (относительно app/views
). Например, если запускаем код в AdminProductsController
который находится в app/controllers/admin
, можете отрендерить результат экшна в шаблон в app/views/products
следующим образом:
render "products/show"
Rails знает, что эта вьюха принадлежит другому контроллеру, поскольку содержит символ слэша в строке. Если хотите быть точными, можете использовать опцию :template
(которая требовалась в Rails 2.2 и более ранних):
render template: "products/show"
Рендеринг произвольного файла
Метод render
также может использовать вьюху, которая находится вне вашего приложения:
render file: "/u/apps/warehouse_app/current/app/views/products/show"
Опция :file
принимает абсолютный путь в файловой системе. Разумеется, вам необходимы права на просмотр того, что вы используете для рендера.
NOTE: Использование опции :file
в комбинации с данными, введенными пользователем, может привести к проблемам безопасности, так как злоумышленник может использовать этот экшн для доступа к важным, с точки зрения безопасности, файлам вашей файловой системы.
NOTE: По умолчанию файл рендериться с использованием текущего макета.
TIP: Если вы используете Rails под Microsoft Windows, то должны использовать опцию :file
для рендера файла, потому что имена файлов Windows не имеют тот же формат, как имена файлов Unix.
Оборачивание
Вышеописанные три метода рендера (рендеринг другого шаблона в контроллере, рендеринг шаблона в другом контроллере и рендеринг произвольного файла в файловой системе) на самом деле являются вариантами одного и того же экшна.
Фактически в классе BooksController, в экшне update, в котором мы хотим отрендерить шаблон edit, если книга не была успешно обновлена, все нижеследующие вызовы отрендерят шаблон edit.html.erb
в директории views/books
:
render :edit
render action: :edit
render "edit"
render "edit.html.erb"
render action: "edit"
render action: "edit.html.erb"
render "books/edit"
render "books/edit.html.erb"
render template: "books/edit"
render template: "books/edit.html.erb"
render "/path/to/rails/app/views/books/edit"
render "/path/to/rails/app/views/books/edit.html.erb"
render file: "/path/to/rails/app/views/books/edit"
render file: "/path/to/rails/app/views/books/edit.html.erb"
Какой из них вы будете использовать - это вопрос стиля и соглашений, но практическое правило заключается в использовании простейшего, который больше подходит по стилю написания вашего кода.
Использование render
с :inline
Метод render
вполне может обойтись без вьюхи, если вы используете опцию :inline
для поддержки ERB, как части вызова метода. Это вполне валидно:
render inline: "<% products.each do |p| %><p><%= p.name %></p><% end %>"
WARNING: Должно быть серьезное основание для использования этой опции. Вкрапление ERB в контроллер нарушает MVC ориентированность Rails и создает трудности для других разработчиков в следовании логике вашего проекта. Вместо этого используйте отдельную erb-вьюху.
По умолчанию встроенный рендеринг использует ERB. Можете принудить использовать вместо этого Builder с помощью опции :type
:
render inline: "xml.p {'Horrid coding practice!'}", type: :builder
Рендеринг текста
Вы можете послать простой текст - совсем без разметки - обратно браузеру с использованием опции :plain
в render
:
render plain: "OK"
TIP: Рендеринг чистого текста наиболее полезен, когда вы делаете Ajax-отклик или отвечаете на запросы веб-сервиса, ожидающего что-то иное, чем HTML.
NOTE: По умолчанию при использовании опции :plain
текст рендерится без использования текущего макета. Если хотите, чтобы Rails вложил текст в текущий макет, необходимо добавить опцию layout: true
и использовать расширение .text.erb
для файла макета.
Рендеринг HTML
Вы можете вернуть HTML, используя опцию :html
метода render
:
render html: helpers.tag.strong('Not Found')
TIP: Это полезно когда вы хотите отрендерить небольшой кусочек HTML кода. Однако, если у вас достаточно сложная разметка, стоит рассмотреть выделение её в файл шаблона.
NOTE: Когда используется опция html:
, HTML объекты будут экранироваться, если строка не состоит из API, поддерживающих html_safe
.
Рендеринг JSON
JSON - это формат данных JavaScript, используемый многими библиотеками Ajax. Rails имеет встроенную поддержку для преобразования объектов в JSON и рендеринга этого JSON обратно браузеру:
render json: @product
TIP: Не нужно вызывать to_json
в объекте, который хотите рендерить. Если используется опция :json
, render
автоматически вызовет to_json
за вас.
Рендеринг XML
Rails также имеет встроенную поддержку для преобразования объектов в XML и рендеринга этого XML обратно вызывающему:
render xml: @product
TIP: Не нужно вызывать to_xml
в объекте, который хотите рендерить. Если используется опция :xml
, render
автоматически вызовет to_xml
за вас.
Рендеринг внешнего JavaScript
Rails может рендерить чистый JavaScript:
render js: "alert('Hello Rails');"
Это пошлет указанную строку в браузер с типом MIME text/javascript
.
Рендеринг необработанного содержимого
Вы можете вернуть необработанный текст, без установки типа содержимого,
используя опцию :body
, метода render
:
render body: "raw"
TIP: Эта опция должна использоваться, только если вам не важен тип содержимого ответа.
Использование :plain
или :html
уместнее в большинстве случаев.
NOTE: Возвращенным откликом от этой опции будет text/html
(если не будет переопределен),
так как это тип содержимого по умолчанию у отклика Action Dispatch.
Опции для render
Вызов метода render
как правило принимает пять опций:
:content_type
:layout
:location
:status
:formats
Опция :content_type
По умолчанию Rails будет обрабатывать результаты операции рендеринга с типом содержимого MIME text/html
(или application/json
, если используется опция :json
, или application/xml
для опции :xml
). Иногда бывает так, что нужно изменить это, и тогда необходимо настроить опцию :content_type
:
render file: filename, content_type: "application/rss"
Опция :layout
С большинством опций для render
, отрендеренное содержимое отображается как часть текущего макета. Вы узнаете более подробно о макетах, и как их использовать, позже в этом руководстве.
Опция :layout
нужна, чтобы сообщить Rails о необходимости использовать определенный файл как макет для текущего экшна:
render layout: "special_layout"
Также можно сообщить Rails, что требуется рендерить вообще без макета:
render layout: false
Опция :location
Опцию :location
можно использовать, чтобы установить заголовок HTTP Location
:
render xml: photo, location: photo_url(photo)
(the-status-option) Опция :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 |
NOTE: Если попытаться отрендерить содержимое наряду с кодом статуса без содержимого (100-199, 204, 205 или 304), он будет исключён из ответа.
Опция :formats
Rails использует формат, определённый в запросе (или :html
по умолчанию). Вы можете изменить его, передав в опцию :formats
символ или массив:
render formats: :xml
render formats: [:json, :xml]
Если шаблон с указанным форматом не существует, вызывается ошибка ActionView::MissingTemplate
.
Поиск макетов
Чтобы найти текущий макет, 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 также предоставляет несколько способов для более точного назначения определенных макетов отдельным контроллерам и экшнам.
Определение макетов для контроллеров
Вы можете переопределить дефолтные соглашения по макетам в контроллере, используя объявление 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
как макет.
Выбор макетов во время выполнения
Можно использовать символ для отсрочки выбора макета до тех пор, пока не будет обработан запрос:
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
Условные макеты
Макеты, определенные на уровне контроллера, поддерживают опции :only
и :except
. Эти опции принимают либо имя метода, либо массив имен методов, соответствующих именам методов в контроллере:
class ProductsController < ApplicationController
layout "product", except: [:index, :rss]
end
С таким объявлением макет product
будет использован везде, кроме методов rss
и index
.
Наследование макета
Объявление макета ниже по иерархии и более специфическое объявление макета всегда переопределяет более общие. Например:
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
Наследование шаблона
Следуя логике наследования макета, если шаблон или партиал не найдены по обычному пути, контроллер будет искать шаблон или партиал для рендеринга по цепочке наследования. Например:
# in app/controllers/application_controller
class ApplicationController < ActionController::Base
end
# in app/controllers/admin_controller
class AdminController < ApplicationController
end
# in 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>.
Избегание ошибок двойного рендера
Рано или поздно, большинство разработчиков на 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
за один проход. Еще может помочь такая вещь, как and return
. Вот исправленная версия метода:
def show
@book = Book.find(params[:id])
if @book.special?
render action: "special_show" and return
end
render action: "regular_show"
end
Убедитесь, что используете and return
вместо && return
, поскольку && return
не будет работать в связи с приоритетом операторов в языке Ruby.
Отметьте, что неявный рендер, выполняемый ActionController, определяет, был ли вызван render
поэтому следующий код будет работать без проблем:
def show
@book = Book.find(params[:id])
if @book.special?
render action: "special_show"
end
end
Это отрендерит книгу (book) с special?
, заданным с помощью шаблона special_show
, в то время как остальные книги будут рендериться с дефолтным шаблоном show
.
Использование 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)
NOTE: redirect_to
и redirect_back
не прерывают и не возвращают из выполняемого метода немедленно, а просто устанавливают отклики HTTP. Выражения, следующие после них в методе, будут выполнены. При необходимости можно прервать явным return
или любым другим механизмом прерывания.
Получение различного кода статуса перенаправления
Rails использует код статуса HTTP 302, временное перенаправление, при вызове redirect_to
. Если хотите использовать иной код статуса, возможно 301, постоянное перенаправление, можете использовать опцию :status
:
redirect_to photos_path, status: 301
Подобно опции :status
для render
, :status
для redirect_to
принимает и числовые, и символьные обозначения заголовка.
Различие между 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, сообщающим пользователю, что произошло.
Использование 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
Структурирование макетов
Когда Rails рендерит вьюху как отклик, он делает это путем объединения вьюхи с текущим макетом, используя правила для нахождения текущего макета, которые были рассмотрены ранее. В макетах у вас есть доступ к трем инструментам для объединения различных кусочков результата для формирования общего отклика:
- Теги ассетов
yield
иcontent_for
- Партиалы
Хелперы ассетных тегов
Хелперы ассетных тегов предоставляют методы для генерации HTML, связывающие вьюхи с лентами новостей, JavaScript, таблицами стилей, изображениями, видео и аудио. В Rails доступно шесть хелперов ассетных тегов:
auto_discovery_link_tag
javascript_include_tag
stylesheet_link_tag
image_tag
video_tag
audio_tag
Эти теги можно использовать в макетах или других вьюхах, хотя auto_discovery_link_tag
, javascript_include_tag
и stylesheet_link_tag
как правило используются в разделе <head>
макета.
WARNING: Хелперы ассетных тегов не проверяют существование ассетов по заданному расположению; они просто предполагают, что вы знаете, что делаете, и генерируют ссылку.
Присоединение каналов с помощью auto_discovery_link_tag
Хелпер 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".
Присоединение файлов 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" %>
Присоединение файлов CSS с помощью stylesheet_link_tag
Хелпер 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
создает ссылки с media="screen" rel="stylesheet"
. Можно переопределить любое из этих дефолтных значений, указав соответствующую опцию (:media, :rel):
<%= stylesheet_link_tag "main_print", media: "print" %>
Присоединение изображений с помощью image_tag
Хелпер image_tag
создает HTML-тег <img />
для определенного файла. По умолчанию файлы загружаются из public/images
.
WARNING: Обратите внимание, что нужно указывать расширение изображения.
<%= 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" %>
Присоединение видео с помощью video_tag
Хелпер video_tag
создает тег HTML 5 <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>
Присоединение аудиофайлов с помощью audio_tag
Хелпер audio_tag
создает тег HTML 5 <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
, файл аудио предварительно загружается для пользователя при загрузке страницы.
Понимание yield
В контексте макета, yield
определяет раздел, где должно быть вставлено содержимое из вьюхи. Самый простой способ его использования - это иметь один yield
там, куда вставится все содержимое вьюхи, которая в настоящий момент рендерится:
<html>
<head>
</head>
<body>
<%= yield %>
</body>
</html>
Также можете создать макет с несколькими разделами yield:
<html>
<head>
<%= yield :head %>
</head>
<body>
<%= yield %>
</body>
</html>
Основное тело вьюхи всегда рендериться в неименованный yield
. Чтобы рендерить содержимое в именованный yield
, используйте метод content_for
.
Использование метода 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 в хедер макета в целом.
Использование партиалов
Частичные шаблоны - также называемые "партиалы" - являются еще одним подходом к разделению процесса рендеринга на более управляемые кусочки. С партиалами можно перемещать код для рендеринга определенных частей отклика в свои отдельные файлы.
Именование партиалов
Чтобы отрендерить партиал как часть вьюхи, используем метод render
внутри вьюхи:
<%= render "menu" %>
Это отрендерит файл, названный _menu.html.erb
в этом месте в пределах рендеремой вьюхи. Обратите внимание на начальный символ подчеркивания: файлы партиалов начинаются со знака подчеркивания, чтобы отличать их от обычных вьюх, несмотря на то, что в вызове они указаны без подчеркивания. Это справедливо даже тогда, когда партиалы вызываются из другой папки:
<%= render "shared/menu" %>
Этот код вытянет партиал из app/views/shared/_menu.html.erb
.
Использование партиалов для упрощения вьюх
Один из способов применения партиалов это использоваться их как эквивалент подпрограмм: способ переместить часть разметки из вьюхи так, чтобы можно было легче понять, что там происходит. Например, у вас может быть такая вьюха:
<%= 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 |f| %> <p> Name contains: <%= f.text_field :name_contains %> </p> <% end %>
roles/index.html.erb
<%= render "shared/search_filters", search: @q do |f| %> <p> Title contains: <%= f.text_field :title_contains %> </p> <% end %>
shared/_search_filters.html.erb
<%= form_for(search) do |f| %> <h1>Search form:</h1> <fieldset> <%= yield f %> </fieldset> <p> <%= f.submit "Search" %> </p> <% end %>
TIP: Для содержимого, располагаемого на всех страницах вашего приложения, можете использовать партиалы прямо в макетах.
Макеты партиала
Партиал может использовать свой собственный файл макета, подобно тому, как вьюха может использовать макет. Например, можете вызвать подобный партиал:
<%= render partial: "link_area", layout: "graybar" %>
Это найдет партиал с именем _link_area.html.erb
и отрендерит его, используя макет _graybar.html.erb
. Отметьте, что макеты для партиалов также начинаются с подчеркивания, как и обычные партиалы, и размещаются в той же папке с партиалами, которым они принадлежат (не в основной папке layouts
).
Также отметьте, что явное указание partial
необходимо, когда передаются дополнительные опции, такие как layout
Передача локальных переменных
В партиалы также можно передавать локальные переменные, что делает их более мощными и гибкими. Например, можете использовать такую технику для уменьшения дублирования между страницами 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 |f| %> <p> <b>Zone name</b><br> <%= f.text_field :name %> </p> <p> <%= f.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
в родительской вьюхе.
Рендеринг коллекций
Партиалы часто используют для рендеринга коллекций. Когда коллекция передается в партиал с помощью опции :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." %>
Локальные переменные
Чтобы использовать пользовательские имена локальных переменных в партиале, определите опцию :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".
TIP: Rails также создает переменную счетчика, доступную в партиале, вызываемом коллекцией, названную именем члена коллекции с добавленным _counter
. Например, если рендерится @products
, в партиале можно обратиться к product_counter
, который говорит, сколько раз партиал был отрендерен. Это не работает в сочетании с опцией as: :value
.
Также можно определить второй партиал, который будет отрендерен между экземплярами главного партиала, используя опцию :spacer_template
:
Разделяющие шаблоны
<%= render partial: @products, spacer_template: "product_ruler" %>
Rails отрендерит партиал _product_ruler
(без переданных в него данных) между каждой парой партиалов _product
.
Макеты коллекции партиала
При рендеренге коллекций также возможно использовать опцию :layout
:
<%= render partial: "product", collection: @products, layout: "special_layout" %>
Макет будет отрендерен вместе с партиалом для каждого элемента коллекции. Переменные текущего объекта и object_counter также будут доступны в макете, как это происходит в партиале.
Использование вложенных макетов
Возможно, ваше приложение потребует макет, немного отличающийся от обычного макета приложения, для поддержки одного определенного контроллера. Вместо повторения главного макета и редактирования его, можете выполнить это с помощью вложенных макетов (иногда называемых подшаблонами). Вот пример:
Предположим, имеется макет 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
.