Создание и настройка генераторов и шаблонов Rails

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

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

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

1. Первый контакт

При создании приложения с помощью команды rails фактически вы используете генератор Rails. После этого можно получить список всех доступных генераторов, просто вызвав bin/rails generate:

$ rails new myapp
$ cd myapp
$ bin/rails generate

Чтобы создать новое приложение rails, мы используем глобальную команду rails, которую установил гем rails с помощью gem install rails. Когда внутри директории вашего приложения, мы используем команду bin/rails, которая использует комплект rails этого приложения.

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

$ bin/rails generate helper --help

2. Создание своего генератора

Начиная с Rails 3.0, генераторы создаются на основе Thor. Thor представляет мощные опции для парсинга и великолепный API для взаимодействия с файлами. Например, давайте создадим генератор, создающий файл инициализатора с именем initializer.rb внутри config/initializers.

Первым шагом является создание файла lib/generators/initializer_generator.rb со следующим содержимым:

class InitializerGenerator < Rails::Generators::Base
  def create_initializer_file
    create_file "config/initializers/initializer.rb", "# Add initialization content here"
  end
end

create_file - это метод, представленный Thor::Actions. Документация по create_file и другие методы Thor находятся в документации по Thor

Наш новый генератор очень прост: он наследуется от Rails::Generators::Base и содержит одно определение метода. Когда генератор вызывается, каждый публичный метод в генераторе выполняется в порядке, в котором он определен. Наконец, мы вызываем метод create_file, который создаст файл в указанном месте с заданным содержимым. Если вы знакомы с Rails Application Templates API, API генераторов покажется вам очень знакомым.

Чтобы вызвать наш новый генератор, нужно всего лишь выполнить:

$ bin/rails generate initializer

Перед тем, как продолжить, давайте посмотрим на описание нашего нового генератора:

$ bin/rails generate initializer --help

Rails обычно способен генерировать хорошие описания, если генератор расположен в пространствах имен, таких как ActiveRecord::Generators::ModelGenerator, но не в этом частном случае. Эту проблему можно решить двумя способами. Первым является вызов desc внутри нашего генератора:

class InitializerGenerator < Rails::Generators::Base
  desc "This generator creates an initializer file at config/initializers"
  def create_initializer_file
    create_file "config/initializers/initializer.rb", "# Add initialization content here"
  end
end

Теперь можно просмотреть новое описание, вызвав --help на новом генераторе. Вторым способом является добавление описания в файле USAGE в той же директории, что и наш генератор. Мы это сделаем на следующем этапе.

3. Создание генераторов с помощью генераторов

У самих генераторов есть генератор:

$ bin/rails generate generator initializer
      create  lib/generators/initializer
      create  lib/generators/initializer/initializer_generator.rb
      create  lib/generators/initializer/USAGE
      create  lib/generators/initializer/templates
      invoke  test_unit
      create    test/lib/generators/initializer_generator_test.rb

Вот только что созданный генератор:

class InitializerGenerator < Rails::Generators::NamedBase
  source_root File.expand_path('templates', __dir__)
end

Сперва обратите внимание, что он унаследован от Rails::Generators::NamedBase вместо Rails::Generators::Base. Это означает, что наш генератор ожидает как минимум один аргумент, который будет именем инициализатора и будет доступным в нашем коде в переменной name.

Это можно увидеть, если вызвать описание для генератора (не забудьте удалить файл старого генератора):

$ bin/rails generate initializer --help
Usage:
  bin/rails generate initializer NAME [options]

Также можно увидеть, что в нашем новом генераторе есть метод класса source_root. Этот метод указывает на место расположения шаблонов нашего генератора, если таковые имеются, и по умолчанию он указывает на созданную директорию lib/generators/initializer/templates.

Чтобы понять, что такое шаблон генератора, давайте создадим файл lib/generators/initializer/templates/initializer.rb со следующим содержимым:

# Add initialization content here

А теперь изменим генератор, чтобы он копировал этот файл при вызове:

class InitializerGenerator < Rails::Generators::NamedBase
  source_root File.expand_path('templates', __dir__)

  def copy_initializer_file
    copy_file "initializer.rb", "config/initializers/#{file_name}.rb"
  end
end

И выполним наш генератор:

$ bin/rails generate initializer core_extensions

Теперь мы видим, что инициализатор с именем core_extensions был создан в config/initializers/core_extensions.rb с содержимым нашего шаблона. Это означает, что copy_file копирует файл из корневой директории исходников в заданный путь назначения. Метод file_name автоматически создается, когда мы наследуем от Rails::Generators::NamedBase.

Доступные для генераторов методы раскрываются в последнем разделе этого руководства.

4. Поиск генераторов

При запуске bin/rails generate initializer core_extensions Rails затребует эти файлы в следующем порядке, пока один из них не будет найден:

rails/generators/initializer/initializer_generator.rb
generators/initializer/initializer_generator.rb
rails/generators/initializer_generator.rb
generators/initializer_generator.rb

Если ни один не найден, вы получите сообщение об ошибке.

Вышеуказанный пример положит файлы в папку lib приложения, поскольку сказано, что эта директория принадлежит $LOAD_PATH.

5. Настройка рабочего процесса

Собственные генераторы Rails достаточно гибки, чтобы позволить вам настроить скаффолд. Они могут быть настроены в config/application.rb, вот несколько настроек по умолчанию:

config.generators do |g|
  g.orm             :active_record
  g.template_engine :erb
  g.test_framework  :test_unit, fixture: true
end

Так как мы настраиваем наш рабочий процесс, давайте сперва посмотрим, как выглядит наш скаффолд:

$ bin/rails generate scaffold User name:string
      invoke  active_record
      create    db/migrate/20130924151154_create_users.rb
      create    app/models/user.rb
      invoke    test_unit
      create      test/models/user_test.rb
      create      test/fixtures/users.yml
      invoke  resource_route
       route    resources :users
      invoke  scaffold_controller
      create    app/controllers/users_controller.rb
      invoke    erb
      create      app/views/users
      create      app/views/users/index.html.erb
      create      app/views/users/edit.html.erb
      create      app/views/users/show.html.erb
      create      app/views/users/new.html.erb
      create      app/views/users/_form.html.erb
      invoke    test_unit
      create      test/controllers/users_controller_test.rb
      invoke    helper
      create      app/helpers/users_helper.rb
      invoke    jbuilder
      create      app/views/users/index.json.jbuilder
      create      app/views/users/show.json.jbuilder
      invoke  test_unit
      create    test/application_system_test_case.rb
      create    test/system/users_test.rb

Глядя на этот вывод, легко понять, как работают генераторы в Rails 3.0 и выше. Генератор скаффолда фактически не генерирует ничего, он просто вызывает другие. Это позволяет нам добавить/заменить/убрать любые из этих вызовов. Например, генератор скаффолда вызывает генератор scaffold_controller, который вызывает генераторы erb, test_unit и helper. Поскольку у каждого генератора одна функция, их просто использовать повторно, избегая дублирования кода.

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

config.generators do |g|
  g.orm             :active_record
  g.template_engine :erb
  g.test_framework  :test_unit, fixture: false
end

Если мы сгенерируем другой ресурс с помощью генератора скаффолда, мы увидим, что ни таблица стилей, ни JavaScript, ни фикстуры более не будут созданы. Если мы захотим настраивать его дальше, например использовать DataMapper и RSpec вместо Active Record и TestUnit, это достигается всего лишь добавлением соответствующих гемов в приложение и настройкой ваших генераторов.

Для демонстрации мы собираемся создать новый генератор хелперов, который просто добавляет несколько методов-ридеров для переменных экземпляра. Сначала мы создадим генератор в пространстве имен rails, так как тут rails ищет генераторы, используемые как хуки:

$ bin/rails generate generator rails/my_helper
      create  lib/generators/rails/my_helper
      create  lib/generators/rails/my_helper/my_helper_generator.rb
      create  lib/generators/rails/my_helper/USAGE
      create  lib/generators/rails/my_helper/templates
      invoke  test_unit
      create    test/lib/generators/rails/my_helper_generator_test.rb

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

$ bin/rails generate my_helper products
      create  app/helpers/products_helper.rb

И следующий хелпер будет сгенерирован в app/helpers:

module ProductsHelper
  attr_reader :products, :product
end

Что, собственно, и ожидалось. Можно сообщить скаффолду использовать наш новый генератор хелпера, снова отредактировав config/application.rb:

config.generators do |g|
  g.orm             :active_record
  g.template_engine :erb
  g.test_framework  :test_unit, fixture: false
  g.helper          :my_helper
end

и увидев его в действии при вызове генератора:

$ bin/rails generate scaffold Article body:text
      [...]
      invoke    my_helper
      create      app/helpers/articles_helper.rb

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

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

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

# lib/generators/rails/my_helper/my_helper_generator.rb
class Rails::MyHelperGenerator < Rails::Generators::NamedBase
  def create_helper_file
    create_file "app/helpers/#{file_name}_helper.rb", <<-FILE
module #{class_name}Helper
  attr_reader :#{plural_name}, :#{plural_name.singularize}
end
    FILE
  end

  hook_for :test_framework
end

Теперь, когда вызывается генератор хелпера, и как тестовый фреймворк настроен TestUnit, он попытается вызвать Rails::TestUnitGenerator и TestUnit::MyHelperGenerator. Поскольку ни один из них не определен, можно сообщить нашему генератору вместо них вызывать TestUnit::Generators::HelperGenerator, который определен, так как это генератор Rails. Для этого нужно всего лишь добавить:

# Search for :helper instead of :my_helper
hook_for :test_framework, as: :helper

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

6. Настройка рабочего процесса, изменяя шаблоны генераторов

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

В Rails 3.0 и выше генераторы не просто ищут шаблоны в корневом пути, они также ищут по другим путям. И одно из них — lib/templates. Поскольку мы хотим изменить Rails::Generators::HelperGenerator, можно это осуществить, просто сделав копию шаблона в lib/templates/rails/helper с именем helper.rb. Так давайте же создадим этот файл со следующим содержимым:

module <%= class_name %>Helper
  attr_reader :<%= plural_name %>, :<%= plural_name.singularize %>
end

и отменим последнее изменение в config/application.rb:

config.generators do |g|
  g.orm             :active_record
  g.template_engine :erb
  g.test_framework  :test_unit, fixture: false
  g.stylesheets     false
end

Теперь, если сгенерируете другой ресурс, то увидите похожий результат!

Еще одним обычным использованием пользовательских шаблонов является переопределение шаблонов скаффолда вью по умолчанию. Любой из них можно переопределить, создав соответствующий файл (например, index.html.erb, show.html.erb, и т.д.) в lib/templates/erb/scaffold.

Шаблоны скаффолда в Rails часто используют теги ERB; эти теги необходимо экранировать, чтобы сгенерированный результат являлся валидным кодом ERB.

Например, в шаблоне необходим следующий экранированный тег ERB (обратите внимание на дополнительный %)...

<%%= stylesheet_link_tag :application %>

...чтобы сгенерировать следующий результат:

<%= stylesheet_link_tag :application %>

7. Добавление фолбэков генераторов

Еще одна особенность генераторов, которая очень полезна, это фолбэки. Например, представим, что вы хотите добавить особенность над TestUnit, такую как shoulda. Так как TestUnit уже реализует все генераторы, требуемые Rails, а shoulda всего лишь хочет переопределить часть из них, нет необходимости для shoulda переопределять некоторые генераторы, она может просто сообщить Rails использовать генератор TestUnit, если такой не найден в пространстве имен Shoulda.

Можно с легкостью смоделировать это поведение, снова изменив наш config/application.rb:

config.generators do |g|
  g.orm             :active_record
  g.template_engine :erb
  g.test_framework  :shoulda, fixture: false

  # Добавим фолбэк!
  g.fallbacks[:shoulda] = :test_unit
end

Теперь, если создать скаффолд Comment, вы увидите, что были вызваны генераторы shoulda, но в итоге они всего лишь переуступили генераторам TestUnit:

$ bin/rails generate scaffold Comment body:text
      invoke  active_record
      create    db/migrate/20130924143118_create_comments.rb
      create    app/models/comment.rb
      invoke    shoulda
      create      test/models/comment_test.rb
      create      test/fixtures/comments.yml
      invoke  resource_route
       route    resources :comments
      invoke  scaffold_controller
      create    app/controllers/comments_controller.rb
      invoke    erb
      create      app/views/comments
      create      app/views/comments/index.html.erb
      create      app/views/comments/edit.html.erb
      create      app/views/comments/show.html.erb
      create      app/views/comments/new.html.erb
      create      app/views/comments/_form.html.erb
      invoke    my_helper
      create      app/helpers/comments_helper.rb
      invoke      shoulda
      create        test/helpers/comments_helper_test.rb
      invoke    jbuilder
      create      app/views/comments/index.json.jbuilder
      create      app/views/comments/show.json.jbuilder
      invoke  test_unit
      create    test/application_system_test_case.rb
      create    test/system/comments_test.rb

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

8. Шаблоны приложения

Теперь, когда вы узнали, как генераторы используются внутри приложения, знаете ли вы, что они используются и для генерации приложения тоже? Этот тип генератора называют "template". Далее идет краткий обзор Templates API. Подробную информацию смотрите в руководстве Шаблоны приложения на Rails.

gem "rspec-rails", group: "test"
gem "cucumber-rails", group: "test"

if yes?("Would you like to install Devise?")
  gem "devise"
  generate "devise:install"
  model_name = ask("What would you like the user model to be called? [user]")
  model_name = "user" if model_name.blank?
  generate "devise", model_name
end

В вышеприведенном шаблоне мы определили, что приложение полагается на гемы rspec-rails и cucumber-rails, поэтому они будут добавлены в группу test в Gemfile. Затем мы зададим вопрос пользователю относительно того, хочет ли он установить Devise. Если пользователь ответит "y" или "yes" на этот вопрос, тогда шаблон добавит Devise в Gemfile вне какой-либо группы, а затем запустит генератор devise:install. Затем этот шаблон возьмет пользовательский ввод и запустит генератор devise с переданным ответом пользователя из последнего вопроса.

Представим, что этот шаблон был в файле template.rb. Можно его использовать, чтобы модифицировать результат команды rails new с помощью опции -m и передачей имени файла:

$ rails new thud -m template.rb

Эта команда сгенерирует приложение Thud, а затем применит шаблон к сгенерированному результату.

Шаблоны не обязательно должны храниться в локальной системе, опция -m также поддерживает онлайн шаблоны:

$ rails new thud -m https://gist.github.com/radar/722911/raw/

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

9. Добавление аргументов командной строки

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

class_option :scope, type: :string, default: 'read_products'

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

$ bin/rails generate initializer --scope write_products

К аргументам командной строки можно обратиться с помощью метода options в классе генератора. То есть:

@scope = options['scope']

10. Методы генератора

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

Методы, представленные Thor не раскрываются в этом руководстве, а находятся в документации по Thor

10.1. gem

Указывает зависимость приложения от гема.

gem "rspec", group: "test", version: "2.1.0"
gem "devise", "1.1.5"

Доступны следующие опции:

  • :group - Группа в Gemfile, где должен быть гем.
  • :version - Строка версии гема, которую нужно использовать. Также может быть указана в качестве второго аргумента метода.
  • :git - URL репозитория git для этого гема.

Любые дополнительные опции, переданные в этот метод помещаются в конце строчки:

gem "devise", git: "https://github.com/plataformatec/devise.git", branch: "master"

Вышеприведенный код поместит следующую строчку в Gemfile:

gem "devise", git: "https://github.com/plataformatec/devise.git", branch: "master"

10.2. gem_group

Оборачивает вхождения гемов в группу:

gem_group :development, :test do
  gem "rspec-rails"
end

10.3. add_source

Добавляет определенный источник в Gemfile:

add_source "http://gems.github.com"

Этот метод также принимает блок:

add_source "http://gems.github.com" do
  gem "rspec-rails"
end

10.4. inject_into_file

Встраивает блок кода в определенную позицию вашего файла.

inject_into_file 'name_of_file.rb', after: "#The code goes below this line. Don't forget the Line break at the end\n" do <<-'RUBY'
  puts "Hello World"
RUBY
end

10.5. gsub_file

Заменяет текст в файле.

gsub_file 'name_of_file.rb', 'method.to_be_replaced', 'method.the_replacing_code'

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

10.6. application

Добавляет строчку в config/application.rb непосредственно после определения класса приложения.

application "config.asset_host = 'http://example.com'"

Также этот метод может принимать блок:

application do
  "config.asset_host = 'http://example.com'"
end

Доступные опции:

  • :env - Определяет среду для этой конфигурационной опции. Если хотите использовать эту опцию с блочным синтаксисом, рекомендуемый синтаксис следующий:
application(nil, env: "development") do
  "config.asset_host = 'http://localhost:3000'"
end

10.7. git

Запускает определенную команду git:

git :init
git add: "."
git commit: "-m First commit!"
git add: "onefile.rb", rm: "badfile.cxx"

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

10.8. vendor

Помещает файл, содержащий указанный код, в vendor.

vendor "sekrit.rb", '#top secret stuff'

Этот метод также принимает блок:

vendor "seeds.rb" do
  "puts 'in your app, seeding your database'"
end

10.9. lib

Помещает файл, содержащий указанный код, в lib.

lib "special.rb", "p Rails.root"

Этот метод также принимает блок:

lib "super_special.rb" do
  "puts 'Super special!'"
end

10.10. rakefile

Создает файл Rake в директории lib/tasks приложения.

rakefile "test.rake", 'task(:hello) { puts "Hello, there" }'

Этот метод также принимает блок:

rakefile "test.rake" do
  %Q{
    task rock: :environment do
      puts "Rockin'"
    end
  }
end

10.11. initializer

Создает инициализатор в директории config/initializers приложения:

initializer "begin.rb", "puts 'this is the beginning'"

Этот метод также принимает блок и ожидает возврата строки:

initializer "begin.rb" do
  "puts 'this is the beginning'"
end

10.12. generate

Запускает указанный генератор, где первый аргумент это имя генератора, а оставшиеся аргументы передаются непосредственно в генератор.

generate "scaffold", "forums title:string description:text"

10.13. rake

Запускает указанную задачу Rake.

rake "db:migrate"

Доступные опции:

  • :env - Указывает среду, в которой запускается эта задача rake.
  • :sudo - Запускать ли эту задачу с помощью sudo. По умолчанию false.

10.14. route

Добавляет текст в файл config/routes.rb:

route "resources :people"

10.15. readme

Выводит содержимое файла из source_path шаблона, обычно README.

readme "README"