原文:Ruby on Rails Guides — Getting Started with Engines
Rails::Engine
允許你包裝特定的 Rails 應用程式或功能子集,並與其他應用程式共享或在更大的封裝應用程式中。每個 Rails::Application
都只是一個引擎,它允許簡單的功能及應用程式共享。
任何 Rails::Engine
也是一個 Rails::Railtie
,所以在 railties 可用的相同方法(如 rake_tasks
和 generators
)和配置選項也可用於引擎。
在本指南中,你將瞭解引擎以及如何透過乾淨且非常易於使用的介面,為主應用程式提供附加功能。
閱讀本指南後,你將知道:
- 是什麼構成了引擎。
- 如何產生引擎。
- 如何為引擎建立功能。
- 如何將引擎掛載到應用程式中。
- 如何在應用程式中覆寫引擎功能。
- 透過載入和配置 Hook 避免載入 Rails 框架。
1. 什麼是引擎?
引擎可以看成是為主應用程式提供功能的微型應用程式。Rails 應用程式實際上只是一個「增壓」引擎,Rails::Application
類別繼承了許多來自 Rails::Engine
的行為。
因此,引擎和應用程式可以被認為是幾乎相同的東西,只有存在細微的差異,你將在本指南中看到。引擎和應用程式也共享一個公用的結構。
引擎也與外掛程式密切相關。兩者共享一個公用的 lib
目錄結構,並且都是使用 rails plugin new
產生器產生的。不同之處在於,引擎被 Rails 視為一個「完整外掛程式」(如傳遞給產生器指令的 --full
選項所示)。我們實際上會在此處使用 --mountable
選項,其中包含 --full
的所有功能,甚至更多。本指南全文將這些「完整外掛程式」簡稱為「引擎」。引擎可以是外掛程式,外掛程式可以是引擎。
將在本指南中建立的引擎稱為「blorgh」。此引擎將為其主應用程式提供部落格功能,允許建立新文章和評論。在本指南的開頭,你將獨自在引擎內工作,但在後面的章節中,你將看到如何將其掛載到應用程式中。
引擎也可以與主應用程式隔離。意思是應用程式可以擁有路由輔助方法提供的路徑,如 articles_path
,並使用一個也提供了名為 articles_path
路徑的引擎,而且兩者不會發生衝突。除此之外,控制器、模型和資料表名稱也具有命名空間。本指南稍後將介紹如何執行此操作。
重要的是在任何時候牢記這一點,應用程式應該總是優先於其引擎。應用程式是對其環境中發生的事情有最終決定權的物件。引擎應該只是加強它,而不是徹底改變它。
查看其他引擎的範例,查看 Devise,一個為其父應用程式提供身份驗證的引擎,或 Thredded,一個提供論壇功能的引擎。還有 Spree 提供電子商務平台,以及 Refinery CMS,一個 CMS 引擎。
最後,如果沒有 James Adam、Piotr Sarnacki、Rails 核心團隊和其他一些人的工作,引擎是不可能實現的。如果你見過他們,別忘了說聲謝謝!
2. 產生一個引擎
要產生引擎,你需要執行外掛程式產生器並根據需求將選項傳遞給它。對於「blorgh」範例,你需要建立一個「可掛載的」引擎,在終端機中執行此指令:
$ rails plugin new blorgh --mountable
輸入以下內容即可看到外掛程式產生器的完整選項清單:
$ rails plugin --help
--mountable
選項告訴產生器你想建立一個「可掛載的」且命名空間隔離的引擎。此產生器將提供與 --full
選項相同的骨架結構。--full
選項告訴產生器你想建立一個引擎,包含提供以下內容的骨架結構:
一個
app
目錄樹一個
config/routes.rb
檔案:Rails.application.routes.draw do end
一個在
lib/blorgh/engine.rb
的檔案,其功能與標準 Rails 應用程式的config/application.rb
檔案相同:module Blorgh class Engine < ::Rails::Engine end end
--mountable
選項會加入 --full
選項:
靜態資源清單檔案(
application.js
和application.css
)一個具有命名空間的
ApplicationController
一個具有命名空間的
ApplicationHelper
引擎的佈局視圖樣板
命名空間隔離的
config/routes.rb
:Blorgh::Engine.routes.draw do end
命名空間隔離的
lib/blorgh/engine.rb
:module Blorgh class Engine < ::Rails::Engine isolate_namespace Blorgh end end
此外,--mountable
選項透過將以下內容加到虛擬應用程式的路由檔案 test/dummy/config/routes.rb
,告訴產生器將引擎掛載到位於 test/dummy
的虛擬測試應用程式中:
mount Blorgh::Engine => "/blorgh"
2.1 引擎內部
2.1.1 關鍵檔案
在這個全新引擎的根目錄有個 blorgh.gemspec
檔案。當你稍後將引擎掛載到應用程式中時,你將在 Rails 應用程式的 Gemfile
中使用此行進行操作:
gem 'blorgh', path: 'engines/blorgh'
不要忘記像往常一樣執行 bundle install
。透過在 Gemfile
中指定它作為一個 gem,Bundler 將載入它,解析此 blorgh.gemspec
檔案並載入 lib
目錄中的一個名為 lib/blorgh.rb
的檔案。這個檔案載入了 blorgh/engine.rb
檔案(位於 lib/blorgh/engine.rb
)並定義了一個名為 Blorgh
的基本模組。
require "blorgh/engine"
module Blorgh
end
有些引擎選擇使用這個檔案為其引擎提供全域配置選項。這是一個比較好的想法,所以如果你想提供配置選項,那麼定義引擎
module
的檔案就是完美的選擇。把方法放到模組裡面就可以了。
lib/blorgh/engine.rb
是引擎的基底類別:
module Blorgh
class Engine < ::Rails::Engine
isolate_namespace Blorgh
end
end
透過繼承 Rails::Engine
類別,這個 gem 會通知 Rails 在指定路徑上有一個引擎,並且將引擎正確地掛載到應用程式,執行諸如將引擎的 app
目錄加到模型、Mailer、控制器和視圖的載入路徑。
這裡的 isolate_namespace
方法值得特別注意。這個呼叫負責將控制器、模型、路由和其他東西隔離到自己的命名空間中,使其遠離應用程式內部的類似元件。沒有這個,引擎的元件就有可能「洩漏」到應用程式,造成不必要的中斷,或者重要的引擎元件可能會被應用程式中類似名稱的東西覆蓋。這種衝突的其中一個例子就是輔助方法。在不呼叫 isolate_namespace
的情況下,引擎的輔助方法將被包含在應用程式的控制器中。
高度推薦在
Engine
類別定義中保留isolate_namespace
。如果沒有它,引擎中產生的類別可能 會與應用程式發生衝突。
此命名空間的隔離意思是透過呼叫 bin/rails g model
產生的模型,如 bin/rails g model article
,名稱不是 Article
,而是具有命名空間的名稱 Blorgh::Article
。另外,模型的資料表也具有命名空間,成為 blorgh_articles
,而不僅僅是 articles
。與模型命名空間類似,一個名為 ArticlesController
的控制器變成了 Blorgh::ArticlesController
,並且該控制器的視圖不會在 app/views/articles
,而是 app/views/blorgh/articles
。Mailer 也具有命名空間。
最後,路由也被隔離在引擎內。這是命名空間中最重要的部分之一,稍後將在本指南的路由部分中進行討論。
2.1.2 app 目錄
在 app 目錄中有標準的 assets
、controllers
、helpers
、mailers
、models
和 views
目錄,你應該已從應用程式對這些目錄瞭若指掌。helpers
、mailers
和 models
目錄是空的,因此在本節中沒有描述它們。當我們編寫引擎時,我們將在未來的章節中更深入地研究模型。
在 app/assets
目錄中,有 images
、javascripts
和 stylesheets
目錄,你應該早已熟悉這些目錄,因為它們與應用程式類似。然而,這裡的一個不同之處在於,每個目錄都包含一個帶有引擎名稱的子目錄。因為這個引擎具有命名空間,它的靜態資源也應該如此。
在 app/controllers
目錄中有一個 blorgh
目錄,其中包含了一個名為 application_controller.rb
的檔案。該檔案將為引擎的控制器提供任何公用功能。blorgh
目錄是存放引擎的其它控制器的地方。透過將它們放在這個命名空間的目錄中,可以防止它們與其它引擎甚至是應用程式中的同名控制器發生衝突。
引擎內的
ApplicationController
類別命名就像一個 Rails 應用程式,以便更容易將應用程式轉換為引擎。
因為 Ruby 不斷尋找的方式,你可能會遇到一種狀況,你的控制器繼承自主應用程式控制器,而不是你的引擎應用程式控制器。Ruby 能夠解析
ApplicationController
常數,因此不會觸發自動載入機制。有關更多詳細資訊,請查看自動載入和重新載入常數指南中的常數什麼時候不被忽略章節。預防這種狀況發生最好的方法是使用require_dependency
來確保引擎的應用程式控制器已被載入。例如:
# app/controllers/blorgh/articles_controller.rb:
require_dependency "blorgh/application_controller"
module Blorgh
class ArticlesController < ApplicationController
...
end
end
不要使用 require
,因為它會中斷開發環境中類別的自動重新載入,使用 require_dependency
確保類別以正確的方式載入和卸除。
最後,app/views
目錄中有一個 layouts
資料夾,其中包含了一個檔案在 blorgh/application.html.erb
。該檔案允許你指定引擎的佈局。如果此引擎要作為獨立引擎使用,那麼你可以在此檔案中加入任何對佈局的客製化,而不是應用程式的 app/views/layouts/application.html.erb
檔案。
如果不希望對引擎的使用者強制佈局,那麼可以刪除此檔案並在引擎的控制器中引用不同的佈局。
2.1.3 bin 目錄
這個目錄包含一個 bin/rails
檔案,它讓你能夠像在應用程式中一樣使用 rails
子指令和產生器。也就是說,你可以很容易地透過執行這樣的指令來產生新的控制器和模型:
$ bin/rails g model
請記住,在 Engine
類別中具有 isolate_namespace
的引擎內部使用這些指令產生的任何東西,都將具有命名空間。
2.1.4 test 目錄
test
目錄是用來存放引擎測試的地方。為了測試引擎,在 test/dummy
中嵌入了一個精簡版的 Rails 應用程式。這個應用程式會將引擎掛載到 test/dummy/config/routes.rb
檔案:
Rails.application.routes.draw do
mount Blorgh::Engine => "/blorgh"
end
該行將引擎掛載到 /blorgh
路徑上,這將使應用程式僅能透過該路徑存取它。
在測試目錄裡面有 test/integration
目錄,引擎的整合測試應該放在這裡。也可以在 test
目錄中建立其它目錄。例如,你可能希望為模型測試建立一個 test/models
目錄。
3. 提供引擎功能
本指南涵蓋的引擎提供了送交文章和評論的功能,遵循與入門指南類似的思路,並伴隨一些新的觀點。
3.1 產生 Article 資源
產生部落格引擎的第一件事是 Article
模型和相關的控制器。要快速建立這個,你可以使用 Rails 鷹架產生器。
$ bin/rails generate scaffold article title:string text:text
該指令會輸出以下資訊:
invoke active_record
create db/migrate/[timestamp]_create_blorgh_articles.rb
create app/models/blorgh/article.rb
invoke test_unit
create test/models/blorgh/article_test.rb
create test/fixtures/blorgh/articles.yml
invoke resource_route
route resources :articles
invoke scaffold_controller
create app/controllers/blorgh/articles_controller.rb
invoke erb
create app/views/blorgh/articles
create app/views/blorgh/articles/index.html.erb
create app/views/blorgh/articles/edit.html.erb
create app/views/blorgh/articles/show.html.erb
create app/views/blorgh/articles/new.html.erb
create app/views/blorgh/articles/_form.html.erb
invoke test_unit
create test/controllers/blorgh/articles_controller_test.rb
invoke helper
create app/helpers/blorgh/articles_helper.rb
invoke test_unit
create test/application_system_test_case.rb
create test/system/articles_test.rb
invoke assets
invoke js
create app/assets/javascripts/blorgh/articles.js
invoke css
create app/assets/stylesheets/blorgh/articles.css
invoke css
create app/assets/stylesheets/scaffold.css
鷹架產生器所做的第一件事是呼叫 active_record
產生器,它為資源產生遷移和模型。但是請注意,遷移被命名為 create_blorgh_articles
,而不是常見的 create_articles
。這是由於 Blorgh::Engine
類別的定義中呼叫 isolate_namespace
方法造成的。這裡的模型也具有命名空間,由於 Engine
類別中呼叫 isolate_namespace
,所以放在 app/models/blorgh/article.rb
而不是 app/models/article.rb
。
接下來,為此模型呼叫 test_unit
產生器,在 test/models/blorgh/article_test.rb
(而不是 test/models/article_test.rb
)產生一個模型測試,並在 test/fixtures/blorgh/articles.yml
(而不是 test/fixtures/articles.yml
)產生測試資料。
然後,插入一行資源路由到引擎的 config/routes.rb
檔案。這一行只是 resources :articles
,將引擎的 config/routes.rb
檔案變成這樣:
Blorgh::Engine.routes.draw do
resources :articles
end
這裡請注意,路由是被定義在 Blorgh::Engine
物件上,而不是 YourApp::Application
類別。這是為了使引擎路由被限制在引擎本身,並且如測試目錄部分所示可以掛載在特定點上。它還會使引擎的路由與應用程式內的路由隔離。本指南的路由部分有詳細的說明。
接下來,scaffold_controller
產生器被呼叫,產生一個名為 Blorgh::ArticlesController
(在 app/controllers/blorgh/articles_controller.rb
)的控制器及其相關視圖在 app/views/blorgh/articles
。此產生器還為控制器產生測試(test/controllers/blorgh/articles_controller_test.rb
)和輔助方法(app/helpers/blorgh/articles_helper.rb
)。
這個產生器建立的所有東西都具有命名空間。控制器的類別被定義在 Blorgh
模組:
module Blorgh
class ArticlesController < ApplicationController
...
end
end
ArticlesController
類別繼承自Blorgh::ApplicationController
,而不是應用程式的ApplicationController
。
app/helpers/blorgh/articles_helper.rb
中的輔助方法也具有命名空間:
module Blorgh
module ArticlesHelper
...
end
end
這有助於防止與任何其它可能具有文章資源的引擎或應用程式發生衝突。
最後,產生了兩個靜態資源檔案:app/assets/javascripts/blorgh/articles.js
和 app/assets/stylesheets/blorgh/articles.css
。稍後你會看到如何使用這些東西。
你可以透過在引擎的根目錄執行 bin/rails db:migrate
來執行由鷹架產生器產生的遷移,然後在 test/dummy
中執行 rails server
來查看引擎的內容。當你開啟 http://localhost:3000/blorgh/articles
時,將看到已產生的預設鷹架。點看看吧!你剛剛產生了你的第一個引擎的第一個功能。
如果你更喜歡使用主控台,也可以像 Rails 應用程式一樣使用 rails console
。記住:Article
模型是具有命名空間的,因此要引用它,你必須稱呼它為 Blorgh::Article
。
>> Blorgh::Article.find(1)
=> #<Blorgh::Article id: 1 ...>
最後一件事是這個引擎的 articles
資源應該是引擎的根。每當有人進入掛載引擎的根路徑時,應該顯示文章列表。如果將此行插入引擎內的 config/routes.rb
檔案中,便可以實現這一點:
root to: "articles#index"
現在人們只需要到引擎的根目錄來查看所有文章,而不是訪問 /articles
。意思是現在你只需要去 http://localhost:3000/blorgh
,而不是 http://localhost:3000/blorgh/articles
。
3.2 產生 Comments 資源
現在,引擎可以建立新的文章,那麼加上評論功能也是有意義的。為此,你需要建立評論模型、評論控制器,然後修改文章鷹架以顯示評論並允許人們建立新評論。
從應用程式根目錄執行模型產生器。告訴它產生一個 Comment
模型,資料表有兩欄:integer 型別的 article_id
和 text 型別的 text
欄位。
$ bin/rails generate model Comment article_id:integer text:text
這將輸出以下內容:
invoke active_record
create db/migrate/[timestamp]_create_blorgh_comments.rb
create app/models/blorgh/comment.rb
invoke test_unit
create test/models/blorgh/comment_test.rb
create test/fixtures/blorgh/comments.yml
該產生器呼叫將產生它所需的必要模型檔案,將檔案放在命名空間 blorgh
目錄下,並建立一個名為 Blorgh::Comment
的模型類別。現在執行遷移來建立我們的 blorgh_comments 資料表:
$ bin/rails db:migrate
要在文章上顯示評論,編輯 app/views/blorgh/articles/show.html.erb
並在「編輯」連結前加上此行:
<h3>Comments</h3>
<%= render @article.comments %>
這一行需要在 Blorgh::Article
模型中定義一個 has_many
關聯的評論,不過現在還沒有。要定義它,開啟 app/models/blorgh/article.rb
並將這一行加到模型中:
has_many :comments
將模型變成:
module Blorgh
class Article < ApplicationRecord
has_many :comments
end
end
因為
has_many
是在Blorgh
模組的一個類別中定義的,Rails 會知道你想為這些物件使用Blorgh::Comment
模型,所以不需要在這裡指定使用:class_name
選項。
接下來,需要有一個表單,以便可以在文章上建立評論。要加上它,在 app/views/blorgh/articles/show.html.erb
中呼叫呈現 @article.comments
的下方加上這一行:
<%= render "blorgh/comments/form" %>
接下來,這一行將呈現的部分視圖需要存在。建立一個新目錄在 app/views/blorgh/comments
,並且建立一個名為 _form.html.erb
的新檔案,該檔案包含這些內容以便建立所需的部分視圖:
<h3>New comment</h3>
<%= form_with(model: [@article, @article.comments.build], local: true) do |form| %>
<p>
<%= form.label :text %><br>
<%= form.text_area :text %>
</p>
<%= form.submit %>
<% end %>
送交此表單時,它將嘗試對引擎內的 /articles/:article_id/comments
路由執行 POST
請求。此路由目前不存在,但是可以透過將 config/routes.rb
中的 resources :articles
更改為以下幾行來建立:
resources :articles do
resources :comments
end
這會為評論建立一個表單所需要的巢狀路由。
路由現在已經存在了,但是此路由所去的控制器還不存在。要建立它,從應用程式根目錄執行這個指令:
$ bin/rails g controller comments
該指令會輸出以下資訊:
create app/controllers/blorgh/comments_controller.rb
invoke erb
exist app/views/blorgh/comments
invoke test_unit
create test/controllers/blorgh/comments_controller_test.rb
invoke helper
create app/helpers/blorgh/comments_helper.rb
invoke assets
invoke js
create app/assets/javascripts/blorgh/comments.js
invoke css
create app/assets/stylesheets/blorgh/comments.css
該表單會向 /articles/:article_id/comments
發出 POST
請求,對應到 Blorgh::CommentsController
中的 create
動作。這個動作需要被建立,可以透過在 app/controllers/blorgh/comments_controller.rb
的類別定義中放入以下幾行來完成:
def create
@article = Article.find(params[:article_id])
@comment = @article.comments.create(comment_params)
flash[:notice] = "Comment has been created!"
redirect_to articles_path
end
private
def comment_params
params.require(:comment).permit(:text)
end
這是讓新評論表單運作所需的最後一步。然而,顯示評論還不太正確。如果你現在要建立評論,則會看到此錯誤:
Missing partial blorgh/comments/_comment with {:handlers=>[:erb, :builder],
:formats=>[:html], :locale=>[:en, :en]}. Searched in: *
"/Users/ryan/Sites/side_projects/blorgh/test/dummy/app/views" *
"/Users/ryan/Sites/side_projects/blorgh/app/views"
引擎無法找到呈現評論所需的部分視圖。Rails 會先尋找應用程式的(test/dummy
)app/views
目錄,然後在引擎的 app/views
目錄中尋找。當找不到它時,便會拋出這個錯誤。引擎知道要尋找 blorgh/comments/_comment
,因為它接收的模型物件是來自 Blorgh::Comment
類別。
目前,這個部分視圖僅負責呈現評論文字。在 app/views/blorgh/comments/_comment.html.erb
建立新一個檔案,並將此行放入其中:
<%= comment_counter + 1 %>. <%= comment.text %>
comment_counter
區域變數是由呼叫 <%= render @article.comments %>
給我們的,它會自動定義並在迭代每個評論時遞增計數器。在這個例子中,它用於在每個評論建立時顯示一個小數字。
完成了部落格引擎的評論功能。現在是時候在應用程式中使用它了。
4. 安裝到應用程式
在應用程式中使用引擎非常簡單。本節會介紹如何將引擎掛載到應用程式和所需的初始設定,以及將引擎連結到由應用程式提供的 User
類別,以便為引擎中的文章和評論提供所有權。
4.1 掛載引擎
首先,需要在應用程式的 Gemfile
中指定引擎。如果沒有應用程式可以方便地進行測試,可以像這樣在引擎目錄外使用 rails new
指令產生一個:
$ rails new unicorn
通常,可以像一般的 gem 一樣,透過在 Gemfile
內將引擎指定在內來完成。
gem 'devise'
但是,因為你正在本機上開發 blorgh
引擎,你需要在 Gemfile
指定 :path
選項:
gem 'blorgh', path: 'engines/blorgh'
然後執行 bundle
來安裝 gem。
如先前所述,透過將 gem 放在 Gemfile
中,它將會在 Rails 載入時被載入。它會先從引擎載入 lib/blorgh.rb
,然後是 lib/blorgh/engine.rb
,它是定義引擎主要功能的檔案。
要讓引擎的功能可以從應用程式存取,需要將它掛載到應用程式的 config/routes.rb
檔案:
mount Blorgh::Engine, at: "/blog"
這一行會將引擎掛載到應用程式中的 /blog
。當應用程式以 rails server
執行時,可以在 http://localhost:3000/blog
上訪問它。
其他引擎,如 Devise,透過讓你在路由中指定自訂輔助方法(例如
devise_for
)來處理這一點。這些輔助方法完成相同的事,將引擎的部分功能掛載到預先定義可客製化的路徑上。
4.2 安裝引擎
引擎包含需要在應用程式資料庫中建立的 blorgh_articles
和 blorgh_comments
資料表的遷移,以便引擎的模型可以正確查詢它們。要將這些遷移複製到應用程式中,請從 Rails 引擎的 test/dummy
目錄執行以下指令:
$ bin/rails blorgh:install:migrations
如果你有多個需要複製遷移的引擎,請改用 railties:install:migrations
:
$ bin/rails railties:install:migrations
第一次執行這個指令時,將複製引擎中的所有遷移。下次執行時,它只會複製尚未被複製的遷移。第一次執行指令會輸出以下資訊:
Copied migration [timestamp_1]_create_blorgh_articles.blorgh.rb from blorgh
Copied migration [timestamp_2]_create_blorgh_comments.blorgh.rb from blorgh
第一個時間戳記([timestamp_1]
)將是目前時間,第二個時間戳記([timestamp_2]
)將是目前時間加上一秒。之所以這樣做是為了在應用程式中任何現有的遷移之後執行引擎的遷移。
要在應用程式中執行這些遷移,只需執行:bin/rails db:migrate
。當透過 http://localhost:3000/blog
訪問引擎時,文章會是空的。這是因為在應用程式建立的資料表與引擎內建立的資料表不同。繼續,玩玩新掛載的引擎吧。你會發現它和只有一個引擎時是相同的。
如果你只想從一個引擎執行遷移,可以透過指定 SCOPE
來完成:
bin/rails db:migrate SCOPE=blorgh
如果你想在刪除引擎之前還原引擎的遷移,這可能很有用。要還原 blorgh 引擎的所有遷移,你可以執行以下指令:
bin/rails db:migrate SCOPE=blorgh VERSION=0
4.3 使用應用程式提供的類別
4.3.1 使用應用程式提供的模型
引擎建立後,可能需要使用應用程式中的特定類別來提供引擎和應用程式之間的連結。以 blorgh
引擎的情況來說,讓文章和評論有作者會很有意義。
一個典型的應用程式可能有一個 User
類別,用於表示文章或評論的作者。但可能會出現這樣的情況,應用程式以不同名稱命名此類別,如 Person
。因此,引擎不應該明確的為 User
類別寫死關聯。
為了在這種情況下保持簡單,應用程式將有一個名為 User
的類別,來表示應用程式的使用者(我們將進一步進行配置)。它可以在應用程式中使用這個指令產生:
$ bin/rails g model user name:string
需要在這裡執行 bin/rails db:migrate
指令,以確保我們的應用程式有 users
資料表供將來使用。
同樣地,為了簡單起見,文章表單會有一個名為 author_name
的新文字欄位,使用者可以選擇放置他們的名字。引擎將使用這個名稱建立一個新的 User
物件或者尋找一個擁有該名稱的物件。引擎會將文章與找到或建立的 User
物件建立關聯。
首先,author_name
文字欄位需要被加到引擎內的 app/views/blorgh/articles/_form.html.erb
部分視圖。可以在 title
欄位上方加入以下程式碼:
<div class="field">
<%= form.label :author_name %><br>
<%= form.text_field :author_name %>
</div>
接下來,我們需要更新 Blorgh::ArticleController#article_params
方法來允許新的表單參數:
def article_params
params.require(:article).permit(:title, :text, :author_name)
end
Blorgh::Article
模型應該有一些程式碼將 author_name
欄位轉換為一個實際的 User
物件,並在儲存文章前將其關聯為該文章的 author
。它還需要為此欄位設定一個 attr_accessor
,以便為其定義設值(setter)方法和取值(getter)方法。
為此,你需要在 app/models/blorgh/article.rb
為 author_name
加上 attr_accessor
、作者的關聯和 before_validation
呼叫。author
關聯會暫時寫死到 User
類別。
attr_accessor :author_name
belongs_to :author, class_name: "User"
before_validation :set_author
private
def set_author
self.author = User.find_or_create_by(name: author_name)
end
透過用 User
類別表示 author
關聯的物件,在引擎和應用程式之間建立了一個連結。需要有一種方法將 blorgh_articles
資料表中的記錄與 users
資料表中的記錄關聯起來。因為該關聯名為 author
,所以應該在 blorgh_articles
資料表中加上 author_id
欄位。
要產生這個新欄位,請在引擎中執行這個指令:
$ bin/rails g migration add_author_id_to_blorgh_articles author_id:integer
由於遷移的名稱和後面的欄位說明,Rails 會自動知道你想要將欄位加到特定資料表,並將其寫入遷移中。因此不用手動編寫遷移。
這個遷移需要在應用程式上執行。為此,首先必須使用這個指令複製它:
$ bin/rails blorgh:install:migrations
請注意,這裡只複製了一個遷移。這是因為前兩次遷移已經在第一次執行這個指令時被複製了。
NOTE Migration [timestamp]_create_blorgh_articles.blorgh.rb from blorgh has been skipped. Migration with the same name already exists.
NOTE Migration [timestamp]_create_blorgh_comments.blorgh.rb from blorgh has been skipped. Migration with the same name already exists.
Copied migration [timestamp]_add_author_id_to_blorgh_articles.blorgh.rb from blorgh
使用以下指令執行遷移:
$ bin/rails db:migrate
現在,一切都已經到位,我們完成了作者(用 users
資料表中的記錄表示)與文章(用引擎的 blorgh_articles
資料表中的記錄表示)的關聯。
最後,作者的名稱應該會顯示在文章頁面。在 app/views/blorgh/articles/show.html.erb
內的「標題」輸出上方加上以下程式碼:
<p>
<b>Author:</b>
<%= @article.author.name %>
</p>
4.3.2 使用應用程式提供的控制器
預設情況下,由於 Rails 控制器通常會透過繼承 ApplicationController
共享程式碼,例如身份驗證和存取 session 變數。然而,Rails 引擎的作用域與主應用程式是隔離的,因此每個引擎的 ApplicationController
都具有獨立的命名空間。獨立的命名空間可避免程式碼衝突,但是引擎的控制器常常需要存取主應用程式的 ApplicationController
中的方法。提供存取的簡單方法是讓引擎具有命名空間的 ApplicationController
繼承自主應用程式的 ApplicationController
。對於我們的 Blorgh 引擎,可以透過以下改變 app/controllers/blorgh/application_controller.rb
來完成:
module Blorgh
class ApplicationController < ::ApplicationController
end
end
所以,在做出這個改變之後,他們可以訪問主應用程序的ApplicationController
,就好像它們是主應用程序的一部分。
預設情況下,引擎的控制器繼承自 Blorgh::ApplicationController
。因此透過上述修改,它們將能夠存取主應用程式的 ApplicationController
,就好像它們是主應用程式的一部分。
此修改的前提是,引擎需要執行在具有 ApplicationController
的 Rails 應用程式。
4.4 設定引擎
本節介紹如何讓 User
類別成為可配置的,然後介紹引擎的基本配置注意事項。
4.4.1 在應用程式中設定配置
下一步是讓引擎可以客製化在應用程式中所使用的 User
類別。如先前所述,該類別並非總是 User
。為了使這個配置可客製化,引擎將有一個名為 author_class
的配置,它將用於指定哪個類別代表應用程式中的使用者。
要定義此配置,你應該在引擎的 Blorgh
模組中使用 mattr_accessor
。將此行加到引擎內的 lib/blorgh.rb
中:
mattr_accessor :author_class
這個方法的工作原理與 attr_accessor
和 cattr_accessor
類似,會根據指定名稱為模組提供設值(setter)方法和取值(getter)方法。使用時直接呼叫 Blorgh.author_class
即可。
下一步是將 Blorgh::Article
模型切換到這個新設定。修改這個模型(app/models/blorgh/article.rb
)的 belongs_to
關聯:
belongs_to :author, class_name: Blorgh.author_class
Blorgh::Article
模型中的 set_author
方法也應該使用此類別:
self.author = Blorgh.author_class.constantize.find_or_create_by(name: author_name)
為了避免每次都要對 author_class
結果呼叫 constantize
,我們可以在 lib/blorgh.rb
檔案中覆寫 Blorgh
模組的 author_class
取值(getter)方法,在回傳結果之前呼叫 constantize
:
def self.author_class
@@author_class.constantize
end
此時,上述的 set_author
程式碼將變為:
self.author = Blorgh.author_class.find_or_create_by(name: author_name)
修改後的結果更為簡短,行為更加明確。author_class
方法應該總是回傳 Class
物件。
因為修改後的 author_class
方法回傳 Class
而不是 String
,我們還需要修改 Blorgh::Article
模型中的 belongs_to
定義:
belongs_to :author, class_name: Blorgh.author_class.to_s
要在應用程式設定此配置,應該使用初始化程式。透過初始化程式,配置會在應用程式啟動且呼叫引擎的模型前完成設定,這可能取決於現有的配置設定。
在安裝 blorgh
引擎的應用程式中,建立一個新的初始化程式在 config/initializers/blorgh.rb
,並放入以下內容:
Blorgh.author_class = "User"
重要的是這裡使用字串版本的類別,而不是直接使用類別。如果我們直接使用類別,Rails 會嘗試載入該類別並引用相關資料表。如果資料表不存在,可能會導致問題產生。因此,應該使用字串,然後在引擎中透過
constantize
將其轉換為類別。
接下來嘗試新增一篇文章。你會發現過程和之前相同,不過這次引擎使用的是 config/initializers/blorgh.rb
內配置設定的類別。
現在,不用在意使用者類別到底是什麼,只需要確認使用者類別是否實作所需的 API。引擎只要求使用者類別實作了 find_or_create_by
方法,此方法回傳了使用者類別的物件,以便在建立文章時與其相關聯。當然,這個物件應該有某種可以被引用的識別碼。
4.4.2 引擎基本配置
有時候你可能會想在引擎中使用初始化程式、國際化或其它配置選項。這些事情是可以的,因為 Rails 引擎和 Rails 應用程式共享很多相同的功能。事實上,Rails 應用程式的功能實際上是由引擎提供的功能的超集合!
如果你希望使用初始化程式(載入引擎前應該執行的程式碼),儲存的位置是在它的 config/initializers
資料夾。此目錄的功能在配置指南的初始化程式章節有說明,並且與應用程式中的 config/initializers
目錄完全相同。如果你想使用標準的初始化程式,也是一樣的。
對於語系設定,只需將語系檔案放在 config/locales
目錄,如同在應用程式中一樣。
5. 測試引擎
當引擎建立後,會在 test/dummy
中建立一個小型的虛擬應用程式。這個應用程式被用作引擎的裝載點,使引擎測試非常簡單。你可以透過在目錄中產生控制器、模型或視圖來擴充此應用程式,然後用它們來測試你的引擎。
test
目錄和典型的 Rails 測試環境一樣,支援單元測試、功能測試和整合測試。
5.1 功能測試
在編寫功能測試時,一件值得考慮的事是測試會在 test/dummy
應用程式上執行,而不是引擎。這是由測試環境的設定決定的,引擎需要裝載在應用程式才能測試其主要功能,尤其是控制器。也就是說,如果你在控制器的功能測試中像這樣為控制器編寫一個典型的 GET
請求:
module Blorgh
class FooControllerTest < ActionDispatch::IntegrationTest
include Engine.routes.url_helpers
def test_index
get foos_url
...
end
end
end
它的功能還無法正確運作。這是因為應用程式不知道如何將這些請求傳遞到引擎,除非你明確的告訴它如何處理。為此,你必須在程式中將 @routes
實例變數設定為引擎的路由:
module Blorgh
class FooControllerTest < ActionDispatch::IntegrationTest
include Engine.routes.url_helpers
setup do
@routes = Engine.routes
end
def test_index
get foos_url
...
end
end
end
這告訴應用程式你想對此控制器的 index
動作執行一個 GET
請求,但你想使用引擎的路由,而不是應用程式的路由。
這也確保了引擎的 URL 輔助方法能夠在測試中正常運作。
6. 改進引擎功能
本節介紹如何在 Rails 主應用程式中加入或覆寫引擎的 MVC 功能。
6.1 覆寫模型和控制器
在 Rails 主應用程式中,可以透過打開類別來擴充引擎模型和控制器類別(因為模型和控制器類別只是繼承 Rails 特定功能的 Ruby 類別)。透過打開類別,可以根據主應用程式的需求重新定義引擎的類別。通常會透過裝飾器模式來實作。
對於簡單的類別修改,可以使用 Class#class_eval
。對於複雜的類別修改,可以考慮使用 ActiveSupport::Concern
。
6.1.1 使用裝飾器和載入代碼時的注意事項
因為這些裝飾器沒有被 Rails 應用程式本身引用,Rails 的自動載入系統不會載入你的裝飾器。也就是說,你需要自己載入它們。
以下是執行此操作的一些範例程式碼:
# lib/blorgh/engine.rb
module Blorgh
class Engine < ::Rails::Engine
isolate_namespace Blorgh
config.to_prepare do
Dir.glob(Rails.root + "app/decorators/**/*_decorator*.rb").each do |c|
require_dependency(c)
end
end
end
end
這不僅適用於裝飾器,也適用於任何加入到引擎中但未被主應用程式引用的東西。
6.1.2 使用 Class#class_eval 實作裝飾器模式
加入 Article#time_since_created
:
# MyApp/app/decorators/models/blorgh/article_decorator.rb
Blorgh::Article.class_eval do
def time_since_created
Time.current - created_at
end
end
# Blorgh/app/models/article.rb
class Article < ApplicationRecord
has_many :comments
end
覆寫 Article#summary
:
# MyApp/app/decorators/models/blorgh/article_decorator.rb
Blorgh::Article.class_eval do
def summary
"#{title} - #{truncate(text)}"
end
end
# Blorgh/app/models/article.rb
class Article < ApplicationRecord
has_many :comments
def summary
"#{title}"
end
end
6.1.3 使用 ActiveSupport::Concern 實作裝飾器模式
使您可以顯著模塊化您的代碼。
使用 Class#class_eval
非常適合簡單的調整,但是對於更複雜的類別修改,你可能需要考慮使用 ActiveSupport::Concern。ActiveSupport::Concern 能管理互相關聯相依模組和類別執行時的載入順序,讓你可以放心模組化你的程式碼。
加入 Article#time_since_created
並覆寫 Article#summary
:
# MyApp/app/models/blorgh/article.rb
class Blorgh::Article < ApplicationRecord
include Blorgh::Concerns::Models::Article
def time_since_created
Time.current - created_at
end
def summary
"#{title} - #{truncate(text)}"
end
end
# Blorgh/app/models/article.rb
class Article < ApplicationRecord
include Blorgh::Concerns::Models::Article
end
# Blorgh/lib/concerns/models/article.rb
module Blorgh::Concerns::Models::Article
extend ActiveSupport::Concern
# 'included do' 內的程式碼可以在被載入的地方(article.rb)執行,而不是在
# 模組內(blorgh/concerns/models/article)被執行。
included do
attr_accessor :author_name
belongs_to :author, class_name: "User"
before_validation :set_author
private
def set_author
self.author = User.find_or_create_by(name: author_name)
end
end
def summary
"#{title}"
end
module ClassMethods
def some_class_method
'some class method string'
end
end
end
6.2 覆寫視圖
當 Rails 在尋找要呈現的視圖時,它首先會在應用程式的 app/views
目錄中尋找。如果找不到,接著就會在所有引擎的 app/views
目錄中尋找。
當應用程式要呈現 Blorgh::ArticlesController
的 index 動作的視圖時,它會先在應用程式中尋找 app/views/blorgh/articles/index.html.erb
檔案。如果找不到,接著會在引擎中尋找。
你可以透過在應用程式中建立 app/views/blorgh/articles/index.html.erb
檔案來覆寫這個視圖。然後可以完全改變這個視圖輸出的內容。
現在試著建立 app/views/blorgh/articles/index.html.erb
檔案並放入以下內容:
<h1>Articles</h1>
<%= link_to "New Article", new_article_path %>
<% @articles.each do |article| %>
<h2><%= article.title %></h2>
<small>By <%= article.author %></small>
<%= simple_format(article.text) %>
<hr>
<% end %>
6.3 路由
預設情況下,引擎與應用程式的路由是隔離的。這是透過在 Engine
類別中呼叫 isolate_namespace
完成的。也就是說,應用程式及其引擎可以具有相同名稱的路由,並且它們不會發生衝突。
在 config/routes.rb
檔案中,可以在 Engine
類別上定義引擎的路由:
Blorgh::Engine.routes.draw do
resources :articles
end
因為路由是隔離的,如果你想從應用程式中連結到引擎的某個位置時,必須使用引擎的路由代理方法。呼叫一般路由方法如 articles_path
可能會產生非預期的連結,因為應用程式和引擎可能都定義了這個輔助方法。
例如,就以下範例來說,如果是從應用程式呈現樣板,就會指向應用程式的 articles_path
,如果是從引擎呈現樣板,則會指向引擎的 articles_path
:
<%= link_to "Blog articles", articles_path %>
要確保使用的是引擎的 articles_path
路由輔助方法,我們必須透過與引擎相同名稱的路由代理方法來呼叫這個輔助方法。
<%= link_to "Blog articles", blorgh.articles_path %>
如果你想用類似的方式在引擎內引用應用程式,可以使用 main_app
輔助方法:
<%= link_to "Home", main_app.root_path %>
如果你在引擎中使用上述程式碼,它會總是指向應用程式的根目錄。若不使用 main_app
「路由代理」方法,它可能會指向引擎或應用程式的根目錄,取決於從何處呼叫它。
如果嘗試從引擎中呈現的樣板使用應用程式的路由輔助方法,可能會導致呼叫未定義的方法。如果遇到此類問題,請確保你沒有嘗試在引擎內沒有 main_app
前綴的情況下呼叫應用程式的路由輔助方法。
6.4 靜態資源
引擎內的資源與完整應用程式的工作方式相同。由於引擎類別繼承自 Rails::Engine
,應用程式會知道在引擎的 app/assets
和 lib/assets
目錄中尋找靜態資源。
和引擎的所有其它元件一樣,靜態資源應該具有命名空間。也就是說,如果你有一個名為 style.css
的靜態資源,它應該被放在 app/assets/stylesheets/[engine name]/style.css
,而不是 app/assets/stylesheets/style.css
。如果靜態資源不具有命名空間,主應用程式可能會有一個相同名稱的靜態資源,在這種情況下,應用程式的靜態資源會具有較高的優先權,引擎的靜態資源將被忽略。
想像一下,你有一個靜態資源位於 app/assets/stylesheets/blorgh/style.css
,要在應用程式中載入此檔案,只需使用 stylesheet_link_tag
並引用靜態資源即可:
<%= stylesheet_link_tag "blorgh/style.css" %>
你也可以使用 Asset Pipeline 的 require 語法來載入引擎中的靜態資源:
/*
*= require blorgh/style
*/
記住,若要使用 Sass 或 CoffeeScript 等語言,你應該將相關函式庫加到引擎的
.gemspec
。
6.5 獨立的靜態資源和預先編譯
有時候,主應用程式不需要載入引擎的靜態資源。例如,你建立了僅存在於引擎中的管理功能。在這種情況下,主應用程式不需要載入 admin.css
或 admin.js
。只有 gem 的管理後台才需要這些靜態資源。主應用程式在其樣式表中載入 blorgh/admin.css
是沒有意義的。在這種情況下,你應該明確定義這些需要預先編譯的靜態資源。這會告訴 Sprockets 在觸發 bin/rails assets:precompile
時加入你的引擎靜態資源。
你可以在 engine.rb
定義需要預先編譯的靜態資源:
initializer "blorgh.assets.precompile" do |app|
app.config.assets.precompile += %w( admin.js admin.css )
end
更多相關資訊,請參閱 Asset Pipeline 指南。
6.6 其它相依 Gem
引擎的相依套件應該在引擎跟目錄下的 .gemspec
檔案中指定。因為我們可能會以 gem 的方式安裝引擎。如果在引擎的 Gemfile
指定相依套件,那麼 gem install 將無法識別這些相依關係,因此它們不會被安裝,從而導致引擎發生故障。
要指定當 gem install
時應該和引擎一起安裝的相依套件,只需要在引擎中 .gemspec
檔案的 Gem::Specification
區塊內指定:
s.add_dependency "moo"
還可以像這樣指定僅用於開發環境的相依套件:
s.add_development_dependency "moo"
在應用程式內執行 bundle install
時,這兩種相依套件都會被安裝。用於開發環境的相依套件只有在執行引擎測試時才會使用。
注意,如果要在引擎載入時立即載入相依套件,你應該在引擎初始化前就載入它們。例如:
require 'other_engine/engine'
require 'yet_another_engine/engine'
module MyEngine
class Engine < ::Rails::Engine
end
end
所以當你過早載入某些框架時(例如,ActiveRecord::Base),你就違反了使用 Rails 應用程式的使用慣例
7. Active Support 的 on_load Hook
Active Support 是 Ruby on Rails 元件,負責提供 Ruby 語言擴充套件和公用程式。
Rails 程式碼通常可以在應用程式載入時引用。Rails 負責這些框架的載入順序,所以當你過早載入某些框架時(例如 ActiveRecord::Base
),就違反了 Rails 應用程式的使用慣例。此外,透過在應用程式啟動時載入如 ActiveRecord::Base
等程式碼,你正在載入整個框架,這可能會降低啟動時間,並可能導致載入順序與應用程式啟動發生衝突。
on_load
API 能夠讓我們在初始化的流程中掛載你所需要的功能,而不違反 Rails 所制定的規則。這也將緩解啟動效能下降並避免衝突。
8. 什麼是 on_load Hook?
由於 Ruby 是動態語言,因此某些程式碼會導致載入相關的 Rails 元件。以此程式碼片段為例:
ActiveRecord::Base.include(MyActiveRecordHelper)
當這段程式碼片段被載入時,發現有 ActiveRecord::Base
,因此 Ruby 會去尋找該常數的定義並載入它。這將導致整個 Active Record 框架在啟動時被載入。
ActiveSupport.on_load
是一種延遲載入程式碼的機制,直到真正需要時才載入。上述代碼可以修改為:
ActiveSupport.on_load(:active_record) { include MyActiveRecordHelper }
新的程式碼片段會在 ActiveRecord::Base
載入後才載入 MyActiveRecordHelper
。
9. 它是如何運作的?
在 Rails 框架中,載入特定函式庫時會呼叫這些 hook。例如,當 ActionController::Base
被載入時,會呼叫 :action_controller_base
hook。也就是說,ActiveSupport.on_load
呼叫設定用的 :action_controller_base
hook 會在 ActionController::Base
環境中被呼叫。(因此 self
是 ActionController::Base
的實例變數)。
10. 修改程式碼,使用 on_load Hook
修改程式碼很簡單。如果你有一行引用 Rails 框架的程式碼,例如 ActiveRecord::Base
,你可以將該程式碼包裝在 on_load
hook 中。
10.1 範例 1
ActiveRecord::Base.include(MyActiveRecordHelper)
改為
ActiveSupport.on_load(:active_record) { include MyActiveRecordHelper } # self 在這裡指的是 ActiveRecord::Base,所以可以直接呼叫 #include
10.2 範例 2
ActionController::Base.prepend(MyActionControllerHelper)
改為
ActiveSupport.on_load(:action_controller_base) { prepend MyActionControllerHelper } # self 在這裡指的是 ActionController::Base,所以可以直接呼叫 #prepend
10.3 範例 3
ActiveRecord::Base.include_root_in_json = true
改為
ActiveSupport.on_load(:active_record) { self.include_root_in_json = true } # self 在這裡指的是 ActiveRecord::Base
11. 可用的 Hook
這些是可以在程式碼中使用的 hook。
要接上以下某個類別的初始化過程,請使用可用的 hook。
類別 | 可用的 Hook |
---|---|
ActionCable |
action_cable |
ActionController::API |
action_controller_api |
ActionController::API |
action_controller |
ActionController::Base |
action_controller_base |
ActionController::Base |
action_controller |
ActionController::TestCase |
action_controller_test_case |
ActionDispatch::IntegrationTest |
action_dispatch_integration_test |
ActionDispatch::SystemTestCase |
action_dispatch_system_test_case |
ActionMailer::Base |
action_mailer |
ActionMailer::TestCase |
action_mailer_test_case |
ActionView::Base |
action_view |
ActionView::TestCase |
action_view_test_case |
ActiveJob::Base |
active_job |
ActiveJob::TestCase |
active_job_test_case |
ActiveRecord::Base |
active_record |
ActiveSupport::TestCase |
active_support_test_case |
i18n |
i18n |
12. 配置 Hook
這些是可用的配置 hook。它們並沒有涉及到任何特定的框架,而是在整個應用程式中執行。
Hook | 使用案例 |
---|---|
before_configuration |
第一個執行的配置區塊。在所有初始化程式執行前呼叫。 |
before_initialize |
第二個執行的配置區塊。框架初始化前呼叫。 |
before_eager_load |
第三個執行的配置區塊。config.eager_load 設為 false 時不執行。 |
after_initialize |
最後執行的配置區塊。框架初始化完成後呼叫。 |
12.1 範例
config.before_configuration { puts 'I am called before any initializers' }