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 Core Team 和其他許多人的努力,引擎就不可能實現。如果您遇到他們,別忘了向他們表示感謝!
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
的虛擬測試應用程式的路由檔案 test/dummy/config/routes.rb
,將引擎掛載到虛擬測試應用程式內部
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
方法值得特別注意。這個呼叫負責將控制器、模型、路由和其他項目隔離到自己的命名空間中,遠離應用程式內部的類似元件。如果不這樣做,引擎的元件就有可能「外洩」到應用程式中,造成不必要的干擾,或者重要的引擎元件可能會被應用程式中名稱類似的項目覆寫。此類衝突的一個範例是輔助程式。如果不呼叫 isolate_namespace
,引擎的輔助程式會包含在應用程式的控制器中。
強烈建議將 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
。郵件、工作和輔助程式也會加上命名空間。
最後,路由也會在引擎中隔離。這是命名空間最重要的部分之一,會在這個指南的 路由 部分中進一步說明。
2.1.2 app
目錄
在 app
目錄中,有標準的 assets
、controllers
、helpers
、jobs
、mailers
、models
和 views
目錄,這些目錄你應該從應用程式中很熟悉。我們會在撰寫引擎時,在未來的部分中更深入探討模型。
在 app/assets
目錄中,有 images
和 stylesheets
目錄,你應該因為它們與應用程式中的類似性而很熟悉。然而,這裡有一個不同之處,每個目錄都包含一個具有引擎名稱的子目錄。由於這個引擎會加上命名空間,因此它的資產也應該這樣做。
在 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/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
方法。此處的模型也有命名空間,由於 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
和 test/system/blorgh/articles_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
這有助於避免與其他可能也有文章資源的引擎或應用程式產生衝突。
您可以執行 bin/rails db:migrate
於引擎根目錄,執行由鷹架產生器產生的遷移,然後在 test/dummy
中執行 bin/rails server
,就能看到引擎目前的狀態。當您開啟 https://127.0.0.1:3000/blorgh/articles
時,您會看到已產生的預設鷹架。到處點點看!您剛剛產生了第一個引擎的第一個功能。
如果您比較喜歡在主控台中操作,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 產生評論資源
現在引擎可以建立新文章了,接下來當然就是加上評論功能。要做到這一點,你需要產生一個評論模型、一個評論控制器,然後修改文章鷹架以顯示評論並允許人們建立新評論。
從引擎根目錄執行模型產生器。告訴它產生一個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
模型中定義一個評論的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" %>
接下來,這行文字要呈現的局部檔案必須存在。在 app/views/blorgh/comments
中建立一個新目錄,並在其中建立一個名為 _form.html.erb
的新檔案,其中包含這段內容,以建立所需的局部檔案
<h3>New comment</h3>
<%= form_with model: [@article, @article.comments.build] 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 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.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
通常,在 Gemfile
內指定引擎會將它指定為一個正常的、日常的寶石。
gem 'devise'
但是,因為您在您的本機電腦上開發 blorgh
引擎,您需要在您的 Gemfile
中指定 :path
選項
gem 'blorgh', path: 'engines/blorgh'
然後執行 bundle
來安裝寶石。
如前所述,透過將寶石放入 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
物件關聯。
首先,需要將 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::ArticlesController#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 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
類似,但會在模組上提供一個具有指定名稱的設定值和取得值的方法。若要使用它,必須使用 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
的索引動作的檢視時,它會先在應用程式中尋找路徑 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
。只有寶石的管理員配置需要這些資產。主機應用程式包含 "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.7 其他寶石相依性
引擎內部的 Gem 依賴項應在引擎根目錄的 .gemspec
檔案中指定。原因是引擎可能會以 Gem 安裝。如果依賴項是在 Gemfile
中指定,傳統的 Gem 安裝將無法辨識這些依賴項,因此不會安裝它們,導致引擎無法正常運作。
若要指定在傳統 gem install
期間應與引擎一起安裝的依賴項,請在引擎的 .gemspec
檔案中的 Gem::Specification
區塊中指定。
s.add_dependency "moo"
若要指定應僅安裝為應用程式的開發依賴項,請像這樣指定
s.add_development_dependency "moo"
當在應用程式內執行 bundle install
時,這兩種依賴項都會安裝。Gem 的開發依賴項僅在執行引擎的開發和測試時使用。
請注意,如果你想在需要引擎時立即需要依賴項,你應在引擎初始化之前需要它們。例如
require "other_engine/engine"
require "yet_another_engine/engine"
module MyEngine
class Engine < ::Rails::Engine
end
end
7 載入和組態掛鉤
Rails 程式碼通常可以在載入應用程式時參照。Rails 負責這些架構的載入順序,所以當你過早載入架構(例如 ActiveRecord::Base
)時,你違反了應用程式與 Rails 之間的隱含合約。此外,在應用程式啟動時載入 ActiveRecord::Base
等程式碼,會載入整個架構,這可能會減慢你的啟動時間,並可能導致載入順序和應用程式啟動時發生衝突。
載入和組態掛鉤是允許你掛入此初始化程序的 API,而不會違反與 Rails 的載入合約。這也會減輕啟動效能降低,並避免衝突。
7.1 避免載入 Rails 架構
由於 Ruby 是動態語言,某些程式碼會導致不同的 Rails 架構載入。例如,看看這個程式片段
ActiveRecord::Base.include(MyActiveRecordHelper)
這段程式碼片段表示當載入這個檔案時,會遇到 ActiveRecord::Base
。這個遭遇會導致 Ruby 尋找該常數的定義並載入它。這會導致整個 Active Record 框架在開機時載入。
ActiveSupport.on_load
是一種機制,可用於延遲載入程式碼,直到實際需要為止。上面的程式碼片段可以變更為
ActiveSupport.on_load(:active_record) do
include MyActiveRecordHelper
end
這個新的程式碼片段只會在載入 ActiveRecord::Base
時包含 MyActiveRecordHelper
。
7.2 何時呼叫 Hooks?
在 Rails 框架中,這些 Hooks 會在載入特定函式庫時呼叫。例如,當載入 ActionController::Base
時,會呼叫 :action_controller_base
Hook。這表示所有帶有 :action_controller_base
Hooks 的 ActiveSupport.on_load
呼叫都將在 ActionController::Base
的內容中呼叫(表示 self
將會是 ActionController::Base
)。
7.3 修改程式碼以使用載入 Hooks
修改程式碼通常很簡單。如果您有一行程式碼參照 Rails 框架,例如 ActiveRecord::Base
,您可以將該程式碼包裝在載入 Hook 中。
修改對 include
的呼叫
ActiveRecord::Base.include(MyActiveRecordHelper)
變為
ActiveSupport.on_load(:active_record) do
# self refers to ActiveRecord::Base here,
# so we can call .include
include MyActiveRecordHelper
end
修改對 prepend
的呼叫
ActionController::Base.prepend(MyActionControllerHelper)
變為
ActiveSupport.on_load(:action_controller_base) do
# self refers to ActionController::Base here,
# so we can call .prepend
prepend MyActionControllerHelper
end
修改對類別方法的呼叫
ActiveRecord::Base.include_root_in_json = true
變為
ActiveSupport.on_load(:active_record) do
# self refers to ActiveRecord::Base here
self.include_root_in_json = true
end
7.4 可用的載入 Hooks
這些是您可以在自己的程式碼中使用的載入 Hooks。若要連接到下列其中一個類別的初始化程序,請使用可用的 Hook。
類別 | Hook |
---|---|
ActionCable |
action_cable |
ActionCable::Channel::Base |
action_cable_channel |
ActionCable::Connection::Base |
action_cable_connection |
ActionCable::Connection::TestCase |
action_cable_connection_test_case |
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::Response |
action_dispatch_response |
ActionDispatch::Request |
action_dispatch_request |
ActionDispatch::SystemTestCase |
action_dispatch_system_test_case |
ActionMailbox::Base |
action_mailbox |
ActionMailbox::InboundEmail |
action_mailbox_inbound_email |
ActionMailbox::Record |
action_mailbox_record |
ActionMailbox::TestCase |
action_mailbox_test_case |
ActionMailer::Base |
action_mailer |
ActionMailer::TestCase |
action_mailer_test_case |
ActionText::Content |
action_text_content |
ActionText::Record |
action_text_record |
ActionText::RichText |
action_text_rich_text |
ActionText::EncryptedRichText |
action_text_encrypted_rich_text |
ActionView::Base |
action_view |
ActionView::TestCase |
action_view_test_case |
ActiveJob::Base |
active_job |
ActiveJob::TestCase |
active_job_test_case |
ActiveModel::Model |
active_model |
ActiveRecord::Base |
active_record |
ActiveRecord::TestFixtures |
active_record_fixtures |
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter |
active_record_postgresqladapter |
ActiveRecord::ConnectionAdapters::Mysql2Adapter |
active_record_mysql2adapter |
ActiveRecord::ConnectionAdapters::TrilogyAdapter |
active_record_trilogyadapter |
ActiveRecord::ConnectionAdapters::SQLite3Adapter |
active_record_sqlite3adapter |
ActiveStorage::Attachment |
active_storage_attachment |
ActiveStorage::VariantRecord |
active_storage_variant_record |
ActiveStorage::Blob |
active_storage_blob |
ActiveStorage::Record |
active_storage_record |
ActiveSupport::TestCase |
active_support_test_case |
i18n |
i18n |
7.5 可用設定掛勾
設定掛勾不會掛入任何特定架構,而是在整個應用程式的背景下執行。
Hook | 使用案例 |
---|---|
before_configuration |
第一個可設定區塊執行。在執行任何初始化器之前呼叫。 |
before_initialize |
在框架初始化之前執行的第二個可設定區塊。 |
before_eager_load |
在框架初始化之前執行的第三個可設定區塊。如果將 config.eager_load 設為 false,則不會執行。 |
after_initialize |
在框架初始化之後執行的最後一個可設定區塊。 |
可以在 Engine 類別中呼叫設定掛勾。
module Blorgh
class Engine < ::Rails::Engine
config.before_configuration do
puts 'I am called before any initializers'
end
end
end
回饋
歡迎您協助提升本指南的品質。
如果您發現任何錯字或事實錯誤,請協助我們修正。您可以參閱我們的 文件貢獻 章節,了解如何開始。
您也可能會發現不完整或過時的內容。請務必為 main 新增任何遺漏的文件。請先查看 Edge Guides,以確認問題是否已在 main 分支中修正。請查看 Ruby on Rails 指南準則,了解格式和慣例。
如果您發現需要修正的地方,但無法自行修正,請 開啟問題。
最後,歡迎在 官方 Ruby on Rails 論壇 上討論任何與 Ruby on Rails 文件相關的事項。