Генераторы Rails - необходимый инструмент, если вы планируете улучшить свой рабочий процесс. С помощью этого руководства вы изучите, как создавать генераторы и настраивать существующие.
После прочтения этого руководства, вы узнаете:
При создании приложения с помощью команды 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
Начиная с 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
в той же директории, что и наш генератор. Мы это сделаем на следующем этапе.
У самих генераторов есть генератор:
$ 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
.
Доступные для генераторов методы раскрываются в последнем разделе этого руководства.
При запуске 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
.
Собственные генераторы 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
Теперь можно снова запустить скаффолд для другого ресурса и увидеть, что он также генерирует тесты!
На предыдущем шаге мы просто хотели добавить строчку в сгенерированный хелпер без добавления какой-либо дополнительной функциональности. Имеется более простой способ, чтобы сделать такое - замена шаблонов для уже существующих генераторов, в нашем случае 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 %>
Еще одна особенность генераторов, которая очень полезна, это фолбэки. Например, представим, что вы хотите добавить особенность над 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
Фолбэки позволяют вашим генераторам иметь единственную ответственность, увеличить повторное использование кода и уменьшить дублирование.
Теперь, когда вы узнали, как генераторы используются внутри приложения, знаете ли вы, что они используются и для генерации приложения тоже? Этот тип генератора называют "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/
В то время как последний раздел этого руководства не раскрывает, как генерировать замечательные шаблоны, он познакомит вас с доступными методами, с помощью которых вы сможете создать их самостоятельно. Абсолютно те же методы доступны и для генераторов.
Генераторы Rails легко модифицировать, чтобы они принимали произвольные аргументы командной строки. Эта функциональность исходит из Thor:
class_option :scope, type: :string, default: 'read_products'
Теперь наш генератор может быть вызван следующим образом:
$ bin/rails generate initializer --scope write_products
К аргументам командной строки можно обратиться с помощью метода options
в классе генератора. То есть:
@scope = options['scope']
Следующие методы доступны как для генераторов, так и для шаблонов Rails.
Методы, представленные Thor не раскрываются в этом руководстве, а находятся в документации по Thor
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"
gem_group
Оборачивает вхождения гемов в группу:
gem_group :development, :test do
gem "rspec-rails"
end
add_source
Добавляет определенный источник в Gemfile
:
add_source "http://gems.github.com"
Этот метод также принимает блок:
add_source "http://gems.github.com" do
gem "rspec-rails"
end
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
gsub_file
Заменяет текст в файле.
gsub_file 'name_of_file.rb', 'method.to_be_replaced', 'method.the_replacing_code'
Этот метод можно сделать более точным с помощью регулярных выражений. Таким же образом можно использовать append_file
и prepend_file
, чтобы поместить код в начало или конец файла соответственно.
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
git
Запускает определенную команду git:
git :init
git add: "."
git commit: "-m First commit!"
git add: "onefile.rb", rm: "badfile.cxx"
Значения хэша будут аргументами или опциями, переданными в определенную команду git. Как показано в последнем примере, одновременно могут быть определены несколько команд git, но не гарантируется соответствие порядка их запуска порядку, в котором они определены.
vendor
Помещает файл, содержащий указанный код, в vendor
.
vendor "sekrit.rb", '#top secret stuff'
Этот метод также принимает блок:
vendor "seeds.rb" do
"puts 'in your app, seeding your database'"
end
lib
Помещает файл, содержащий указанный код, в lib
.
lib "special.rb", "p Rails.root"
Этот метод также принимает блок:
lib "super_special.rb" do
"puts 'Super special!'"
end
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
initializer
Создает инициализатор в директории config/initializers
приложения:
initializer "begin.rb", "puts 'this is the beginning'"
Этот метод также принимает блок и ожидает возврата строки:
initializer "begin.rb" do
"puts 'this is the beginning'"
end
generate
Запускает указанный генератор, где первый аргумент это имя генератора, а оставшиеся аргументы передаются непосредственно в генератор.
generate "scaffold", "forums title:string description:text"
rake
Запускает указанную задачу Rake.
rake "db:migrate"
Доступные опции:
:env
- Указывает среду, в которой запускается эта задача rake.
:sudo
- Запускать ли эту задачу с помощью sudo
. По умолчанию false
.
route
Добавляет текст в файл config/routes.rb
:
route "resources :people"
readme
Выводит содержимое файла из source_path
шаблона, обычно README.
readme "README"