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.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。如果用户回答“是”(或“y”),模板将Devise添加到Gemfile
,并询问Devise用户模型的名称(默认为User
)。稍后,在运行了bundle install
之后,模板将运行Devise生成器和rails db:migrate
(如果指定了Devise模型)。最后,模板将git add
和git 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 文档的任何问题。