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

通常情況下,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
      # 在此加入初始化內容
    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 view模板

為了看到這一點,讓我們創建一個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

現在,當我們運行 scaffold 產生器時,我們可以看到 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 模型,模板將運行 Devise 產生器和 rails db:migrate。最後,模板將使用 git addgit commit 提交整個應用程式目錄。

我們可以通過將 -m 選項傳遞給 rails new 命令來在生成新的 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 論壇 上進行。