更多内容请访问 rubyonrails.org:

创建和自定义 Rails 生成器与模板

Rails 生成器是改善工作流程的必备工具。通过本指南,您将学习如何创建生成器和自定义现有生成器。

阅读本指南后,您将了解

  • 如何查看应用程序中有哪些可用的生成器。
  • 如何使用模板创建生成器。
  • Rails 在调用生成器之前如何搜索生成器。
  • 如何通过覆盖生成器模板来自定义脚手架。
  • 如何通过覆盖生成器来自定义脚手架。
  • 如何使用回退来避免覆盖大量的生成器。
  • 如何创建应用程序模板。

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 提供了额外的断言。



返回顶部