更多資訊請至 rubyonrails.org:

建立與自訂 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 之上,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 內建產生器使用的樣板。例如,我們可以覆寫 支架控制器樣板支架視圖樣板

若要查看此操作,讓我們建立一個 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 產生器會調用其他產生器,例如 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

現在,如果我們再次執行 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 產生器回退

覆寫特定產生器的另一種方法是使用回退 (fallbacks)。回退允許一個產生器命名空間委託給另一個產生器命名空間。

例如,假設我們想用我們自己的 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。如果使用者回答「yes」(或「y」),樣板會將 Devise 新增至 Gemfile,並詢問使用者 Devise 使用者模型的名稱(預設為 User)。稍後,在執行 bundle install 後,如果指定了 Devise 模型,樣板將會執行 Devise 產生器和 rails db:migrate。最後,樣板會使用 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 提供了許多輔助方法,例如

10 測試產生器

Rails 透過 Rails::Generators::Testing::Behaviour 提供了測試輔助方法,例如

如果針對產生器執行測試,您需要設定 RAILS_LOG_TO_STDOUT=true,以便偵錯工具正常運作。

RAILS_LOG_TO_STDOUT=true ./bin/test test/generators/actions_test.rb

除了這些之外,Rails 還透過 Rails::Generators::Testing::Assertions 提供了額外的斷言。



返回頂部