Routes. The Beginning

    Роуты в рельсах очень важная вещь. Но до поры до времени можно даже не обращать внимание на них. Особенно если вы пользуетесь командой scaffold, которая автоматически все прописывает. Но в какой-то момент появляется необходимость создавать нестандартные роуты. Это значит самое время залезать в файл routes.rb в папке config вашего проекта.

    Что такое роуты


    Роуты — это система маршрутов (путей, url'ов) на вашем сайте. Благодаря роутам мы можем иметь красивые и ясные для пользователей ссылки. Введя ссылку вроде mysite.ru/articles/2008/november/13 мы получим все статьи за 13 ноября 2008 года, а по ссылке mysite.ru/shop/shoes получим каталог обуви из вашего магазина. При всем при этом, структура каталогов сайта никак не изменяется. В любой момент мы можем изменить роуты не трогая расположение самих файлов. Но чтобы все это работало нам необходимо настроить роуты.

    К практике


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

    rails routes
    cd routes


    Окей. Проект создан и мы вошли в рабочую папку. Сразу набросимся на роуты:

    rake routes

    эта команда вам выдаст две строчки стандартных роутов.
    /:controller/:action/:id
    /:controller/:action/:id.:format


    Это значит, что любой урл сейчас будет парситься по этим двум правилам.
    :controller — это Контроллер =). Это компонент MVC, который чаще всего выступает как посредник между Представлением (HTML) и Моделью (база данных, скажем). Дальше будет яснее, но скорее всего вы итак знаете, что это такое.
    :action — это вызываемый метод контроллера. У контроллера обычно много методов.
    :id — если я вно не указывать запрет на создание id, то по умолчанию любая модель (таблица БД) создается с полем id. Поэтому любой элемент модели имеет id. И когда вы хотите удалить/редактировать/что угодно делать с каким-то конкретным элементом модели вы обязаны передать в контроллер этот самый id.

    Окей. Давайте мы создадим новостной журнал. Для этого нам нужны:
    — Таблица news в нашей базе данных (Модель). В БД мы будем хранить заголовок статьи (title), автора статьи (author) и собственно саму статью (article)
    — Набор методов для работы с БД (Контроллер)
    — HTML формы для ввода, редактирования, чтения новостей (Представление)
    Мы можем создавать все это по отдельности. Но сейчас мы упростим себе задачу и используем функцию scaffold для генерации пачки готовых файлов.

    ./script/generate scaffold Magazine title:string author:string article:text

    Мы только что создали все выше перечисленное (а также Helpers, о которых как-нибудь в другой раз). Также команда scaffold сама создала необходимые роуты. Еще раз наберите команду rake routes и вывалится кипа новых роутов
    
    magazines		              GET      /magazines                       {:controller=>"magazines", :action=>"index"}
    formatted_magazines	      GET      /magazines.:format               {:controller=>"magazines", :action=>"index"}
    			                      POST    /magazines                       {:controller=>"magazines", :action=>"create"}
    			                      POST    /magazines.:format               {:controller=>"magazines", :action=>"create"}
    new_magazine		      GET       /magazines/new                   {:controller=>"magazines", :action=>"new"}
    formatted_new_magazine    GET       /magazines/new.:format           {:controller=>"magazines", :action=>"new"}
    edit_magazine		              GET       /magazines/:id/edit              {:controller=>"magazines", :action=>"edit"}
    formatted_edit_magazine     GET       /magazines/:id/edit.:format      {:controller=>"magazines", :action=>"edit"}
    magazine		                      GET       /magazines/:id                   {:controller=>"magazines", :action=>"show"}
    formatted_magazine	      GET       /magazines/:id.:format           {:controller=>"magazines", :action=>"show"}
    			                      PUT       /magazines/:id                   {:controller=>"magazines", :action=>"update"}
    			                      PUT       /magazines/:id.:format           {:controller=>"magazines", :action=>"update"}
    			                      DELETE /magazines/:id                   {:controller=>"magazines", :action=>"destroy"}
    			                      DELETE /magazines/:id.:format           {:controller=>"magazines", :action=>"destroy"}
    

    Теперь мы запустим сервер и поиграемся с журналом. Но сперва мы создадим нашу базу данных и запустим миграции.

    rake db:create
    rake db:migrate
    ./script/server


    Наш журнал теперь доступен по адресу localhost:3000/magazines
    Создайте пару новых статей.

    Вернемся к таблице роутов выше. Первый столбец — это именные роуты. Они очень удобны. Есть несколько вариантов сейчас сделать ссылку на создание новой статьи. Откройте файл app/views/magazines/index.html.erb — это представление для метода index в контроллере magazines_controller.
    В самом низу давайте допишем немного кода.

    <hr>
    <%= link_to 'Новая статья', :action => 'new' %><br />
    <%= link_to 'Новая статья', '/magazines/new' %><br />
    <%= link_to 'Новая статья', new_magazine_path %><br />
    <%= link_to 'Новая статья', new_magazine_url %><br />


    Самым правильным будет использование последних двух методов. Разница в том, что url возвращает полную ссылку (http://localhost:3000/magazines/new), а path только путь (/magazines/new). Почему лучше пользоваться именными роутами? Именной роут это переменная, изменив которую вы меняете все ссылки, которые пользуются этим роутом. Писать пути от руки вобще не рекомендуется, если приспичило, то лучше написать :action => 'new' (зачастую именных роутов на все случаи жизне не хватает, поэтому этот вариант очень распространен).

    Второй столбец таблицы — это метод запроса. Одна и таже ссылка, но с разным методом ведет на разные методы контроллера. К примеру, в том же app/views/magazines/index.html.erb:
    <%= link_to 'Show', magazine %>
    <%= link_to 'Destroy', magazine, :method => :delete %>

    В первой ссылке исполняется дефолтный GET запрос (можно не указывать в ссылке :method=>:get), а во второй отправляется метод :delete. А ссылка magazine остается в обоих случаях одинаковая.

    Третий столюбец — это собственно ссылки, которые мы получим в HTML. Последний столбец — это соответствие ссылок контроллеру и методу. Как уже выше писалось, любую ссылку можно представить в виде пары :controller, :action (ну и иногда :method).

    <%= link_to 'Ссылка на другой контроллер из контроллера magazines', :controller => 'blogs', :action=>'show', :id=>'1', :method=>'GET' %>
    Так мы получим ссылку на блог с индексом 1. (Тут метод :get можно было и не указывать)
    <%= link_to 'Ссылка на родной метод контроллера', :action => 'show', :id => '1' %>
    Ссылка на статью с индексом 1. Контроллер и HTTP метод в данном случае указывать не надо, так как GET исполняется по умолчанию, а контроллер, если не указан, выполняется тот же.

    Тепрь откройте файл config/routes.rb (можете удалить весь закомментированный текст)

    map.resources :magazines
    map.connect ':controller/:action/:id'
    map.connect ':controller/:action/:id.:format'


    Первую строчку вставила команда scaffold. Эта строчка и добавила нам пачку роутов, которую мы наблюдали выше.

    Если вы сейчас наберете просто localhost:3000 вы попадете на приветственную страницу. Давайте это исправим.

    map.root :controller => 'magazines'

    Теперь из папки public удалите index.html и зайдя на localhost:3000 вы попадете напрямую куда надо =). Кроме того если вы просмотрите все роуты занова (rake routes), то увидите новый именной роут root. И в меню сможете сделать ссылку на «Главную» вида:

    <%= link_to 'Главная', root %>

    И вы всегда без ущерба ссылкам сможете изменить домашнюю страницу, скажем, на ваш магазин map.root :controller => 'shop'

    II уровень


    Собственно создав root вы создали первый именной роут своими руками.
    Давайте создадим именной роут «localhost:3000/zhurnal». Не хотим мы буржуйский 'magazines', хотим 'zhurnal'!

    map.zhurnal '/klevi_zhurnal/:id', :controller => 'magazines', :id => nil

    Итак, мы создали именной роут zhurnal, урл которого будет выглядеть как localhost:3000/klevi_zhurnal, а контент он будет получать от контроллера magazines. Если мы попробуем прочесть статью теперь вроде localhost:3000/klevi_zhurnal/1 — то мы обламаемся. Внесем в наш роут немного изменений:

    map.zhurnal '/klevi_zhurnal/:action/:id', :controller => 'magazines', :action => 'index', :id => nil

    Что все это значит:
    — урл вида /klevi_zhurnal/ будет отработан :controller => 'magazines', :action => 'index', :id => 'nil' — то есть мы получим индексовую страницу (index.html.erb)
    — /klevi_zhurnal/1 выплюнет ошибку, что action '1' не существует (посмотрите на последовательность передачи аргумента в роуте)
    — /klevi_zhurnal/show скажет, что ID не указано
    — /klevi_zhurnal/show/1 — выдаст вам статью с ID=1 (если она конечно существует)
    — /klevi_zhurnal/edit/1 — выдаст форму редактирования этой статьи

    Правда теперь несколько тяжелее выглядят сами ссылки:
    Вместо <%= link_to 'Все статьи', magazines_path %> будет <%= link_to 'Все статьи', zhurnal_path %>
    Вместо <%= link_to 'Статья номер 1', magazine_path('1') %> будет <%= link_to 'Статья номер 1', zhurnal_path(:action=>'show', :id=>'1') %>

    Обратите внимание на то, что для лучшего понимания роутов введена система множественного/единственного числа:
    показать все статьи magazines_path,
    показать отдельную статью: magazine_path.
    Чорт — не самое правильное слово вообще выбрал =). Если бы у нас все называлось Article:
    index => articles_path, show => article_path(:id)

    Теперь давайте создадим новый метод.
    Откройте app/controllers/magazines_controller.rb
    Добавьте туда метод

    def random
    offset = rand(Magazine.count :all)
    @magazine = Magazine.find(:first, :offset => offset)
    render :action => 'show'
    end


    Этот метод просто возвращает рандомно статью. Давайте попробуем его вызвать: localhost:3000/magazines/random
    Получаем ошибку — требует от нас ID. Почему? Потому что стандартный роут продразумевает роут вида :controller/:action/:id.
    Давайте попробуем вызвать роут по правилам:
    localhost:3000/magazines/random/1230492
    Записи с таким ID не существует — но все работает! Так как мы в нашем методе не используем ID вообще — то для нас и не принципиально какую ерунду мы там напишем.
    Давайте теперь все же попробуем сделать корректный роут вида localhost:3000/magazines/random/
    Для этого существует опция :collection => { :action => :HTTP_method }
    Наш :action это :random, метод — :get
    получаем

    map.resources :magazines, :collection => { :random => :get }

    теперь все работает! =)

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

    API для желающих
    Скринкасты Райана Бейтса
    Поделиться публикацией

    Комментарии 34

      +1
      Спасибо за статью. В рунете до сих сложно найти хорошие статьи о Ruby on Rails для новичков. Жду продолжения :)
        +1
        За то наши западные коллеги изобилуют. За целый день устаешь от английского +)
        +1
        Спасибо, очень полезно для новичков.
          0
          Скомканно. Нет объяснений по какому принципу редактируются роуты.

          Например, что мы сделали с последним — я вообще не понял.
            0
            Ближе к концу я начал уже торопиться, ибо рабочее время поджимало — поэтому начал суетиться.

            Про последнее.
            Стандартные роуты (map.resources) прописывают автоматически только один роут без ID (index), а все остальные методы по умолчанию требуют ID (:controller/:action/:id). но часто бывает, что создается метод, который «сам по себе» (в нашем случае это метод random). Для этого метода автоматически прописывается роут вида magazines/random/ID. При этом какой ID не впихни — это никак не повлияет (потому что он просто там никак не используется). Можно, конечно, так и оставить, но это не красиво. Поэтому я привел решение, как такие коллизии решаются. То есть как привести роут к виду :controller/:action (magazines/random). Собственно добавлением опции :collection = { :action => :HTML_method }.
              +1
              Круто. Собственно, зачем мы это делали и так было ясно, не ясно было — как.

              Предлагаю добавить в статью пару предложений про опцию :collection.
                0
                Внес небольшую правку.
                Более основательно описывать опции и виды роутов я думаю в следующей статье.
                  0
                  тогда если уж говорить про collection, то надо и member упомянуть. Но это думаю уже в следующей статье =)
                    0
                    Именно это я и озвучил вроде)
            +1
            Я бы не называл моделью базу данных — все таки уровни доступа к данным и уровень бизнес-логики — разные вещи.
              +1
              Вы устрашающе правы, но это малоактуально =). Думаю вы понимаете о чем я. Тут много каких обобщений которые имеют исключительно фигуральную ценность.
                +1
                Согласен. Я то все прекрасно понял. Просто те, для кого эта тема новая, могут в итоге ассоциировать эти понятия и в итоге неправильно трактовать паттерн в дальнейшем.

                Думаю, раз речь идет об MVC, можно сделать небольшое отступление, где кратко описать роль каждого из компонентов (M, V и C) и что они представляют, а в основном тексте уже использовать просто «Модель».

                Ибо для общего понимания важны все детали, даже те, которые, казалось бы, в целом не имеют прямого отношения к делу.
                  +1
                  Ну, просвещения ради я там ссылку на MVC дал )
                  Вообще это тема другой беседы. И я, честно говоря, не стал бы браться за разъяснение паттернов. Для меня это нечто на уровне ощущений. А еще я никогда себя не корю за сложную выборку из контроллера -). МВЦ все же нечто из разряда «идеального» Платона.
                    +1
                    Ну да, ссылки вполне достаточно :)
                    Не, паттерны для меня после участия в масштабных проектах — как раз не на уровне ощущений, а самая что ни на есть практика.
                    Все конечно зависит от конкретного проекта, но в крупном проекте, который придется поддерживать в длительной перспективе, развивать и переписывать со временем, я бы, как руководитель, навалял бы люлей за сложную выборку в контроллере :)) Впрочем, опять же зависит от того, какой и где MVC использовать.
                    Хотя, я говорю про ASP.NET MVC, который мы использовали, и он по концепции является абсолютным аналогом MVC в RoR. Так что люлей не избежать :)
                      0
                      Вот же нарвался)
              +1
              Для полноты картины нужна вторая часть, с map.root, :shallow, вложенными роутерами и :only, :except.
                0
                Да-да. Продолжение будет)
                0
                возможно ли обойти следующее ограничение и есть ли оно вообще?
                /home/:routeName будет /home/чтотоName

                  0
                  хм. если бы как-то поподробнее. Что здесь? )
                    0
                    ну смотри, есть начало роута: но нет конца
                    те конструкции типа :routetest можно воспринимать как роут :route и строку test или как один роут :routetest
                      0
                      :blabla — это символ, он никак не делится, поэтому конструкция типа :routetest — будет восприниматься только как символ :routetest =)
                      но можно сделать такой роут (что будет идентично):
                      map.test '/HOME/:action:test', :controller => 'magazines', :action => 'index', :test=>'test'
                      если вызвать роут test.path получим /HOME/indextest
                        0
                        какие интересные у вас костыли ;)
                        спасибо за ответ
                          0
                          какая задача — такие и костыли =)
                        –1
                        Есть вариант сделать кетч-алл роут типа такого:
                        map.connect '*path', :controller => 'redirect', :action => 'index'
                        и уже потом делать отбор в контроллере того, что надо.
                        Вот рельсокаст на эту тему:
                        railscasts.com/episodes/46-catch-all-route
                    0
                    Спасибо за статью.
                    Знающие люди, подскажите пожалуйста, насколько легально (с идеологической точки зрения) делать так?:

                    Есть двухуровневое дерево категорий. У модели Category есть атрибут nice_url, который определяется из title'a категории путём выкидывания пробелов и других символов «плохих» для url. Известно, что категорий не много и меняются они нечасто.

                    В routes.rb я пишу:

                    Category.find(:all, :conditions => {parent_id => nil}).each do |category|
                    category.children.each do |subcategory|
                    map.connect "#{category.nice_url}/#{subcategory.nice_url}", :controller => 'articles', :category_id => subcategory.id
                    end
                    map.connect "#{category.nice_url}", :controller => 'articles', :category_id => category.id
                    end

                    таким образом я получаю для всего дерева категорий роуты вида:
                    map.connect «science/biology», :controller => 'articles', :category_id => 31
                    map.connect «science/math», :controller => 'articles', :category_id => 24
                    map.connect «science», :controller => 'articles', :category_id => 54
                    map.connect «programming/php», :controller => 'articles', :category_id => 23
                    map.connect «programming/cpp», :controller => 'articles', :category_id => 11
                    map.connect «programming/ruby», :controller => 'articles', :category_id => 26
                    map.connect «programming», :controller => 'articles', :category_id => 87


                    Для верности в модели Category добавляю обработчик after_save, который перегружает роуты (вызывается нечасто)
                    ActionController::Routing::Routes.reload!

                    Опять же вопрос. Насколько валидно так делать? Как сделать роуты вида «category/subcategory/article», при условии, что статей, в отличии от категорий много, а часто перегружать все роуты — не вариант?

                    ЗЫ. Про Route globbing [map.connect '*path', :controller => 'articles', :action => 'unrecognized?'] я знаю, и надеюсь, что есть другой способ, без ручного разбора url, а самое главное без последующей ручной, его генерации.

                    ЗЗЫ. Хотя это коммент-вопрос, возможно кому-то такой подход к динамическим роутам будет интересен/полезен. Во второй версии рельсов это работало без проблем.

                      0
                      А так не пробовали

                      #routes.rb
                      map.connect ':parent/:child/:action/:id', :controller => 'articles', :action => 'index', :id => 'nil'

                      #articles_controller.rb
                      parent = Category.find_by_title(params['parent'])
                      child = Category.find_by_title(params['child'], :conditions => ['parent = ?', parent.id])

                      Как-то так, нет?
                        +1
                        Спасибо, но это немного не то… так мне в контроллере нужно найти парента по nice_url, убедиться, что у него есть чайлд, с соответствующим nice_url, а потом получить id чайлда и выбрать нужные данные.

                        Аналогично при вызове функции url_for или link_to мне нужно иметь информацию о чайлде и паренте (если он есть… это тоже надо проверять).

                        В описаном мной подходе нужно всего лишь написать url_for(:controller=>'articles', :category_id=>26) и мне неважно на каком уровне вложенности лежит категория с id=26. Ц меня автоматом подхватится роут
                        map.connect «programming/ruby», :controller => 'articles', :category_id => 26
                        и вернётся ссылка «programming/ruby»
                          0
                          Очень специфический подход ;)
                          Попробуйте использовать символы в таком случае
                          К примеру, вместо
                          map.connect "#{category.nice_url}/#{subcategory.nice_url}", :controller => 'articles', :category_id => subcategory.id
                          map.connect "#{category.nice_url}", :controller => 'articles', :category_id => category.id
                          попробуйте так
                          map.connect ":parent/:child", :controller => 'articles', :category_id => subcategory.id, :parent => '#{category.nice_url}', :child => '#{subcategory.nice_url}'
                          map.connect ":parent", :controller => 'articles', :category_id => category.id, :parent => '#{category.nice_url}'

                          Тогда в котроллере вы сможете пользоваться параметрами GET через params['parent'] и params['child'], которые вернут те самые nice_url
                        0
                        ехх… google to_param и всё будет очень правильно.
                        0
                        Напишите что это Rails, а не весь Руби — Ruby is not Rails ;). Ещё же есть Merb, там другой API у route (например, опциональные параметры можно задать без регулярных выражений:
                        map('(/:lang)/:article(.:format)').to(:controller => :article, :action => :show).name(:article)
                        А, например, в Ramaze роутеры строятся динамически на основе имён класса и методов.
                          0
                          Вы про блог? Блога по рельсам нет. А вообще из статьи ясно, что речь о рельсах, а не руби =)
                          +1
                          И, кстати, более правильно использовать RESTfull роутеры:
                          resource :post
                          создаст сразу роутеры:
                          GET post/ — просмотр всех постов
                          PUT post/ — добавление ного
                          GET post/:id — просмотр конкретного поста
                          GET post/:id/edit — страница редактирования
                          POST post/:id — изменение поста
                          DELETE post/:id — удаление поста
                            0
                            Ну да, про рестфул я еще отдельно написать хочу. Это все же целая идеология ).
                            Роуты это все же не роутеры =)
                            0
                            Может не в тему, но вот прикольная штучка для работы с субдоменами subdomain-fu
                            в роутах пишем:
                            map.connect '*path', :controller => 'users', :action => 'show', :conditions => { :foreign_domain => true }

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

                            Самое читаемое