1 初次接触
当您使用 rails
命令创建应用程序时,您实际上是在使用 Rails 生成器。之后,您可以通过调用 bin/rails generate
来获取所有可用生成器的列表。
$ rails new myapp
$ cd myapp
$ bin/rails generate
要创建 Rails 应用程序,我们使用 rails
全局命令,它使用通过 gem install rails
安装的 Rails 版本。当您在应用程序目录中时,我们使用 bin/rails
命令,它使用与应用程序捆绑在一起的 Rails 版本。
您将获得所有随 Rails 附带的生成器的列表。要查看特定生成器的详细描述,请使用 --help
选项调用该生成器。例如
$ bin/rails generate scaffold --help
2 创建您的第一个生成器
生成器构建在 Thor 之上,Thor 提供了强大的解析选项和用于操作文件的出色 API。
让我们构建一个生成器,它在 config/initializers
中创建一个名为 initializer.rb
的初始化程序文件。第一步是在 lib/generators/initializer_generator.rb
创建一个文件,内容如下
class InitializerGenerator < Rails::Generators::Base
def create_initializer_file
create_file "config/initializers/initializer.rb", <<~RUBY
# Add initialization content here
RUBY
end
end
我们的新生成器非常简单:它继承自 Rails::Generators::Base
并且有一个方法定义。当调用生成器时,生成器中的每个公共方法都会按定义顺序依次执行。我们的方法调用 create_file
,它将在给定的目标位置创建具有给定内容的文件。
要调用我们的新生成器,我们运行
$ 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", <<~RUBY
# Add initialization content here
RUBY
end
end
现在,我们可以通过在新生成器上调用 --help
来查看新描述。
第二种添加描述的方法是在与生成器相同的目录中创建一个名为 USAGE
的文件。我们将在下一步执行此操作。
3 使用生成器创建生成器
生成器本身有一个生成器。让我们删除我们的 InitializerGenerator
并使用 bin/rails generate generator
来生成一个新的生成器
$ rm lib/generators/initializer_generator.rb
$ 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
create config/initializers/core_extensions.rb
$ cat config/initializers/core_extensions.rb
# Add initialization content here
我们看到 copy_file
创建了 config/initializers/core_extensions.rb
,其中包含模板的内容。(在目标路径中使用的 file_name
方法继承自 Rails::Generators::NamedBase
。)
4 生成器命令行选项
生成器可以使用 class_option
支持命令行选项。例如
class InitializerGenerator < Rails::Generators::NamedBase
class_option :scope, type: :string, default: "app"
end
现在,我们的生成器可以使用 --scope
选项调用
$ bin/rails generate initializer theme --scope dashboard
选项值可以通过 options
在生成器方法中访问
def copy_initializer_file
@scope = options["scope"]
end
5 生成器解析
解析生成器名称时,Rails 会使用多个文件名来查找生成器。例如,当您运行 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 查找并加载该文件。
6 覆盖 Rails 生成器模板
解析生成器模板文件时,Rails 还会在多个位置查找。其中一个位置是应用程序的 lib/templates/
目录。此行为允许我们覆盖 Rails 内置生成器使用的模板。例如,我们可以覆盖 脚手架控制器模板 或 脚手架视图模板。
为了演示这一点,让我们创建一个 lib/templates/erb/scaffold/index.html.erb.tt
文件,其内容如下
<%% @<%= plural_table_name %>.count %> <%= human_name.pluralize %>
请注意,模板是一个 ERB 模板,它渲染另一个 ERB 模板。因此,任何应该出现在结果模板中的 <%
必须在生成器模板中转义为 <%%
。
现在,让我们运行 Rails 的内置脚手架生成器
$ bin/rails generate scaffold Post title:string
...
create app/views/posts/index.html.erb
...
app/views/posts/index.html.erb
的内容为
<% @posts.count %> Posts
7 覆盖 Rails 生成器
Rails 的内置生成器可以通过 config.generators
进行配置,包括完全覆盖一些生成器。
首先,让我们仔细看看脚手架生成器的工作原理。
$ bin/rails generate scaffold User name:string
invoke active_record
create db/migrate/20230518000000_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
create app/views/users/_user.html.erb
invoke resource_route
invoke test_unit
create test/controllers/users_controller_test.rb
create test/system/users_test.rb
invoke helper
create app/helpers/users_helper.rb
invoke test_unit
invoke jbuilder
create app/views/users/index.json.jbuilder
create app/views/users/show.json.jbuilder
从输出中,我们可以看到脚手架生成器调用了其他生成器,例如 `scaffold_controller` 生成器。其中一些生成器也会调用其他生成器。特别是,`scaffold_controller` 生成器调用了几个其他生成器,包括 `helper` 生成器。
让我们用一个新的生成器覆盖内置的 `helper` 生成器。我们将命名生成器 `my_helper`。
$ 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
在 `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", <<~RUBY
module #{class_name}Helper
# I'm helping!
end
RUBY
end
end
最后,我们需要告诉 Rails 使用 `my_helper` 生成器而不是内置的 `helper` 生成器。为此,我们使用 `config.generators`。在 `config/application.rb` 中,让我们添加:
config.generators do |g|
g.helper :my_helper
end
现在,如果我们再次运行脚手架生成器,我们就会看到 `my_helper` 生成器正在运行。
$ bin/rails generate scaffold Article body:text
...
invoke scaffold_controller
...
invoke my_helper
create app/helpers/articles_helper.rb
...
您可能注意到,内置 `helper` 生成器的输出包含“调用 test_unit”,而 `my_helper` 的输出则没有。虽然 `helper` 生成器默认情况下不会生成测试,但它确实提供了一个钩子,可以使用 hook_for
来实现。我们也可以在 `MyHelperGenerator` 类中包含 `hook_for :test_framework, as: :helper` 来做到这一点。有关更多信息,请参见 `hook_for` 文档。
7.1 生成器回退
覆盖特定生成器的另一种方法是使用回退。回退允许生成器命名空间委托给另一个生成器命名空间。
例如,假设我们想用自己的 `my_test_unit:model` 生成器覆盖 `test_unit:model` 生成器,但我们不想替换所有其他 `test_unit:*` 生成器,例如 `test_unit:controller`。
首先,我们在 `lib/generators/my_test_unit/model/model_generator.rb` 中创建 `my_test_unit:model` 生成器。
module MyTestUnit
class ModelGenerator < Rails::Generators::NamedBase
source_root File.expand_path("templates", __dir__)
def do_different_stuff
say "Doing different stuff..."
end
end
end
接下来,我们使用 `config.generators` 将 `test_framework` 生成器配置为 `my_test_unit`,但我们也配置了一个回退,以便任何缺失的 `my_test_unit:*` 生成器都解析为 `test_unit:*`
config.generators do |g|
g.test_framework :my_test_unit, fixture: false
g.fallbacks[:my_test_unit] = :test_unit
end
现在,当我们运行脚手架生成器时,我们看到 `my_test_unit` 已替换了 `test_unit`,但只有模型测试受到影响。
$ bin/rails generate scaffold Comment body:text
invoke active_record
create db/migrate/20230518000000_create_comments.rb
create app/models/comment.rb
invoke my_test_unit
Doing different stuff...
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
create app/views/comments/_comment.html.erb
invoke resource_route
invoke my_test_unit
create test/controllers/comments_controller_test.rb
create test/system/comments_test.rb
invoke helper
create app/helpers/comments_helper.rb
invoke my_test_unit
invoke jbuilder
create app/views/comments/index.json.jbuilder
create app/views/comments/show.json.jbuilder
8 应用程序模板
应用程序模板是一种特殊的生成器。它们可以使用所有 生成器辅助方法,但编写为 Ruby 脚本而不是 Ruby 类。以下是一个示例
# template.rb
if yes?("Would you like to install Devise?")
gem "devise"
devise_model = ask("What would you like the user model to be called?", default: "User")
end
after_bundle do
if devise_model
generate "devise:install"
generate "devise", devise_model
rails_command "db:migrate"
end
git add: ".", commit: %(-m 'Initial commit')
end
首先,模板询问用户是否要安装 Devise。如果用户回复“yes”(或“y”),模板会将 Devise 添加到 `Gemfile` 中,并询问用户 Devise 用户模型的名称(默认为 `User`)。稍后,在运行 `bundle install` 后,模板将运行 Devise 生成器和 `rails db:migrate`(如果指定了 Devise 模型)。最后,模板将 `git add` 和 `git commit` 整个应用程序目录。
我们可以在生成新的 Rails 应用程序时运行模板,方法是将 `-m` 选项传递给 `rails new` 命令。
$ rails new my_cool_app -m path/to/template.rb
或者,我们可以在现有应用程序中使用 `bin/rails app:template` 运行模板。
$ bin/rails app:template LOCATION=path/to/template.rb
模板也不需要存储在本地 - 您也可以指定 URL 而不是路径。
$ rails new my_cool_app -m http://example.com/template.rb
$ bin/rails app:template LOCATION=http://example.com/template.rb
9 生成器辅助方法
Thor 通过 Thor::Actions
提供了许多生成器辅助方法,例如
除了这些之外,Rails 还通过 Rails::Generators::Actions
提供了许多辅助方法,例如
10 测试生成器
Rails 通过 Rails::Generators::Testing::Behaviour
提供了测试辅助方法,例如
如果针对生成器运行测试,您需要设置 `RAILS_LOG_TO_STDOUT=true`,以便调试工具正常工作。
RAILS_LOG_TO_STDOUT=true ./bin/test test/generators/actions_test.rb
除了这些之外,Rails 还通过 Rails::Generators::Testing::Assertions
提供了额外的断言。