edge
更多信息请访问 rubyonrails.org: 更多 Ruby on 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构建的,它提供了强大的选项解析和用于操作文件的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
      # 在这里添加初始化内容
    RUBY
  end
end

我们的新生成器非常简单:它继承自Rails::Generators::Base,并且有一个方法定义。当调用生成器时,生成器中的每个公共方法按照定义的顺序依次执行。我们的方法调用了create_file,它将在给定的目标位置创建一个具有给定内容的文件。

要调用我们的新生成器,我们运行:

$ bin/rails generate initializer

在继续之前,让我们看一下我们的新生成器的描述:

$ bin/rails generate initializer --help

如果生成器有命名空间,例如ActiveRecord::Generators::ModelGenerator,Rails通常能够推导出一个好的描述,但在这种情况下不能。我们可以通过两种方式解决这个问题。第一种方式是在生成器内部调用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
      # 在这里添加初始化内容
    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,内容如下:

# 在这里添加初始化内容

并且让我们更改生成器以在调用时复制此模板: ```ruby 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
# 在这里添加初始化内容

我们可以看到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内置生成器使用的模板。例如,我们可以覆盖scaffold controller模板scaffold视图模板

为了看到这个过程,让我们创建一个lib/templates/erb/scaffold/index.html.erb.tt文件,内容如下:

<%% @<%= plural_table_name %>.count %> <%= human_name.pluralize %>

注意,模板是一个ERB模板,用于渲染另一个_ERB模板。因此,在生成器模板中,任何应该出现在结果_模板中的<%必须转义为<%%

现在让我们运行Rails内置的scaffold生成器:

$ 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生成器

可以通过config.generators配置Rails内置生成器,包括完全覆盖某些生成器。

首先,让我们更详细地了解scaffold生成器的工作原理。

$ 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生成器调用了其他生成器,比如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
        # 我在帮助!
      end
    RUBY
  end
end

最后,我们需要告诉Rails使用my_helper生成器而不是内置的helper生成器。为此,我们使用config.generators。在config/application.rb中,让我们添加:

config.generators do |g|
  g.helper :my_helper
end

现在,如果我们再次运行scaffold生成器,我们会看到my_helper生成器的效果:

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

注意:你可能会注意到内置的helper生成器的输出中包含了"invoke test_unit",而my_helper的输出中没有。尽管helper生成器默认不生成测试,但它提供了一个使用hook_for的钩子来生成测试的方法。我们可以在MyHelperGenerator类中包含hook_for :test_framework, as: :helper来实现相同的功能。有关更多信息,请参阅hook_for文档。

7.1 生成器回退

覆盖特定生成器的另一种方法是使用回退。回退允许一个生成器命名空间委托给另一个生成器命名空间。 例如,假设我们想要覆盖test_unit:model生成器,使用我们自己的my_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.generatorstest_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。如果用户回答“是”(或“y”),模板将Devise添加到Gemfile,并询问Devise用户模型的名称(默认为User)。稍后,在运行了bundle install之后,模板将运行Devise生成器和rails db:migrate(如果指定了Devise模型)。最后,模板将git addgit commit整个应用目录。

我们可以通过在rails new命令中传递-m选项来在生成新的Rails应用程序时运行我们的模板:

$ 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提供了许多辅助方法,例如:

反馈

欢迎您帮助改进本指南的质量。

如果您发现任何拼写错误或事实错误,请贡献您的意见。 要开始,请阅读我们的 文档贡献 部分。

您还可能会发现不完整的内容或过时的内容。 请为主要内容添加任何缺失的文档。请先检查 Edge 指南,以验证问题是否已经修复或尚未修复。 请参阅 Ruby on Rails 指南准则 以了解样式和规范。

如果您发现需要修复但无法自行修复的问题,请 提交问题

最后但同样重要的是,欢迎您在 官方 Ruby on Rails 论坛 上讨论有关 Ruby on Rails 文档的任何问题。