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
選項
- 資源清單檔案(
blorgh_manifest.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
目錄新增至模型、郵件程式、控制器和視圖的載入路徑等任務。
此處的 isolate_namespace
方法值得特別注意。此呼叫負責將控制器、模型、路由和其他元件隔離到它們自己的命名空間中,使其與應用程式內相似的元件分開。如果沒有這個機制,引擎的元件有可能「洩漏」到應用程式中,導致不必要的干擾,或者重要的引擎元件可能被應用程式內類似名稱的物件覆蓋。其中一個衝突的例子是 helpers。如果沒有呼叫 isolate_namespace
,引擎的 helpers 將會被包含在應用程式的控制器中。
強烈建議將 isolate_namespace
這行程式碼保留在 Engine
類別定義中。如果沒有它,引擎中產生的類別可能會與應用程式發生衝突。
命名空間隔離的意義在於,透過呼叫 bin/rails generate model
生成的模型,例如 bin/rails generate model article
,不會被稱為 Article
,而是會被命名空間化並稱為 Blorgh::Article
。此外,模型的資料表也會被命名空間化,變成 blorgh_articles
,而不是單純的 articles
。與模型命名空間類似,名為 ArticlesController
的控制器會變成 Blorgh::ArticlesController
,而該控制器的視圖將不會在 app/views/articles
,而是在 app/views/blorgh/articles
。Mailers、jobs 和 helpers 也會被命名空間化。
最後,路由也會在引擎內被隔離。這是命名空間最重要的部分之一,將在本指南的 路由 章節中進一步討論。
2.1.2 app
目錄
在 app
目錄中,您會看到標準的 assets
、controllers
、helpers
、jobs
、mailers
、models
和 views
目錄,您應該對這些目錄很熟悉,它們與應用程式中的目錄相似。我們將在後續章節中,在撰寫引擎時,更深入地探討模型。
在 app/assets
目錄內,有 images
和 stylesheets
目錄,您應該也對這些目錄很熟悉,因為它們與應用程式中的目錄相似。然而,這裡有一個不同之處,每個目錄都包含一個以引擎名稱命名的子目錄。由於這個引擎將被命名空間化,它的 assets 也應該如此。
在 app/controllers
目錄內,有一個 blorgh
目錄,其中包含一個名為 application_controller.rb
的檔案。此檔案將為引擎的控制器提供任何通用功能。blorgh
目錄是引擎的其他控制器將放置的地方。透過將它們放置在這個命名空間化的目錄中,您可以防止它們可能與其他引擎甚至應用程式中名稱相同的控制器發生衝突。
引擎內的 ApplicationController
類別的命名方式與 Rails 應用程式相同,以便讓您更容易將您的應用程式轉換為引擎。
如同 app/controllers
,您會在 app/helpers
、app/jobs
、app/mailers
和 app/models
目錄下找到一個 blorgh
子目錄,其中包含相關的 application_*.rb
檔案,用於收集通用功能。透過將您的檔案放置在這個子目錄下並命名空間化您的物件,您可以防止它們可能與其他引擎甚至應用程式中名稱相同的元素發生衝突。
最後,app/views
目錄包含一個 layouts
資料夾,其中包含 blorgh/application.html.erb
檔案。此檔案允許您為引擎指定一個版面配置。如果這個引擎將作為獨立引擎使用,那麼您應該在這個檔案中添加任何自訂設定,而不是在應用程式的 app/views/layouts/application.html.erb
檔案中。
如果您不想強迫引擎的使用者使用某個版面配置,您可以刪除此檔案,並在引擎的控制器中參考不同的版面配置。
2.1.3 bin
目錄
此目錄包含一個檔案,bin/rails
,它允許您像在應用程式中一樣使用 rails
子命令和產生器。這表示您將能夠透過執行以下命令,輕鬆地為此引擎產生新的控制器和模型
$ bin/rails generate model
請記住,當然,在 Engine
類別中具有 isolate_namespace
的引擎內使用這些命令產生的任何東西都將被命名空間化。
2.1.4 test
目錄
test
目錄是引擎測試將放置的地方。為了測試引擎,其中內嵌了一個精簡版的 Rails 應用程式,位於 test/dummy
。此應用程式將在 test/dummy/config/routes.rb
檔案中掛載引擎。
Rails.application.routes.draw do
mount Blorgh::Engine => "/blorgh"
end
這行程式碼將引擎掛載在路徑 /blorgh
,這將使其僅能透過該路徑在應用程式中存取。
在 test 目錄內,有一個 test/integration
目錄,引擎的整合測試應該放在這裡。您也可以在 test
目錄中建立其他目錄。例如,您可能希望為您的模型測試建立一個 test/models
目錄。
3 提供引擎功能
本指南涵蓋的引擎提供提交文章和評論功能,並遵循與 入門指南 類似的脈絡,並有一些新的變化。
對於此章節,請確保在 blorgh
引擎目錄的根目錄中執行命令。
3.1 產生文章資源
為部落格引擎產生的第一件事是 Article
模型和相關的控制器。為了快速產生這些,您可以使用 Rails scaffold 產生器。
$ 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
create app/views/blorgh/articles/_article.html.erb
invoke resource_route
invoke test_unit
create test/controllers/blorgh/articles_controller_test.rb
create test/system/blorgh/articles_test.rb
invoke helper
create app/helpers/blorgh/articles_helper.rb
invoke test_unit
scaffold 產生器做的第一件事是呼叫 active_record
產生器,它會為資源產生一個遷移和一個模型。然而,請注意,遷移被稱為 create_blorgh_articles
,而不是通常的 create_articles
。這是由於在 Blorgh::Engine
類別的定義中呼叫了 isolate_namespace
方法。此處的模型也被命名空間化,放置在 app/models/blorgh/article.rb
而不是 app/models/article.rb
,原因是在 Engine
類別中呼叫了 isolate_namespace
。
接下來,會為此模型呼叫 test_unit
產生器,在 test/models/blorgh/article_test.rb
(而不是 test/models/article_test.rb
) 產生模型測試,並在 test/fixtures/blorgh/articles.yml
(而不是 test/fixtures/articles.yml
) 產生 fixture。
之後,會將資源的一行插入到引擎的 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
和 test/system/blorgh/articles_test.rb
) 和一個 helper (app/helpers/blorgh/articles_helper.rb
)。
此產生器建立的所有內容都被妥善地命名空間化。控制器的類別定義在 Blorgh
模組中
module Blorgh
class ArticlesController < ApplicationController
# ...
end
end
ArticlesController
類別繼承自 Blorgh::ApplicationController
,而不是應用程式的 ApplicationController
。
app/helpers/blorgh/articles_helper.rb
內的 helper 也被命名空間化
module Blorgh
module ArticlesHelper
# ...
end
end
這有助於防止與任何其他也可能具有文章資源的引擎或應用程式發生衝突。
您可以在引擎的根目錄執行 bin/rails db:migrate
來執行 scaffold 產生器產生的遷移,然後在 test/dummy
中執行 bin/rails server
,來查看引擎目前的樣子。當您開啟 https://127.0.0.1:3000/blorgh/articles
時,您會看到產生的預設 scaffold。點擊看看!您剛剛產生了第一個引擎的第一個功能。
如果您寧願在主控台中操作,bin/rails console
也會像 Rails 應用程式一樣運作。請記住:Article
模型已被命名空間化,因此要參考它,您必須將其稱為 Blorgh::Article
。
irb> Blorgh::Article.find(1)
=> #<Blorgh::Article id: 1 ...>
最後一件事是,此引擎的 articles
資源應該是引擎的根目錄。每當有人前往引擎掛載的根路徑時,應該顯示文章列表。如果將以下這行插入引擎內的 config/routes.rb
檔案中,就可以達成這個目的
root to: "articles#index"
現在人們只需要前往引擎的根目錄即可看到所有文章,而不需要訪問 /articles
。這表示現在您只需要前往 https://127.0.0.1:3000/blorgh
,而不是 https://127.0.0.1:3000/blorgh/articles
。
3.2 產生評論資源
現在引擎可以建立新文章,新增評論功能也合情合理。要做到這一點,您需要產生一個評論模型、一個評論控制器,然後修改文章 scaffold 以顯示評論並允許人們建立新評論。
從引擎根目錄執行模型產生器。告知它產生一個 Comment
模型,相關的資料表有兩個欄位:一個 article_id
整數和一個 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
模型上定義 comments 的 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
中對 render @article.comments
的呼叫下方
<%= render "blorgh/comments/form" %>
接下來,需要存在這行程式碼將會呈現的 partial。在 app/views/blorgh/comments
建立一個新目錄,並在其中建立一個名為 _form.html.erb
的新檔案,其中包含建立所需 partial 的內容
<h3>New comment</h3>
<%= form_with model: [@article, @article.comments.build] do |form| %>
<p>
<%= form.label :text %><br>
<%= form.textarea :text %>
</p>
<%= form.submit %>
<% end %>
當提交此表單時,它會嘗試向引擎內的 /articles/:article_id/comments
路徑發送 POST
請求。目前這個路徑不存在,但可以透過將 config/routes.rb
內的 resources :articles
行變更為以下幾行來建立:
resources :articles do
resources :comments
end
這會為留言建立巢狀路由,這也是表單所需要的。
現在路由存在了,但這個路由指向的控制器還不存在。要建立它,請在引擎根目錄執行此命令:
$ bin/rails generate 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 test_unit
表單將向 /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.expect(comment: [: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
通常,在 Gemfile
中指定引擎會像指定一般的 gem 一樣。
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
路徑上。當應用程式以 bin/rails server
執行時,可以透過 https://127.0.0.1:3000/blog
存取。
其他引擎,例如 Devise,則會以稍微不同的方式處理,要求您在路由中指定自訂的輔助方法(例如 devise_for
)。這些輔助方法的功能完全相同,只是將引擎的功能掛載在預先定義的路徑上,而這個路徑可能是可自訂的。
4.2 引擎設定
引擎包含 blorgh_articles
和 blorgh_comments
資料表的遷移,這些遷移需要在應用程式的資料庫中建立,引擎的模型才能正確查詢它們。若要將這些遷移複製到應用程式中,請從應用程式的根目錄執行以下命令:
$ bin/rails blorgh:install:migrations
如果有多個引擎需要複製遷移,請改用 railties:install:migrations
:
$ bin/rails railties:install:migrations
您可以透過指定 MIGRATIONS_PATH 在來源引擎中為遷移指定自訂路徑。
$ bin/rails railties:install:migrations MIGRATIONS_PATH=db_blourgh
如果您有多個資料庫,也可以透過指定 DATABASE 來指定目標資料庫。
$ bin/rails railties:install:migrations DATABASE=animals
當第一次執行此命令時,它會複製引擎中的所有遷移。下次執行時,它只會複製尚未複製的遷移。第一次執行此命令會輸出類似以下內容:
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
。當透過 https://127.0.0.1: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 generate model user name:string
這裡需要執行 bin/rails db:migrate
命令,以確保我們的應用程式具有 users
資料表供未來使用。
此外,為了簡化起見,文章表單會有一個新的文字欄位,名為 author_name
,使用者可以在其中選擇輸入自己的名稱。然後,引擎會取得這個名稱,並使用它建立新的 User
物件,或是找到已經有該名稱的物件。然後,引擎會將文章與找到或建立的 User
物件關聯起來。
首先,需要在引擎內的 app/views/blorgh/articles/_form.html.erb
局部視圖中加入 author_name
文字欄位。可以使用以下程式碼將其加入 title
欄位的上方:
<div class="field">
<%= form.label :author_name %><br>
<%= form.text_field :author_name %>
</div>
接下來,我們需要更新 Blorgh::ArticlesController#article_params
方法,以允許新的表單參數:
def article_params
params.expect(article: [: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
呼叫。作者關聯暫時會硬式編碼為 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 generate 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/_article.html.erb
內的「標題」輸出上方加入以下程式碼:
<p>
<strong>Author:</strong>
<%= article.author.name %>
</p>
4.3.2 使用應用程式提供的控制器
由於 Rails 控制器通常會為驗證和存取會話變數等事物共用程式碼,因此預設會繼承自 ApplicationController
。然而,Rails 引擎的作用域是獨立於主要應用程式運作,因此每個引擎都會取得一個作用域的 ApplicationController
。這個命名空間可以避免程式碼衝突,但通常引擎控制器需要存取主要應用程式的 ApplicationController
中的方法。提供此存取權的簡單方法是將引擎的作用域 ApplicationController
變更為繼承自主要應用程式的 ApplicationController
。對於我們的 Blorgh 引擎,可以透過變更 app/controllers/blorgh/application_controller.rb
使其如下所示:
module Blorgh
class ApplicationController < ::ApplicationController
end
end
預設情況下,引擎的控制器會繼承自 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"
這裡非常重要的一點是,要使用類別的 String
版本,而不是類別本身。如果您使用類別,Rails 會嘗試載入該類別,然後參考相關的資料表。如果資料表還不存在,這可能會導致問題。因此,應該使用 String
,然後稍後在引擎中使用 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 覆寫模型和控制器
父應用程式可以重新開啟引擎模型和控制器,以擴展或裝飾它們。
覆寫可以組織在專用的 app/overrides
目錄中,該目錄會被自動載入器忽略,並在 to_prepare
回呼中預先載入。
# config/application.rb
module MyApp
class Application < Rails::Application
# ...
overrides = "#{Rails.root}/app/overrides"
Rails.autoloaders.main.ignore(overrides)
config.to_prepare do
Dir.glob("#{overrides}/**/*_override.rb").sort.each do |override|
load override
end
end
end
end
6.1.1 使用 class_eval
重新開啟現有類別
例如,為了覆寫引擎模型:
# Blorgh/app/models/blorgh/article.rb
module Blorgh
class Article < ApplicationRecord
# ...
end
end
您只需建立一個重新開啟該類別的檔案:
# MyApp/app/overrides/models/blorgh/article_override.rb
Blorgh::Article.class_eval do
# ...
end
非常重要的是,覆寫要重新開啟類別或模組。如果類別或模組還沒有在記憶體中,使用 class
或 module
關鍵字會定義它們,這是錯誤的,因為定義存在於引擎中。如上所示使用 class_eval
可確保您正在重新開啟。
6.1.2 使用 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/blorgh/article.rb
module Blorgh
class Article < ApplicationRecord
include Blorgh::Concerns::Models::Article
end
end
# Blorgh/lib/concerns/models/article.rb
module Blorgh::Concerns::Models::Article
extend ActiveSupport::Concern
# `included do` causes the block to be evaluated in the context
# in which the module is included (i.e. Blorgh::Article),
# rather than in the module itself.
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 自動載入和引擎
請查看自動載入和重新載入常數指南,以獲取有關自動載入和引擎的更多資訊。
6.3 覆寫視圖
當 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.4 路由
引擎內的路由預設會與應用程式隔離。這是透過 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.5 資源
引擎內的資源與完整的應用程式的運作方式相同。由於引擎類別繼承自 Rails::Engine
,應用程式將知道在引擎的 app/assets
和 lib/assets
目錄中查找資源。
與引擎的所有其他元件一樣,資源應該被命名空間。這意味著,如果您有一個名為 style.css
的資源,則應將其放在 app/assets/stylesheets/[引擎名稱]/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.6 分離資源和預先編譯
在某些情況下,主應用程式不需要您的引擎資源。例如,假設您建立了一個僅適用於您的引擎的管理功能。在這種情況下,主應用程式不需要 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
有關更多資訊,請閱讀資源管道指南。
6.7 其他 Gem 相依性
引擎內的 Gem 相依性應在引擎根目錄的 .gemspec
檔案中指定。原因是引擎可能會作為 gem 安裝。如果相依性在 Gemfile
中指定,則傳統的 gem 安裝將無法辨識這些相依性,因此它們不會被安裝,從而導致引擎無法正常運作。
若要指定在傳統的 gem install
期間應與引擎一起安裝的依賴項,請在引擎的 .gemspec
檔案內的 Gem::Specification
區塊中指定它。
s.add_dependency "moo"
若要指定只應作為應用程式的開發依賴項安裝的依賴項,請這樣指定:
s.add_development_dependency "moo"
當在應用程式內執行 bundle install
時,兩種依賴項都會被安裝。gem 的開發依賴項只會在引擎的開發和測試運行時使用。
請注意,如果希望在引擎被 require 時立即 require 依賴項,您應該在引擎初始化之前 require 它們。例如:
require "other_engine/engine"
require "yet_another_engine/engine"
module MyEngine
class Engine < ::Rails::Engine
end
end