v7.1.3.2
更多資訊請至 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
      # 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.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 控制器範本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 產生器

Rails 內建的產生器可透過 config.generators 進行設定,包括完全覆寫某些產生器。

首先,讓我們仔細瞭解 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_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.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。如果使用者回答「是」(或「y」),範本會將 Devise 加入 Gemfile,並詢問使用者 Devise 使用者模型的名稱 (預設為 User)。稍後,在執行 bundle install 之後,範本會執行 Devise 產生器和 rails db:migrate (如果指定了 Devise 模型)。最後,範本會對整個應用程式目錄執行 git addgit 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 提供許多輔助方法,例如

回饋

我們鼓勵您協助提升本指南的品質。

如果您發現任何錯字或事實錯誤,請協助我們修正。若要開始,您可以閱讀我們的 文件貢獻 章節。

您也可能會發現不完整的內容或未更新的內容。請務必為主要內容新增任何遺漏的文件。請務必先查看 Edge Guides,以驗證問題是否已在主要分支中修正。查看 Ruby on Rails 指南準則 以了解風格和慣例。

如果您發現需要修正的地方,但無法自行修補,請 開啟問題

最後但並非最不重要的一點,我們非常歡迎在 官方 Ruby on Rails 論壇 上針對 Ruby on Rails 文件進行任何討論。