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
# Add initialization content here
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
# 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.rbgenerators/initializer/initializer_generator.rbrails/generators/initializer_generator.rbgenerators/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 生成器的输出包含 "invoke 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. 应用程序模板
应用程序模板与生成器略有不同。生成器将文件添加到现有的 Rails 应用程序(模型、视图等),而模板用于自动化新 Rails 应用程序的设置。模板是 Ruby 脚本(通常命名为 template.rb),它们在新 Rails 应用程序生成后立即对其进行自定义。
让我们看看如何在创建新的 Rails 应用程序时使用模板。
8.1. 创建和使用模板
让我们从一个示例模板 Ruby 脚本开始。以下模板在询问用户后将 Devise 添加到 Gemfile 中,并且允许用户命名 Devise 用户模型。在运行 bundle install 之后,模板运行 Devise 生成器并运行迁移。最后,模板执行 git add 和 git commit。
# 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
要在创建新的 Rails 应用程序时应用此模板,您需要使用 -m 选项提供模板的位置
$ rails new blog -m ~/template.rb
上述命令将创建一个名为 blog 的新 Rails 应用程序,其中配置了 Devise gem。
您还可以使用 app:template 命令将模板应用于现有 Rails 应用程序。模板的位置需要通过 LOCATION 环境变量传入
$ bin/rails app:template LOCATION=~/template.rb
模板不必存储在本地,您也可以指定 URL 而不是路径
$ rails new blog -m https://example.com/template.rb
$ bin/rails app:template LOCATION=https://example.com/template.rb
执行第三方远程脚本时应谨慎。由于模板是纯 Ruby 脚本,它很容易包含危害您本地机器的代码(例如下载病毒、删除文件或将您的私人文件上传到服务器)。
上述 template.rb 文件使用了 after_bundle 和 rails_command 等辅助方法,并通过 yes? 等方法增加了用户交互。所有这些方法都是 Rails 模板 API 的一部分。以下部分将通过示例展示如何使用更多这些方法。
9. Rails 生成器 API
生成器和模板 Ruby 脚本可以使用 DSL(领域特定语言)访问多个辅助方法。这些方法是 Rails 生成器 API 的一部分,您可以在 Thor::Actions 和 Rails::Generators::Actions API 文档中找到更多详细信息。
以下是另一个典型的 Rails 模板示例,它脚手架一个模型,运行迁移,并使用 git 提交更改
# template.rb
generate(:scaffold, "person name:string")
route "root to: 'people#index'"
rails_command("db:migrate")
after_bundle do
git :init
git add: "."
git commit: %Q{ -m 'Initial commit' }
end
以下示例中的所有代码片段都可以在模板文件(例如上面的 template.rb 文件)中使用。
9.1. add_source
add_source 方法将给定的源添加到生成的应用程序的 Gemfile 中。
add_source "https://rubygems.org.cn"
如果给定一个块,块中的 gem 条目将包装到源组中。例如,如果您需要从 "http://gems.github.com" 获取一个 gem
add_source "http://gems.github.com/" do
gem "rspec-rails"
end
9.2. after_bundle
after_bundle 方法注册一个回调,该回调将在 gem 打包后执行。例如,只有在 tailwindcss-rails 和 devise 这些 gem 打包后,才执行它们的 "install" 命令是合理的
# Install gems
after_bundle do
# Install TailwindCSS
rails_command "tailwindcss:install"
# Install Devise
generate "devise:install"
end
即使传入了 --skip-bundle,回调也会执行。
9.3. environment
environment 方法在 config/application.rb 的 Application 类内部添加一行。如果指定了 options[:env],则该行将附加到 config/environments 中的相应文件。
environment 'config.action_mailer.default_url_options = {host: "http://yourwebsite.example.com"}', env: "production"
以上代码将配置行添加到 config/environments/production.rb。
9.4. gem
gem 辅助方法将给定 gem 的条目添加到生成的应用程序的 Gemfile 中。
例如,如果您的应用程序依赖于 devise 和 tailwindcss-rails 这两个 gem
gem "devise"
gem "tailwindcss-rails"
请注意,此方法仅将 gem 添加到 Gemfile,它不会安装 gem。
您还可以指定确切的版本
gem "devise", "~> 4.9.4"
您还可以添加注释,这些注释将添加到 Gemfile 中
gem "devise", comment: "Add devise for authentication."
9.5. gem_group
gem_group 辅助方法将 gem 条目包装在组内。例如,只在 development 和 test 组中加载 rspec-rails
gem_group :development, :test do
gem "rspec-rails"
end
9.6. generate
您甚至可以在 template.rb 内部使用 generate 方法调用生成器。以下命令使用给定参数运行 scaffold Rails 生成器
generate(:scaffold, "person", "name:string", "address:text", "age:number")
9.7. git
Rails 模板允许您使用 git 辅助方法运行任何 git 命令
git :init
git add: "."
git commit: "-a -m 'Initial commit'"
9.8. initializer, vendor, lib, file
initializer 辅助方法将一个初始化器添加到生成的应用程序的 config/initializers 目录中。
在 template.rb 文件中添加以下内容后,您可以在应用程序中使用 Object#not_nil? 和 Object#not_blank?
initializer "not_methods.rb", <<-CODE
class Object
def not_nil?
!nil?
end
def not_blank?
!blank?
end
end
CODE
类似地,lib 方法在 lib/ 目录中创建文件,vendor 方法在 vendor/ 目录中创建文件。
还有一个 file 方法(它是 create_file 的别名),它接受一个相对于 Rails.root 的路径,并创建所有必要的目录和文件
file "app/components/foo.rb", <<-CODE
class Foo
end
CODE
上述操作将创建 app/components 目录并将 foo.rb 放入其中。
9.9. rakefile
rakefile 方法在 lib/tasks 下创建一个新的 Rake 文件,并包含给定的任务
rakefile("bootstrap.rake") do
<<-TASK
namespace :boot do
task :strap do
puts "I like boots!"
end
end
TASK
end
上述代码创建了 lib/tasks/bootstrap.rake,其中包含一个 boot:strap rake 任务。
9.10. run
run 方法执行任意命令。假设您要删除 README.rdoc 文件
run "rm README.rdoc"
9.11. rails_command
您可以使用 rails_command 辅助方法在生成的应用程序中运行 Rails 命令。假设您希望在模板 ruby 脚本的某个时刻迁移数据库
rails_command "db:migrate"
命令可以以不同的 Rails 环境运行
rails_command "db:migrate", env: "production"
您还可以运行那些如果失败应该中止应用程序生成的命令
rails_command "db:migrate", abort_on_failure: true
9.12. route
route 方法将一个条目添加到 config/routes.rb 文件中。要将 PeopleController#index 设置为应用程序的默认页面,我们可以添加
route "root to: 'person#index'"
还有许多辅助方法可以操作本地文件系统,例如 copy_file、create_file、insert_into_file 和 inside。您可以查看 Thor API 文档了解详细信息。以下是其中一个方法的示例
9.13. inside
这个 inside 方法使您能够从给定目录运行命令。例如,如果您有一个 edge Rails 的副本,您希望从新应用程序中符号链接,您可以这样做
inside("vendor") do
run "ln -s ~/my-forks/rails rails"
end
还有一些方法允许您从 Ruby 模板与用户交互,例如 ask、yes 和 no。您可以在 Thor Shell 文档中了解所有用户交互方法。让我们看看使用 ask、yes? 和 no? 的示例
9.14. ask
ask 方法允许您从用户那里获取反馈并在模板中使用。假设您希望用户命名您要添加的新闪亮库
lib_name = ask("What do you want to call the shiny library?")
lib_name << ".rb" unless lib_name.index(".rb")
lib lib_name, <<-CODE
class Shiny
end
CODE
9.15. yes? or no?
这些方法允许您从模板中提问,并根据用户的回答决定流程。假设您想提示用户运行迁移
rails_command("db:migrate") if yes?("Run database migrations?")
# no? questions acts the opposite of yes?
10. 测试生成器
Rails 通过 Rails::Generators::Testing::Behavior 提供测试辅助方法,例如
如果对生成器运行测试,您需要设置 RAILS_LOG_TO_STDOUT=true 以使调试工具正常工作。
RAILS_LOG_TO_STDOUT=true ./bin/test test/generators/actions_test.rb
除了这些之外,Rails 还通过 Rails::Generators::Testing::Assertions 提供了额外的断言。