更多資訊請參閱 rubyonrails.org:

開始使用引擎

在本指南中,您將學習有關引擎的知識,以及如何透過乾淨且非常易於使用的介面,為其主機應用程式提供額外功能。

閱讀本指南後,您將了解

  • 是什麼構成引擎。
  • 如何產生引擎。
  • 如何為引擎建立功能。
  • 如何將引擎掛接到應用程式中。
  • 如何覆寫應用程式中的引擎功能。
  • 如何透過載入和配置掛鉤避免載入 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 選項

  • 資源清單檔案(blorgh_manifest.jsapplication.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 目錄中,您會看到標準的 assetscontrollershelpersjobsmailersmodelsviews 目錄,您應該對這些目錄很熟悉,它們與應用程式中的目錄相似。我們將在後續章節中,在撰寫引擎時,更深入地探討模型。

app/assets 目錄內,有 imagesstylesheets 目錄,您應該也對這些目錄很熟悉,因為它們與應用程式中的目錄相似。然而,這裡有一個不同之處,每個目錄都包含一個以引擎名稱命名的子目錄。由於這個引擎將被命名空間化,它的 assets 也應該如此。

app/controllers 目錄內,有一個 blorgh 目錄,其中包含一個名為 application_controller.rb 的檔案。此檔案將為引擎的控制器提供任何通用功能。blorgh 目錄是引擎的其他控制器將放置的地方。透過將它們放置在這個命名空間化的目錄中,您可以防止它們可能與其他引擎甚至應用程式中名稱相同的控制器發生衝突。

引擎內的 ApplicationController 類別的命名方式與 Rails 應用程式相同,以便讓您更容易將您的應用程式轉換為引擎。

如同 app/controllers,您會在 app/helpersapp/jobsapp/mailersapp/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.rbtest/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_articlesblorgh_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_nameattr_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_accessorcattr_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

非常重要的是,覆寫要重新開啟類別或模組。如果類別或模組還沒有在記憶體中,使用 classmodule 關鍵字會定義它們,這是錯誤的,因為定義存在於引擎中。如上所示使用 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/assetslib/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.cssadmin.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


回到頂部