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.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
現在,當我們執行 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 add
和 git 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
提供了額外的斷言。