Роутинг в Rails

Это руководство охватывает открытые для пользователя функции роутинга Rails.

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

  • Как интерпретировать код в config/routes.rb
  • Как создавать свои собственные маршруты, используя либо предпочтительный ресурсный стиль, либо метод match
  • Как определять параметры маршрута, которые передаются в экшн контроллера
  • Как автоматически создавать пути и URL, используя маршрутные хелперы
  • О продвинутых техниках, таких как создание ограничений и монтирование точек назначения Rack

1. Цель роутера Rails

Роутер Rails распознает URL и направляет его в экшн контроллера или в приложение Rack. Он также может генерировать пути и URL, избегая необходимость жестко прописывать строки в ваших вью.

1.1. Соединение URL с кодом

Когда ваше приложение на Rails получает входящий запрос для:

GET /patients/17

оно опрашивает роутер на предмет соответствия экшну контроллера. Если первый соответствующий маршрут это:

get '/patients/:id', to: 'patients#show'

то запрос будет направлен в контроллер patients в экшн show с { id: '17' } в params.

Rails здесь использует именование в змеином_регистре (snake_case) для имен контроллера, если имя контроллера состоит из несколько слов, то, например, MonsterTrucksController необходимо использовать как monster_trucks#show.

1.2. Создание URL из кода

Также можно генерировать пути и URL. Если вышеуказанный маршрут модифицировать на:

get '/patients/:id', to: 'patients#show', as: 'patient'

и ваше приложение содержит код в контроллере:

@patient = Patient.find(params[:id])

и такой в соответствующей вью:

<%= link_to 'Patient Record', patient_path(@patient) %>

тогда роутер сгенерирует путь /patients/17. Это увеличит устойчивость вашей вью и упростит код для понимания. Отметьте, что id не нужно указывать в маршрутном хелпере.

1.3. Настройка маршрутизатора Rails

Маршруты для приложения или engine располагаются в файле config/routes.rb и обычно выглядят так:

Rails.application.routes.draw do
  resources :brands, only: [:index, :show] do
    resources :products, only: [:index, :show]
  end

  resource :basket, only: [:show, :update, :destroy]

  resolve("Basket") { route_for(:basket) }
end

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

Блок Rails.application.routes.draw do ... end, который оборачивает определения маршрутов, требует создания области видимости для DSL маршрутизатора и не должен быть удален.

2. Ресурсный роутинг: по умолчанию в Rails

Ресурсный роутинг позволяет быстро объявлять все общие маршруты для заданного ресурсного контроллера. Единственный вызов resources может объявить все маршруты, необходимые для экшнов index, show, new, edit, create, update и destroy.

2.1. Ресурсы в вебе

Браузеры запрашивают страницы от Rails, выполняя запрос по URL, используя определенный метод HTTP, такой как GET, POST, PATCH, PUT и DELETE. Каждый метод - это запрос на выполнение операции с ресурсом. Ресурсный маршрут соединяет несколько родственных запросов с экшнами в одном контроллере.

Когда приложение на Rails получает входящий запрос для:

DELETE /photos/17

оно просит роутер соединить его с экшном контроллера. Если первый соответствующий маршрут такой:

resources :photos

Rails будет направлять этот запрос в экшн destroy контроллера photos с { id: '17' } в params.

2.2. CRUD, методы и экшны

В Rails ресурсный маршрут предоставляет сопоставление между методами HTTP и URL к экшнам контроллера. По соглашению, каждый экшн также соединяется с определенной операцией CRUD в базе данных. Одна запись в файле роутинга, такая как:

resources :photos

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

Метод HTTP Путь Контроллер#Экшн Использование
GET /photos photos#index отображает список всех фото
GET /photos/new photos#new возвращает форму HTML для создания нового фото
POST /photos photos#create создает новое фото
GET /photos/:id photos#show отображает определенное фото
GET /photos/:id/edit photos#edit возвращает форму HTML для редактирования фото
PATCH/PUT /photos/:id photos#update обновляет определенное фото
DELETE /photos/:id photos#destroy удаляет определенное фото

Поскольку роутер использует как метод HTTP, так и URL, для сопоставления с входящими запросами, четыре URL соединяют с семью различными экшнами.

Маршруты Rails сравниваются в том порядке, в котором они определены, поэтому, если имеется resources :photos до get 'photos/poll' маршрут для экшна show в строчке resources совпадет до строчки get. Чтобы это исправить, переместите строчку get выше строчки resources, чтобы она сравнивалась первой.

2.3. Путь и хелперы URL

Создание ресурсного маршрута также сделает доступными множество хелперов в контроллере вашего приложения. В случае с resources :photos:

  • photos_path возвращает /photos
  • new_photo_path возвращает /photos/new
  • edit_photo_path(:id) возвращает /photos/:id/edit (например, edit_photo_path(10) возвращает /photos/10/edit)
  • photo_path(:id) возвращает /photos/:id (например, photo_path(10) возвращает /photos/10)

Каждый из этих хелперов имеет соответствующий хелпер _url (такой как photos_url), который возвращает тот же путь с добавленными текущими хостом, портом и префиксом пути.

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

2.4. Определение нескольких ресурсов одновременно

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

resources :photos, :books, :videos

Это приведет к такому же результату, как и:

resources :photos
resources :books
resources :videos

2.5. Одиночные ресурсы

Иногда имеется ресурс, который клиенты всегда просматривают без ссылки на ID. Обычный пример, /profile всегда показывает профиль текущего зарегистрированного пользователя. Для этого можно использовать одиночный ресурс, чтобы связать /profile (а не /profile/:id) с экшном show:

get 'profile', to: 'users#show'

Передавая String в to: ожидается следующий формат - controller#action. Когда используется Symbol, опция to: должна быть заменена на action:. Когда используется String без #, опция to: должна быть заменена на controller::

get 'profile', action: :show, controller: 'users'

Этот ресурсный маршрут:

resource :geocoder
resolve('Geocoder') { [:geocoder] }

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

Метод HTTP Путь Контроллер#Экшн Использование
GET /geocoder/new geocoders#new возвращает форму HTML для создания нового геокодера
POST /geocoder geocoders#create создает новый геокодер
GET /geocoder geocoders#show отображает один и только один ресурс геокодера
GET /geocoder/edit geocoders#edit возвращает форму HTML для редактирования геокодера
PATCH/PUT /geocoder geocoders#update обновляет один и только один ресурс геокодера
DELETE /geocoder geocoders#destroy удаляет ресурс геокодера

Поскольку вы можете захотеть использовать один и тот же контроллер и для одиночного маршрута (/account), и для множественного маршрута (/accounts/45), одиночные ресурсы ведут на множественные контроллеры. По этой причине, например, resource :photo и resources :photos создадут и одиночные, и множественные маршруты, привязанные к одному и тому же контроллеру (PhotosController).

Одиночный ресурсный маршрут генерирует эти хелперы:

  • new_geocoder_path возвращает /geocoder/new
  • edit_geocoder_path возвращает /geocoder/edit
  • geocoder_path возвращает /geocoder

Вызов resolve необходим для преобразования экземпляров Geocoder в маршруты через идентификацию записи.

Как и в случае с множественными ресурсами, те же хелперы, оканчивающиеся на _url также включают хост, порт и префикс пути.

2.6. Пространство имен контроллера и роутинг

Возможно, вы захотите организовать группы контроллеров в пространстве имен. Чаще всего группируют административные контроллеры в пространство имен Admin:: и размещают их в директории app/controllers/admin. Можно маршрутизировать на такую группу с помощью блока namespace:

namespace :admin do
  resources :articles, :comments
end

Это создаст ряд маршрутов для каждого контроллера articles и comments. Для Admin::ArticlesController, Rails создаст:

Метод HTTP Путь Контроллер#Экшн Именованный хелпер маршрута
GET /admin/articles admin/articles#index admin_articles_path
GET /admin/articles/new admin/articles#new new_admin_article_path
POST /admin/articles admin/articles#create admin_articles_path
GET /admin/articles/:id admin/articles#show admin_article_path(:id)
GET /admin/articles/:id/edit admin/articles#edit edit_admin_article_path(:id)
PATCH/PUT /admin/articles/:id admin/articles#update admin_article_path(:id)
DELETE /admin/articles/:id admin/articles#destroy admin_article_path(:id)

Если вместо этого хотите маршрут /articles (без префикса /admin) к Admin::ArticlesController, можно указать модуль в блоке scope:

scope module: 'admin' do
  resources :articles, :comments
end

Это также можно сделать одиночным маршрутом:

resources :articles, module: 'admin'

Если вместо этого хотите маршрут /admin/articles к ArticlesController (без префикса модуля Admin::), можно указать путь с помощью блока scope:

scope '/admin' do
  resources :articles, :comments
end

Это также можно сделать одиночным маршрутом:

resources :articles, path: '/admin/articles'

В обоих случаях, хелперы именованных маршрутов остаются теми же, что и без использования scope. В последнем случае, следующие пути соединят с ArticlesController:

Метод HTTP Путь Контроллер#Экшн Именованный хелпер маршрута
GET /admin/articles articles#index articles_path
GET /admin/articles/new articles#new new_article_path
POST /admin/articles articles#create articles_path
GET /admin/articles/:id articles#show article_path(:id)
GET /admin/articles/:id/edit articles#edit edit_article_path(:id)
PATCH/PUT /admin/articles/:id articles#update article_path(:id)
DELETE /admin/articles/:id articles#destroy article_path(:id)

Если хотите использовать другое пространство имен контроллера в блоке namespace, можно указать абсолютный путь контроллера, т.е: get '/foo', to: '/foo#index'.

2.7. Вложенные ресурсы

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

class Magazine < ApplicationRecord
  has_many :ads
end

class Ad < ApplicationRecord
  belongs_to :magazine
end

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

resources :magazines do
  resources :ads
end

В дополнение к маршрутам для magazines, это объявление также создаст маршруты для ads в AdsController. URL с ad требует magazine:

Метод HTTP Путь Контроллер#Экшн Использование
GET /magazines/:magazine_id/ads ads#index отображает список всех ads для определенного magazine
GET /magazines/:magazine_id/ads/new ads#new возвращает форму HTML для создания новой ad, принадлежащей определенному magazine
POST /magazines/:magazine_id/ads ads#create создает новую ad, принадлежащую указанному magazine
GET /magazines/:magazine_id/ads/:id ads#show отражает определенную ad, принадлежащую определенному magazine
GET /magazines/:magazine_id/ads/:id/edit ads#edit возвращает форму HTML для редактирования ad, принадлежащей определенному magazine
PATCH/PUT /magazines/:magazine_id/ads/:id ads#update обновляет определенную ad, принадлежащую определенному magazine
DELETE /magazines/:magazine_id/ads/:id ads#destroy удаляет определенную ad, принадлежащую определенному magazine

Также будут созданы маршрутные хелперы, такие как magazine_ads_url и edit_magazine_ad_path. Эти хелперы принимают экземпляр Magazine как первый параметр (magazine_ads_url(@magazine)).

2.7.1. Ограничения для вложения

Вы можете вкладывать ресурсы в другие вложенные ресурсы, если хотите. Например:

resources :publishers do
  resources :magazines do
    resources :photos
  end
end

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

/publishers/1/magazines/2/photos/3

Соответствующий маршрутный хелпер будет publisher_magazine_photo_url, требующий определения объектов на всех трех уровнях. Действительно, эта ситуация достаточно запутана, так что в статье Jamis Buck предлагает правило хорошей разработки на Rails:

Ресурсы никогда не должны быть вложены глубже, чем на 1 уровень.

2.7.2. Мелкое вложение

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

resources :articles do
  resources :comments, only: [:index, :new, :create]
end
resources :comments, only: [:show, :edit, :update, :destroy]

Эта идея балансирует на грани между наглядностью маршрутов и глубоким вложением. Существует сокращенный синтаксис для получения подобного с помощью опции :shallow:

resources :articles do
  resources :comments, shallow: true
end

Это сгенерирует те же самые маршруты из первого примера. Также можно определить опцию :shallow в родительском ресурсе, в этом случае все вложенные ресурсы будут мелкие:

resources :articles, shallow: true do
  resources :comments
  resources :quotes
  resources :drafts
end

Тут для ресурса articles будут сгенерированы следующие маршруты:

Метод HTTP Путь Контроллер#Экшн Именованный маршрутный хелпер
GET /articles/:article_id/comments(.:format) comments#index article_comments_path
POST /articles/:article_id/comments(.:format) comments#create article_comments_path
GET /articles/:article_id/comments/new(.:format) comments#new new_article_comment_path
GET /comments/:id/edit(.:format) comments#edit edit_comment_path
GET /comments/:id(.:format) comments#show comment_path
PATCH/PUT /comments/:id(.:format) comments#update comment_path
DELETE /comments/:id(.:format) comments#destroy comment_path
GET /articles/:article_id/quotes(.:format) quotes#index article_quotes_path
POST /articles/:article_id/quotes(.:format) quotes#create article_quotes_path
GET /articles/:article_id/quotes/new(.:format) quotes#new new_article_quote_path
GET /quotes/:id/edit(.:format) quotes#edit edit_quote_path
GET /quotes/:id(.:format) quotes#show quote_path
PATCH/PUT /quotes/:id(.:format) quotes#update quote_path
DELETE /quotes/:id(.:format) quotes#destroy quote_path
GET /articles/:article_id/drafts(.:format) drafts#index article_drafts_path
POST /articles/:article_id/drafts(.:format) drafts#create article_drafts_path
GET /articles/:article_id/drafts/new(.:format) drafts#new new_article_draft_path
GET /drafts/:id/edit(.:format) drafts#edit edit_draft_path
GET /drafts/:id(.:format) drafts#show draft_path
PATCH/PUT /drafts/:id(.:format) drafts#update draft_path
DELETE /drafts/:id(.:format) drafts#destroy draft_path
GET /articles(.:format) articles#index articles_path
POST /articles(.:format) articles#create articles_path
GET /articles/new(.:format) articles#new new_article_path
GET /articles/:id/edit(.:format) articles#edit edit_article_path
GET /articles/:id(.:format) articles#show article_path
PATCH/PUT /articles/:id(.:format) articles#update article_path
DELETE /articles/:id(.:format) articles#destroy article_path

Метод shallow в DSL создает область видимости, в котором каждое вложение мелкое. Это генерирует те же самые маршруты из предыдущего примера:

shallow do
  resources :articles do
    resources :comments
    resources :quotes
    resources :drafts
  end
end

Также существуют две опции для scope для настройки мелких маршрутов. :shallow_path добавляет к путям элемента префикс с указанным параметром:

scope shallow_path: "sekret" do
  resources :articles do
    resources :comments, shallow: true
  end
end

Для ресурса комментариев будут сгенерированы следующие маршруты:

Метод HTTP Путь Контроллер#Экшн Именованный хелпер маршрута
GET /articles/:article_id/comments(.:format) comments#index article_comments_path
POST /articles/:article_id/comments(.:format) comments#create article_comments_path
GET /articles/:article_id/comments/new(.:format) comments#new new_article_comment_path
GET /sekret/comments/:id/edit(.:format) comments#edit edit_comment_path
GET /sekret/comments/:id(.:format) comments#show comment_path
PATCH/PUT /sekret/comments/:id(.:format) comments#update comment_path
DELETE /sekret/comments/:id(.:format) comments#destroy comment_path

Опция :shallow_prefix добавляет указанный параметр к именованным хелперам маршрута:

scope shallow_prefix: "sekret" do
  resources :articles do
    resources :comments, shallow: true
  end
end

Для ресурса комментариев будут сгенерированы следующие маршруты:

Метод HTTP Путь Контроллер#Экшн Именованный хелпер маршрута
GET /articles/:article_id/comments(.:format) comments#index article_comments_path
POST /articles/:article_id/comments(.:format) comments#create article_comments_path
GET /articles/:article_id/comments/new(.:format) comments#new new_article_comment_path
GET /comments/:id/edit(.:format) comments#edit edit_sekret_comment_path
GET /comments/:id(.:format) comments#show sekret_comment_path
PATCH/PUT /comments/:id(.:format) comments#update sekret_comment_path
DELETE /comments/:id(.:format) comments#destroy sekret_comment_path

2.8. Концерны маршрутов

Концерны маршрутов позволяют объявлять общие маршруты, которые затем могут быть повторно использованы внутри других ресурсов и маршрутов. Чтобы определить концерн, используйте блок concern:

concern :commentable do
  resources :comments
end

concern :image_attachable do
  resources :images, only: :index
end

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

resources :messages, concerns: :commentable

resources :articles, concerns: [:commentable, :image_attachable]

Вышеуказанное эквивалентно:

resources :messages do
  resources :comments
end

resources :articles do
  resources :comments
  resources :images, only: :index
end

Также их можно использовать где угодно, с помощью concerns. Например, в блоке scope или namespace:

namespace :articles do
  concerns :commentable
end

2.9. Создание путей и URL из объектов

В дополнение к использованию маршрутных хелперов, Rails может также создавать пути и URL из массива параметров. Например, предположим, у вас есть этот набор маршрутов:

resources :magazines do
  resources :ads
end

При использовании magazine_ad_path, можно передать экземпляры Magazine и Ad вместо числовых ID:

<%= link_to 'Ad details', magazine_ad_path(@magazine, @ad) %>

Можно также использовать url_for с набором объектов, и Rails автоматически определит, какой маршрут вам нужен:

<%= link_to 'Ad details', url_for([@magazine, @ad]) %>

В этом случае Rails увидит, что @magazine это Magazine и @ad это Ad, и поэтому использует хелпер magazine_ad_path. В хелперах, таких как link_to, можно определить лишь объект вместо полного вызова url_for:

<%= link_to 'Ad details', [@magazine, @ad] %>

Если хотите ссылку только на magazine:

<%= link_to 'Magazine details', @magazine %>

Для других экшнов следует всего лишь вставить имя экшна как первый элемент массива:

<%= link_to 'Edit Ad', [:edit, @magazine, @ad] %>

Это позволит рассматривать экземпляры модели как URL, что является ключевым преимуществом ресурсного стиля.

2.10. Определение дополнительных экшнов RESTful

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

2.10.1. Добавление маршрутов к элементам

Для добавления маршрута к элементу, добавьте блок member в блок ресурса:

resources :photos do
  member do
    get 'preview'
  end
end

Это распознает /photos/1/preview с GET и направит в экшн preview контроллера PhotosController со значением id ресурса, переданного в params[:id]. Это также создаст хелперы preview_photo_url и preview_photo_path.

В блоке маршрутов элемента каждое имя маршрута определяет метод HTTP, с которым он будет связан. Тут можно использовать get, patch, put, post или delete. Если у вас нет нескольких маршрутов к member, также можно передать :on к маршруту, избавившись от блока:

resources :photos do
  get 'preview', on: :member
end

Можно опустить опцию :on, это создаст такой же маршрут для элемента, за исключением того, что значение id ресурса будет доступно в params[:photo_id] вместо params[:id]. Хелперы маршрутов также будут переименованы из preview_photo_url и preview_photo_path в photo_preview_url и photo_preview_path.

2.10.2. Добавление маршрутов к коллекции

Чтобы добавить маршрут к коллекции, используйте блок collection:

resources :photos do
  collection do
    get 'search'
  end
end

Это позволит Rails распознавать пути, такие как /photos/search с GET и направить в экшн search контроллера PhotosController. Это также создаст маршрутные хелперы search_photos_url и search_photos_path.

Как и с маршрутами к элементу, можно передать :on к маршруту:

resources :photos do
  get 'search', on: :collection
end

Если определяете дополнительные ресурсные маршруты с символом в качестве первого аргумента, помните, что это не эквивалент использования строки. Символы означают экшны контроллера, а строки означают пути.

2.10.3. Добавление маршрутов для дополнительных экшнов new

Чтобы добавить альтернативный экшн new, используйте сокращенный вариант :on:

resources :comments do
  get 'preview', on: :new
end

Это позволит Rails распознавать пути, такие как /comments/new/preview с GET, и направлять их в экшн preview в CommentsController. Он также создаст маршрутные хелперы preview_new_comment_url и preview_new_comment_path.

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

3. Нересурсные маршруты

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

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

В частности, простой роутинг облегчает привязку унаследованных URL к новым экшнам Rails.

3.1. Необязательные параметры

При настройке обычного маршрута вы предоставляете ряд символов, которые Rails связывает с частями входящего запроса HTTP. Например, рассмотрим следующий маршрут:

get 'photos(/:id)', to: 'photos#display'

Если входящий запрос /photos/1 обрабатывается этим маршрутом (так как он не соответствует любому предыдущему маршруту до этого), то результатом будет вызов экшна display в PhotosController, и результирующий параметр "1" будет доступен как params[:id]. Этот маршрут также свяжет входящий запрос /photos с PhotosController#display, поскольку :id — опциональный параметр, обозначенный скобками.

3.2. Динамические сегменты

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

get 'photos/:id/:user_id', to: 'photos#show'

Входящий путь /photos/1/2 будет направлен на экшн show в PhotosController. params[:id] будет установлен как "1", и params[:user_id] будет установлен как "2".

По умолчанию динамические сегменты не принимают точки - потому что точка используется как разделитель для формата маршрутов. Если в динамическом сегменте необходимо использовать точку, добавьте ограничение, переопределяющее это – к примеру, id: /[^\/]+/ позволяет все, кроме слэша.

3.3. Статичные сегменты

Можете определить статичные сегменты при создании маршрута, не начинающиеся с двоеточия в сегменте:

get 'photos/:id/with_user/:user_id', to: 'photos#show'

Этот маршрут соответствует путям, таким как /photos/1/with_user/2. В этом случае params будет { controller: 'photos', action: 'show', id: '1', user_id: '2' }.

3.4. Параметры строки запроса

params также включает любые параметры из строки запроса. Например, с таким маршрутом:

get 'photos/:id', to: 'photos#show'

Входящий путь /photos/1?user_id=2 будет направлен на экшн show контроллера Photos. params будет { controller: 'photos', action: 'show', id: '1', user_id: '2' }.

3.5. Определение значений по умолчанию

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

get 'photos/:id', to: 'photos#show', defaults: { format: 'jpg' }

Rails направит photos/12 в экшн show PhotosController, и установит params[:format] как "jpg".

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

defaults format: :json do
  resources :photos
end

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

3.6. Именование маршрутов

Можно определить имя для любого маршрута, используя опцию :as:

get 'exit', to: 'sessions#destroy', as: :logout

Это создаст logout_path и logout_url как именованные хелперы маршрута в вашем приложении. Вызов logout_path вернет /exit

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

get ':username', to: 'users#show', as: :user
resources :users

Что определит метод user_path, который будет доступен в контроллерах, хелперах и вью, и будет вести на маршрут, такой как /bob. В экшне show из UsersController, params[:username] будет содержать имя пользователя. Измените :username в определении маршрута, если не хотите, чтобы имя параметра было :username.

3.7. Ограничения метода HTTP

В основном следует использовать методы get, post, put, patch и delete для ограничения маршрута определенным методом. Можно использовать метод match с опцией :via для соответствия нескольким методам сразу:

match 'photos', to: 'photos#show', via: [:get, :post]

Также можно установить соответствие всем методам для определенного маршрута, используя :via: :all:

match 'photos', to: 'photos#show', via: :all

Роутинг запросов GET и POST одновременно в один экшн небезопасен. В основном, следует избегать роутинг всех методов в экшн, если нет веской причины делать так.

GET в Rails не проверяет токен CSRF. Никогда не пишите в базу данных из GET запросов, подробнее о контрмерах CSRF смотрите в руководстве Безопасность приложений на Rails.

3.8. Ограничения сегмента

Можно использовать опцию :constraints для соблюдения формата динамического сегмента:

get 'photos/:id', to: 'photos#show', constraints: { id: /[A-Z]\d{5}/ }

Этот маршрут соответствует путям, таким как /photos/A12345, но не /photos/893. Можно выразить тот же маршрут более кратко:

get 'photos/:id', to: 'photos#show', id: /[A-Z]\d{5}/

:constraints принимает регулярное выражение c тем ограничением, что якоря regexp не могут использоваться. Например, следующий маршрут не работает:

get '/:id', to: 'articles#show', constraints: {id: /^\d/}

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

Например, следующие маршруты приведут к articles со значениями to_param наподобие 1-hello-world, которые всегда начинаются с цифры, и к users со значениями to_param наподобие david, которые никогда не начинаются с цифры, чтобы можно было использовать общее корневое пространство имен:

get '/:id', to: 'articles#show', constraints: { id: /\d.+/ }
get '/:username', to: 'users#show'

3.9. Ограничения, основанные на запросе

Также можно ограничить маршрут, основываясь на любом методе в объекте Request, который возвращает String.

Ограничение, основанное на запросе, определяется так же, как и сегментное ограничение:

get 'photos', to: 'photos#index', constraints: { subdomain: 'admin' }

Также можно определить ограничения, используя блок constraints:

namespace :admin do
  constraints subdomain: 'admin' do
    resources :photos
  end
end

Ограничения запроса работают, вызывая метод на объекте Request с тем же именем, что и ключ хэша, а затем сравнивают возвращенное значение со значением хэша. Следовательно, значения ограничений должны соответствовать возвращаемому типу соответствующего метода объекта Request. Например: constraints: { subdomain: 'api' } будет соответствовать поддомену api, как и ожидалось, однако, использование символа constraints: { subdomain: :api } не будет, так как request.subdomain возвращает 'api' как строку.

Имеется исключения для ограничения format: так как это метод на объекте Request, это также неявный опциональный параметр для каждого пути. Ограничения сегмента имеют приоритет, и ограничение format применяется как есть, даже когда передано в хэше. Например, get 'foo', constraints: { format: 'json' } будет соответствовать GET /foo, так как формат опциональный по умолчанию. Однако, используя lambda, как в get 'foo', constraints: lambda { |req| req.format == :json }, маршрут будет соответствовать только явным запросам JSON.

3.10. Продвинутые ограничения

Если имеется более продвинутое ограничение, можете предоставить объект, отвечающий на matches?, который будет использовать Rails. Скажем, вы хотите направить всех пользователей через список ограничений в RestrictedListController. Можно сделать так:

class RestrictedListConstraint
  def initialize
    @ips = RestrictedList.retrieve_ips
  end

  def matches?(request)
    @ips.include?(request.remote_ip)
  end
end

Rails.application.routes.draw do
  get '*path', to: 'restricted_list#index',
    constraints: RestrictedListConstraint.new
end

Ограничения также можно определить как лямбду:

Rails.application.routes.draw do
  get '*path', to: 'restricted_list#index',
    constraints: lambda { |request| RestrictedList.retrieve_ips.include?(request.remote_ip) }
end

И метод matches?, и лямбда получают объект request в качестве аргумента.

3.10.1. Ограничения в блочной форме

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

class RestrictedListConstraint
  # ...То же, что и в предыдущем примере
end

Rails.application.routes.draw do
  constraints(RestrictedListConstraint.new) do
    get '*path', to: 'restricted_list#index'
    get '*other-path', to: 'other_restricted_list#index'
  end
end

Также можно использовать lambda:

Rails.application.routes.draw do
  constraints(lambda { |request| RestrictedList.retrieve_ips.include?(request.remote_ip) }) do
    get '*path', to: 'restricted_list#index'
    get '*other-path', to: 'other_restricted_list#index'
  end
end

3.11. Подстановка маршрутов и подстановочные сегменты

Подстановка маршрутов - это способ указать, что определенные параметры должны соответствовать остальным частям маршрута. Например:

get 'photos/*other', to: 'photos#unknown'

Этот маршрут будет соответствовать photos/12 или /photos/long/path/to/12, установив params[:other] как "12", или "long/path/to/12". Сегменты, начинающиеся со звездочки, называются "подстановочные сегменты" ("wildcard segments").

Подстановочные сегменты могут быть где угодно в маршруте. Например:

get 'books/*section/:title', to: 'books#show'

будет соответствовать books/some/section/last-words-a-memoir с params[:section] равным 'some/section', и params[:title] равным 'last-words-a-memoir'.

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

get '*a/foo/*b', to: 'test#index'

будет соответствовать zoo/woo/foo/bar/baz с params[:a] равным 'zoo/woo', и params[:b] равным 'bar/baz'.

Запросив '/foo/bar.json', ваш params[:pages] будет равен 'foo/bar' с форматом запроса JSON. Если вам нужно вернуть старое поведение 3.0.x, можете предоставить format: false вот так:

get '*pages', to: 'pages#show', format: false

Если хотите сделать сегмент формата обязательным, чтобы его нельзя было опустить, укажите format: true подобным образом:

get '*pages', to: 'pages#show', format: true

3.12. Перенаправление

Можно перенаправить любой путь на другой путь, используя хелпер redirect в вашем роутере:

get '/stories', to: redirect('/articles')

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

get '/stories/:name', to: redirect('/articles/%{name}')

Также можно предоставить блок для redirect, который получает символизированные параметры пути и объект request:

get '/stories/:name', to: redirect { |path_params, req| "/articles/#{path_params[:name].pluralize}" }
get '/stories', to: redirect { |path_params, req| "/articles/#{req.subdomain}" }

Пожалуйста, отметьте, что перенаправлением по умолчанию является 301 "Moved Permanently". Учтите, что некоторые браузеры или прокси серверы закэшируют этот тип перенаправления, сделав старые страницы недоступными. Чтобы изменить статус отклика, можно использовать опцию :status:

get '/stories/:name', to: redirect('/articles/%{name}', status: 302)

Во всех этих случаях, если не предоставить предшествующий хост (http://www.example.com), Rails возьмет эти детали из текущего запроса.

3.13. Роутинг к приложениям Rack

Вместо строки, подобной 'articles#index', соответствующей экшну index в ArticlesController, можно определить любое приложение Rack как конечную точку совпадения.

match '/application.js', to: MyRackApp, via: :all

Пока MyRackApp отвечает на call и возвращает [status, headers, body], роутер не будет различать приложение Rack и экшн. Здесь подходит использование via: :all, если вы хотите позволить своему приложению Rack обрабатывать все методы так, как оно посчитает нужным.

Для любопытства, 'articles#index' фактически расширяется до ArticlesController.action(:index), который возвращает валидное приложение Rack.

Так как proc/lambda это объекты, отвечающие на call, можно реализовывать очень простые маршруты (например, для проверки здоровья) в одну строку:
get '/health', to: ->(env) { [204, {}, ['']] }

Если вы указываете приложение Rack как конечную точку совпадения, помните что маршрут будет неизменным в принимающем приложении. Со следующим маршрутом ваше приложение Rack будет ожидать маршрут /admin:

match '/admin', to: AdminApp, via: :all

Если вы предпочитаете, чтобы ваше приложение Rack получало запросы на корневой путь, используйте вместо этого mount:

mount AdminApp, at: '/admin'

3.14. Использование root

Можно определить, с чем Rails должен связать '/' с помощью метода root:

root to: 'pages#main'
root 'pages#main' # то же самое в краткой форме

Следует поместить маршрут root в начало файла, поскольку это наиболее популярный маршрут и должен быть проверен первым.

Маршрут root связывает с экшном только запросы GET.

root также можно использовать внутри пространств имен и областей видимости. Например:

namespace :admin do
  root to: "admin#index"
end

root to: "home#index"

3.15. Маршруты с символами Unicode

Маршруты с символами unicode можно определять явно. Например:

get 'こんにちは', to: 'welcome#index'

3.16. Прямые маршруты

Можно создавать собственные хелперы URL напрямую, вызывая direct. Например:

direct :homepage do
  "https://rubyonrails.org"
end

# >> homepage_url
# => "https://rubyonrails.org"

Возвращаемое значение блока должно быть валидным аргументом для метода url_for. Таким образом, можно передать валидный строковый URL, хэш, массив, экземпляр Active Model или класс Active Model.

direct :commentable do |model|
  [ model, anchor: model.dom_id ]
end

direct :main do
  { controller: 'pages', action: 'index', subdomain: 'www' }
end

3.17. Использование resolve

Метод resolve позволяет настраивать полиморфное сопоставление моделей. Например:

resource :basket

resolve("Basket") { [:basket] }
<%= form_with model: @basket do |form| %>
  <!-- basket form -->
<% end %>

Это сгенерирует URL в единственном числе /basket вместо обычного /baskets/:id.

4. Настройка ресурсных маршрутов

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

4.1. Определение используемого контроллера

Опция :controller позволяет явно определить контроллер, используемый ресурсом. Например:

resources :photos, controller: 'images'

распознает входящие пути, начинающиеся с /photos, но маршрутизирует к контроллеру Images:

Метод HTTP Путь Контроллер#Экшн Именованный хелпер маршрута
GET /photos images#index photos_path
GET /photos/new images#new new_photo_path
POST /photos images#create photos_path
GET /photos/:id images#show photo_path(:id)
GET /photos/:id/edit images#edit edit_photo_path(:id)
PATCH/PUT /photos/:id images#update photo_path(:id)
DELETE /photos/:id images#destroy photo_path(:id)

Используйте photos_path, new_photo_path и т.д. для генерации путей для этого ресурса.

Для контроллеров в пространстве имен можно использовать нотацию директории. Например:

resources :user_permissions, controller: 'admin/user_permissions'

Это будет маршрутизировано на контроллер Admin::UserPermissions.

Поддерживается только нотация директории. Определение контроллера с помощью нотации константы Ruby (т.е. controller: 'Admin::UserPermissions') может привести к маршрутным проблемам, и в итоге к предупреждению.

4.2. Определение ограничений

Можно использовать опцию :constraints для определения требуемого формата на неявном id. Например:

resources :photos, constraints: { id: /[A-Z][A-Z][0-9]+/ }

Это объявление ограничивает параметр :id соответствием предоставленному регулярному выражению. Итак, в этом случае роутер больше не будет сопоставлять /photos/1 этому маршруту. Вместо этого он будет соответствовать /photos/RR27.

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

constraints(id: /[A-Z][A-Z][0-9]+/) do
  resources :photos
  resources :accounts
end

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

По умолчанию параметр :id не принимает точки - так как точка используется как разделитель для отформатированного маршрута. Если необходимо использовать точку в :id, добавьте ограничение, которое переопределит это - к примеру, id: /[^\/]+/ позволяет все, кроме слэша.

4.3. Переопределение именованных хелперов маршрута

Опция :as позволяет переопределить нормальное именование для именованных маршрутных хелперов. Например:

resources :photos, as: 'images'

распознает входящие пути, начинающиеся с /photos и маршрутизирует запросы к PhotosController, но использует значение опции :as для наименования хелпера:

Метод HTTP Путь Контроллер#Экшн Именованный хелпер маршрута
GET /photos photos#index images_path
GET /photos/new photos#new new_image_path
POST /photos photos#create images_path
GET /photos/:id photos#show image_path(:id)
GET /photos/:id/edit photos#edit edit_image_path(:id)
PATCH/PUT /photos/:id photos#update image_path(:id)
DELETE /photos/:id photos#destroy image_path(:id)

4.4. Переопределение сегментов new и edit

Опция :path_names позволяет переопределить автоматически генерируемые сегменты new и edit в путях, как тут:

resources :photos, path_names: { new: 'make', edit: 'change' }

Это приведет к тому, что роутинг распознает пути, такие как:

/photos/make
/photos/1/change

Фактические имена экшнов не меняются этой опцией. Два показанных пути все еще ведут к экшнам new и edit.

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

scope path_names: { new: 'make' } do
  # остальные ваши маршруты
end

4.5. Префикс именованных маршрутных хелперов

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

scope 'admin' do
  resources :photos, as: 'admin_photos'
end

resources :photos

Это предоставит маршрутные хелперы, такие как admin_photos_path, new_admin_photo_path и т.д. Это изменяет маршрутные хелперы для /admin/photos с photos_path, new_photos_path и т.д. на admin_photos_path, new_admin_photo_path и т.д. Без добавления as: 'admin_photos на resources :photos в области видимости, у resources :photos вне области видимости не будет каких-либо маршрутных хелперов.

Для задания префикса группы маршрутов, используйте :as со scope:

scope 'admin', as: 'admin' do
  resources :photos, :accounts
end

resources :photos, :accounts

Как и прежде, это изменит ресурсные хелперы для области видимости /admin на admin_photos_path and admin_accounts_path и позволяет ресурсам вне области видимости использовать photos_path и accounts_path.

Область видимости namespace автоматически добавляет :as, так же как и префиксы :module и :path.

4.5.1. Параметризованные пространства

Можно задать префикс маршрута именованным параметром так:

scope ':account_id', as: 'account', constraints: { account_id: /\d+/ } do
  resources :articles
end

Это предоставит пути, такие как /1/articles/9 и позволит обратиться к части пути account_id в контроллерах, хелперах и вью как params[:account_id].

Это также сгенерирует хелперы путей и URL, начинающиеся с account_, в которые можно передать ваши объекты:

account_article_path(@account, @article) # => /1/article/9
url_for([@account, @article])            # => /1/article/9
form_with(model: [@account, @article])   # => <form action="/1/article/9" ...>

Мы используем ограничения сегмента, чтобы ограничить пространство, соответствующее только строкам, похожим на ID. Это ограничение можно изменить, приспособив к вашим нуждам, или полностью пропустить. Опция :as также не строго обязательна, но без нее Rails вызовет ошибку при вычислении url_for([@account, @article]) или других хелперов, полагающихся на url_for, таких как form_with.

4.6. Ограничение создаваемых маршрутов

По умолчанию Rails создает маршруты для всех семи дефолтных экшнов (index, show, new, create, edit, update и destroy) для каждого маршрута RESTful вашего приложения. Можно использовать опции :only и :except для точной настройки этого поведения. Опция :only говорит Rails создать только определенные маршруты:

resources :photos, only: [:index, :show]

Теперь запрос GET к /photos будет успешным, а запрос POST к /photos (который обычно соединяется с экшном create) провалится.

Опция :except определяет маршрут или перечень маршрутов, который Rails не должен создавать:

resources :photos, except: :destroy

В этом случае Rails создаст все нормальные маршруты за исключением маршрута для destroy (запрос DELETE к /photos/:id).

Если в вашем приложении много маршрутов RESTful, использование :only и :except для генерации только тех маршрутов, которые Вам фактически нужны, позволит снизить использование памяти и ускорить процесс роутинга.

4.7. Переведенные пути

Используя scope, можно изменить имена путей, генерируемых с помощью resources:

scope(path_names: { new: 'neu', edit: 'bearbeiten' }) do
  resources :categories, path: 'kategorien'
end

Rails теперь создаст маршруты к CategoriesController.

Метод HTTP Путь Контроллер#Экшн Именованный хелпер маршрута
GET /kategorien categories#index categories_path
GET /kategorien/neu categories#new new_category_path
POST /kategorien categories#create categories_path
GET /kategorien/:id categories#show category_path(:id)
GET /kategorien/:id/bearbeiten categories#edit edit_category_path(:id)
PATCH/PUT /kategorien/:id categories#update category_path(:id)
DELETE /kategorien/:id categories#destroy category_path(:id)

4.8. Переопределение единственного числа

Если хотите переопределить единственное число ресурса, следует добавить дополнительные правила в инфлектор с помощью inflections:

ActiveSupport::Inflector.inflections do |inflect|
  inflect.irregular 'tooth', 'teeth'
end

4.9. Использование :as во вложенных ресурсах

Опция :as переопределяет автоматически генерируемое имя для ресурса в хелперах вложенного маршрута. Например:

resources :magazines do
  resources :ads, as: 'periodical_ads'
end

Это создаст маршрутные хелперы, такие как magazine_periodical_ads_url и edit_magazine_periodical_ad_path.

4.10. Переопределение параметров именованных маршрутов

Опция :param переопределяет дефолтный идентификатор ресурса :id (имя динамического сегмента, используемого для генерации маршрутов). К этому сегменту можно обратиться из контроллера с помощью params[<:param>].

resources :videos, param: :identifier
    videos GET  /videos(.:format)                  videos#index
           POST /videos(.:format)                  videos#create
 new_video GET  /videos/new(.:format)              videos#new
edit_video GET  /videos/:identifier/edit(.:format) videos#edit
Video.find_by(identifier: params[:identifier])

Можно переопределить ActiveRecord::Base#to_param связанной модели, чтобы создать URL:

class Video < ApplicationRecord
  def to_param
    identifier
  end
end
video = Video.find_by(identifier: "Roman-Holiday")
edit_video_path(video) # => "/videos/Roman-Holiday/edit"

5. Разделение очень большого маршрутного файла на несколько небольших

Если вы работаете в большом приложении с тысячами маршрутов, единственный файл config/routes.rb может стать громоздким и тяжелым для прочтения.

Rails предлагает способ разделения гигантского единого routes.rb на несколько небольших с помощью макроса draw.

У вас может быть маршрут admin.rb, который содержит все маршруты для административной области, другой файл api.rb для ресурсов API, и т.д.

# config/routes.rb

Rails.application.routes.draw do
  get 'foo', to: 'foo#bar'

  draw(:admin) # Загрузит другой маршрутный файл, расположенный в `config/routes/admin.rb`
end
# config/routes/admin.rb

namespace :admin do
  resources :comments
end

Вызов draw(:admin) в блоке Rails.application.routes.draw попытается загрузить маршрутный файл, по имени. заданному аргументом (в этом примере admin.rb). Файл должен быть расположен в директории config/routes или любой поддиректории (например, config/routes/admin.rb , config/routes/external/admin.rb).

Внутри маршрутного файла admin.rb можно использовать любой маршрутный DSL, но не следует оборачивать его в блок Rails.application.routes.draw, как это сделано в основном файле config/routes.rb.

5.1. Не используйте эту особенность, если вы не нуждаетесь в ней реально

Множество маршрутных файлов делает сложнее понятность и понимание. Для большинства приложений - даже с сотнями маршрутов - разработчикам проще иметь один маршрутный файл. DSL маршрутизации Rails уже предлагает способ разделения маршрутов организованным способом с помощью namespaces и scopes.

6. Осмотр и тестирование маршрутов

Rails предлагает инструменты для осмотра и тестирования маршрутов.

6.1. Список существующих маршрутов

Чтобы получить полный список всех доступных маршрутов вашего приложения, посетите http://localhost:3000/rails/info/routes в браузере, в то время как ваш сервер запущен в режиме development. Команда bin/rails routes, выполненная в терминале, выдаст тот же результат.

Оба метода напечатают все ваши маршруты, в том же порядке, что они появляются в config/routes.rb. Для каждого маршрута вы увидите:

  • Имя маршрута (если имеется)
  • Используемый метод HTTP (если маршрут реагирует не на все методы)
  • Шаблон URL
  • Параметры роутинга для этого маршрута

Например, вот небольшая часть результата команды bin/rails routes для маршрута RESTful:

    users GET    /users(.:format)          users#index
          POST   /users(.:format)          users#create
 new_user GET    /users/new(.:format)      users#new
edit_user GET    /users/:id/edit(.:format) users#edit

Также можно использовать опцию --expanded для включения режима расширенного табличного форматирования.

$ bin/rails routes --expanded

--[ Route 1 ]----------------------------------------------------
Prefix            | users
Verb              | GET
URI               | /users(.:format)
Controller#Action | users#index
--[ Route 2 ]----------------------------------------------------
Prefix            |
Verb              | POST
URI               | /users(.:format)
Controller#Action | users#create
--[ Route 3 ]----------------------------------------------------
Prefix            | new_user
Verb              | GET
URI               | /users/new(.:format)
Controller#Action | users#new
--[ Route 4 ]----------------------------------------------------
Prefix            | edit_user
Verb              | GET
URI               | /users/:id/edit(.:format)
Controller#Action | users#edit

Можно искать маршруты с помощью опции grep: -g. Это выведет любые маршруты, которые частично соответствуют по имени метода хелпера URL, метода HTTP или пути URL.

$ bin/rails routes -g new_comment
$ bin/rails routes -g POST
$ bin/rails routes -g admin

Если хотите просмотреть маршруты, ведущие на определенный контроллер, имеется опция -c.

$ bin/rails routes -c users
$ bin/rails routes -c admin/users
$ bin/rails routes -c Comments
$ bin/rails routes -c Articles::CommentsController

Результат команды bin/rails routes более читаемый, если у вас в окне терминала прокрутка, а не перенос строчек.

6.2. Тестирование маршрутов

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

6.2.1. Оператор контроля assert_generates

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

assert_generates '/photos/1', { controller: 'photos', action: 'show', id: '1' }
assert_generates '/about', controller: 'pages', action: 'about'
6.2.2. Оператор контроля assert_recognizes

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

assert_recognizes({ controller: 'photos', action: 'show', id: '1' }, '/photos/1')

Можете задать аргумент :method, чтобы определить метод HTTP:

assert_recognizes({ controller: 'photos', action: 'create' }, { path: 'photos', method: :post })
6.2.3. Оператор контроля assert_routing

Оператор контроля assert_routing проверяет маршрут с двух сторон: он тестирует, что путь создает опции, и что опции создают путь. Таким образом, он комбинирует функции assert_generates и assert_recognizes:

assert_routing({ path: 'photos', method: :post }, { controller: 'photos', action: 'create' })