v7.1.3.2
更多資訊請至 rubyonrails.org: 更多 Ruby on 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 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.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 的虛擬測試應用程式的路由檔案 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 目錄中,有標準的 assetscontrollershelpersjobsmailersmodelsviews 目錄,這些目錄你應該從應用程式中很熟悉。我們會在撰寫引擎時,在未來的部分中更深入探討模型。

app/assets 目錄中,有 imagesstylesheets 目錄,你應該因為它們與應用程式中的類似性而很熟悉。然而,這裡有一個不同之處,每個目錄都包含一個具有引擎名稱的子目錄。由於這個引擎會加上命名空間,因此它的資產也應該這樣做。

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/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.rbtest/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_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 物件關聯。

首先,需要將 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_nameattr_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_accessorcattr_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

覆寫重新開啟類別或模組非常重要。如果使用 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 的索引動作的檢視時,它會先在應用程式中尋找路徑 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。只有寶石的管理員配置需要這些資產。主機應用程式包含 "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 文件相關的事項。