跳至內容 跳至搜尋

Rails::Engine 允許您將特定的 Rails 應用程式或功能子集包裝起來,並與其他應用程式共享,或在更大的打包應用程式中共享。每個 Rails::Application 只是一個引擎,允許簡單的功能和應用程式共享。

任何 Rails::Engine 也都是一個 Rails::Railtie,因此在 Railties 中可用的相同方法(例如 rake_tasksgenerators)和配置選項也可以在引擎中使用。

建立一個 Engine

如果您希望 gem 的行為像引擎一樣,您必須在插件的 lib 資料夾中的某處為其指定一個 Engine(類似於我們指定 Railtie 的方式)

# lib/my_engine.rb
module MyEngine
  class Engine < Rails::Engine
  end
end

然後確保在您的 config/application.rb 的頂部(或在您的 Gemfile 中)載入此檔案,它將自動載入 app 中的模型、控制器和 helper,在 config/routes.rb 載入路由,在 config/locales/*/ 載入語系檔,並在 lib/tasks/*/ 載入任務。

設定

與 Railties 一樣,引擎可以存取一個包含所有 Railties 和應用程式共享的配置的 config 物件。此外,每個引擎都可以存取限定於該引擎的 autoload_pathseager_load_pathsautoload_once_paths 設定。

class MyEngine < Rails::Engine
  # Add a load path for this specific Engine
  config.autoload_paths << File.expand_path("lib/some/path", __dir__)

  initializer "my_engine.add_middleware" do |app|
    app.middleware.use MyEngine::Middleware
  end
end

產生器

您可以使用 config.generators 方法設定引擎的產生器

class MyEngine < Rails::Engine
  config.generators do |g|
    g.orm             :active_record
    g.template_engine :erb
    g.test_framework  :test_unit
  end
end

您也可以使用 config.app_generators 為應用程式設定產生器

class MyEngine < Rails::Engine
  # note that you can also pass block to app_generators in the same way you
  # can pass it to generators method
  config.app_generators.orm :datamapper
end

路徑

應用程式和引擎具有彈性的路徑配置,這意味著您不需要將控制器放在 app/controllers 中,而是可以放在您認為方便的任何位置。

例如,假設您想將控制器放在 lib/controllers 中。您可以將其設定為選項

class MyEngine < Rails::Engine
  paths["app/controllers"] = "lib/controllers"
end

您也可以從 app/controllerslib/controllers 載入控制器

class MyEngine < Rails::Engine
  paths["app/controllers"] << "lib/controllers"
end

引擎中可用的路徑如下

class MyEngine < Rails::Engine
  paths["app"]                 # => ["app"]
  paths["app/controllers"]     # => ["app/controllers"]
  paths["app/helpers"]         # => ["app/helpers"]
  paths["app/models"]          # => ["app/models"]
  paths["app/views"]           # => ["app/views"]
  paths["lib"]                 # => ["lib"]
  paths["lib/tasks"]           # => ["lib/tasks"]
  paths["config"]              # => ["config"]
  paths["config/initializers"] # => ["config/initializers"]
  paths["config/locales"]      # => ["config/locales"]
  paths["config/routes.rb"]    # => ["config/routes.rb"]
end

Application 類別會在此集合中新增更多路徑。如同您的 Application 中一樣,app 下的所有資料夾都會自動新增到載入路徑中。例如,如果您有一個 app/services 資料夾,它將會被預設新增。

端點

引擎也可以是一個 Rack 應用程式。如果您有一個想要提供某些 Engine 功能的 Rack 應用程式,這會很有用。

要做到這一點,請使用 ::endpoint 方法

module MyEngine
  class Engine < Rails::Engine
    endpoint MyRackApplication
  end
end

現在您可以在應用程式的路由中掛載您的引擎

Rails.application.routes.draw do
  mount MyEngine::Engine => "/engine"
end

中介軟體堆疊

由於引擎現在可以是一個 Rack 端點,它也可以擁有一個中介軟體堆疊。用法與 Application 完全相同

module MyEngine
  class Engine < Rails::Engine
    middleware.use SomeMiddleware
  end
end

路由

如果您沒有指定端點,則路由將被用作預設端點。您可以像使用應用程式的路由一樣使用它們

# ENGINE/config/routes.rb
MyEngine::Engine.routes.draw do
  get "/" => "posts#index"
end

掛載優先順序

請注意,現在您的應用程式中可以有多個路由器,最好避免讓請求通過多個路由器。考慮這種情況

Rails.application.routes.draw do
  mount MyEngine::Engine => "/blog"
  get "/blog/omg" => "main#omg"
end

MyEngine 掛載在 /blog 上,而 /blog/omg 指向應用程式的控制器。在這種情況下,對 /blog/omg 的請求將會通過 MyEngine,如果 Engine 的路由中沒有這樣的路由,它將會被分派到 main#omg。最好交換一下

Rails.application.routes.draw do
  get "/blog/omg" => "main#omg"
  mount MyEngine::Engine => "/blog"
end

現在,Engine 只會收到 Application 沒有處理的請求。

Engine 名稱

有些地方會使用引擎的名稱

  • 路由:當您使用 mount(MyEngine::Engine => '/my_engine') 掛載 Engine 時,它會被用作預設的 :as 選項

  • 用於安裝遷移的 rake 任務 my_engine:install:migrations

Engine 名稱預設根據類別名稱設定。對於 MyEngine::Engine,它將是 my_engine_engine。您可以使用 engine_name 方法手動更改它

module MyEngine
  class Engine < Rails::Engine
    engine_name "my_engine"
  end
end

隔離的 Engine

通常,當您在引擎內建立控制器、helper 和模型時,它們會被視為在應用程式本身內建立。這表示應用程式中的所有 helper 和具名路由也可供您的引擎的控制器使用。

但是,有時您希望將引擎與應用程式隔離,尤其是當您的引擎有自己的路由器時。要做到這一點,您只需要呼叫 ::isolate_namespace。此方法要求您傳遞一個模組,您的所有控制器、helper 和模型都應該嵌套在其中

module MyEngine
  class Engine < Rails::Engine
    isolate_namespace MyEngine
  end
end

使用這樣的引擎,MyEngine 模組內的所有東西都將與應用程式隔離。

考慮這個控制器

module MyEngine
  class FooController < ActionController::Base
  end
end

如果 MyEngine 引擎被標記為隔離,則 FooController 只能存取 MyEngine 中的 helper,以及 MyEngine::Engine.routes 中的 url_helpers

隔離引擎中另一個改變是路由的行為。通常,當您為控制器命名空間時,您也需要為相關路由命名空間。使用隔離引擎,引擎的命名空間會自動套用,因此您不需要在路由中明確指定它

MyEngine::Engine.routes.draw do
  resources :articles
end

如果 MyEngine 是隔離的,則上面的路由將指向 MyEngine::ArticlesController。您也不需要使用較長的 URL helper,例如 my_engine_articles_path。相反,您應該像使用主應用程式一樣,只需使用 articles_path

為了使這種行為與框架的其他部分保持一致,隔離引擎也會影響 ActiveModel::Naming。在一般的 Rails 應用程式中,當您使用命名空間模型(例如 Namespace::Article)時,ActiveModel::Naming 將會產生帶有前綴“namespace”的名稱。在隔離引擎中,為了方便起見,URL helper 和表單欄位中將會省略前綴。

polymorphic_url(MyEngine::Article.new)
# => "articles_path" # not "my_engine_articles_path"

form_with(model: MyEngine::Article.new) do
  text_field :title # => <input type="text" name="article[title]" id="article_title" />
end

此外,隔離引擎將根據其命名空間設定自己的名稱,因此 MyEngine::Engine.engine_name 將會傳回“my_engine”。它還會將 MyEngine.table_name_prefix 設定為“my_engine_”,這表示例如 MyEngine::Article 將預設使用 my_engine_articles 資料表。

Engine 之外使用 Engine 的路由

由於您現在可以在應用程式的路由中掛載引擎,因此您無法在 Application 中直接存取 Engineurl_helpers。當您在應用程式的路由中掛載引擎時,會建立一個特殊的 helper 來允許您執行此操作。考慮這種情況

# config/routes.rb
Rails.application.routes.draw do
  mount MyEngine::Engine => "/my_engine", as: "my_engine"
  get "/foo" => "foo#index"
end

現在,您可以在您的應用程式中使用 my_engine helper

class FooController < ApplicationController
  def index
    my_engine.root_url # => /my_engine/
  end
end

還有一個 main_app helper,可讓您在 Engine 內存取應用程式的路由

module MyEngine
  class BarController
    def index
      main_app.foo_path # => /foo
    end
  end
end

請注意,提供給 mount 的 :as 選項會將 engine_name 作為預設值,因此大多數時候您可以直接省略它。

最後,如果您想使用 polymorphic_url 產生指向引擎路由的 URL,您還需要傳遞引擎 helper。假設您想要建立一個指向引擎路由之一的表單。您只需要將 helper 作為陣列中的第一個元素傳遞,並帶有 URL 的屬性

form_with(model: [my_engine, @user])

此程式碼將使用 my_engine.user_path(@user) 來產生正確的路由。

隔離引擎的 helper

有時您可能想要隔離引擎,但使用為它定義的 helper。如果您只想共享一些特定的 helper,您可以將它們新增到 ApplicationController 中應用程式的 helper 中

class ApplicationController < ActionController::Base
  helper MyEngine::SharedEngineHelper
end

如果您想包含所有引擎的 helper,您可以在引擎的實例上使用 helper 方法

class ApplicationController < ActionController::Base
  helper MyEngine::Engine.helpers
end

它將包含引擎目錄中的所有 helper。請注意,這不包括使用 helper_method 或其他類似解決方案在控制器中定義的 helper,只會包含在 helpers 目錄中定義的 helper。

遷移和種子資料

引擎可以有自己的遷移。遷移的預設路徑與應用程式中的路徑完全相同:db/migrate

要在應用程式中使用引擎的遷移,您可以使用下面的 rake 任務,它會將它們複製到應用程式的目錄中

$ rake ENGINE_NAME:install:migrations

請注意,如果應用程式中已存在同名遷移,則可能會跳過某些遷移。在這種情況下,您必須決定是保留該遷移,還是重新命名應用程式中的遷移並重新執行複製遷移。

如果您的引擎有遷移,您可能還想在 db/seeds.rb 檔案中準備資料庫的資料。您可以使用 load_seed 方法載入該資料,例如

MyEngine::Engine.load_seed

載入優先順序

為了更改引擎的優先順序,您可以在主應用程式中使用 config.railties_order。它將會影響載入視圖、helper、資源和所有其他與引擎或應用程式相關的檔案的優先順序。

# load Blog::Engine with highest priority, followed by application and other railties
config.railties_order = [Blog::Engine, :main_app, :all]
命名空間
方法
A
C
E
F
H
I
L
N
R
已包含的模組

屬性

[讀寫] called_from
[讀寫] isolated
[讀寫] isolated?

類別公開方法

endpoint(endpoint = nil)

# File railties/lib/rails/engine.rb, line 379
def endpoint(endpoint = nil)
  @endpoint ||= nil
  @endpoint = endpoint if endpoint
  @endpoint
end

find(path)

尋找具有給定路徑的引擎。

# File railties/lib/rails/engine.rb, line 424
def find(path)
  expanded_path = File.expand_path path
  Rails::Engine.subclasses.each do |klass|
    engine = klass.instance
    return engine if File.expand_path(engine.root) == expanded_path
  end
  nil
end

find_root(from)

# File railties/lib/rails/engine.rb, line 375
def find_root(from)
  find_root_with_flag "lib", from
end

inherited(base)

# File railties/lib/rails/engine.rb, line 361
def inherited(base)
  unless base.abstract_railtie?
    Rails::Railtie::Configuration.eager_load_namespaces << base

    base.called_from = begin
      call_stack = caller_locations.map { |l| l.absolute_path || l.path }

      File.dirname(call_stack.detect { |p| !p.match?(%r[railties[\w.-]*/lib/rails|rack[\w.-]*/lib/rack]) })
    end
  end

  super
end

isolate_namespace(mod)

# File railties/lib/rails/engine.rb, line 385
def isolate_namespace(mod)
  engine_name(generate_railtie_name(mod.name))

  config.default_scope = { module: ActiveSupport::Inflector.underscore(mod.name) }

  self.isolated = true

  unless mod.respond_to?(:railtie_namespace)
    name, railtie = engine_name, self

    mod.singleton_class.instance_eval do
      define_method(:railtie_namespace) { railtie }

      unless mod.respond_to?(:table_name_prefix)
        define_method(:table_name_prefix) { "#{name}_" }

        ActiveSupport.on_load(:active_record) do
          mod.singleton_class.redefine_method(:table_name_prefix) do
            "#{ActiveRecord::Base.table_name_prefix}#{name}_"
          end
        end
      end

      unless mod.respond_to?(:use_relative_model_naming?)
        class_eval "def use_relative_model_naming?; true; end", __FILE__, __LINE__
      end

      unless mod.respond_to?(:railtie_helpers_paths)
        define_method(:railtie_helpers_paths) { railtie.helpers_paths }
      end

      unless mod.respond_to?(:railtie_routes_url_helpers)
        define_method(:railtie_routes_url_helpers) { |include_path_helpers = true| railtie.routes.url_helpers(include_path_helpers) }
      end
    end
  end
end

new()

# File railties/lib/rails/engine.rb, line 440
def initialize
  @_all_autoload_paths = nil
  @_all_load_paths     = nil
  @app                 = nil
  @config              = nil
  @env_config          = nil
  @helpers             = nil
  @routes              = nil
  @app_build_lock      = Mutex.new
  super
end

公開實體方法

app()

回傳此引擎底層的 Rack 應用程式。

# File railties/lib/rails/engine.rb, line 516
def app
  @app || @app_build_lock.synchronize {
    @app ||= begin
      stack = default_middleware_stack
      config.middleware = build_middleware.merge_into(stack)
      config.middleware.build(endpoint)
    end
  }
end

call(env)

定義此引擎的 Rack API

# File railties/lib/rails/engine.rb, line 533
def call(env)
  req = build_request env
  app.call req.env
end

config()

定義引擎的配置物件。

# File railties/lib/rails/engine.rb, line 552
def config
  @config ||= Engine::Configuration.new(self.class.find_root(self.class.called_from))
end

eager_load!()

# File railties/lib/rails/engine.rb, line 490
def eager_load!
  # Already done by Zeitwerk::Loader.eager_load_all. By now, we leave the
  # method as a no-op for backwards compatibility.
end

endpoint()

回傳此引擎的端點。如果沒有註冊任何端點,則預設為 ActionDispatch::Routing::RouteSet

# File railties/lib/rails/engine.rb, line 528
def endpoint
  self.class.endpoint || routes
end

env_config()

定義在每次呼叫時新增的額外 Rack 環境配置。

# File railties/lib/rails/engine.rb, line 539
def env_config
  @env_config ||= {}
end

helpers()

回傳一個模組,其中包含為引擎定義的所有輔助方法。

# File railties/lib/rails/engine.rb, line 500
def helpers
  @helpers ||= begin
    helpers = Module.new
    AbstractController::Helpers.helper_modules_from_paths(helpers_paths).each do |mod|
      helpers.include(mod)
    end
    helpers
  end
end

helpers_paths()

回傳所有已註冊的輔助方法路徑。

# File railties/lib/rails/engine.rb, line 511
def helpers_paths
  paths["app/helpers"].existent
end

load_console(app = self)

載入控制台並叫用已註冊的鉤子。查看 `Rails::Railtie.console` 以了解更多資訊。

# File railties/lib/rails/engine.rb, line 454
def load_console(app = self)
  run_console_blocks(app)
  self
end

load_generators(app = self)

載入 Rails 產生器並叫用已註冊的鉤子。查看 `Rails::Railtie.generators` 以了解更多資訊。

# File railties/lib/rails/engine.rb, line 476
def load_generators(app = self)
  require "rails/generators"
  run_generators_blocks(app)
  Rails::Generators.configure!(app.config.generators)
  self
end

load_runner(app = self)

載入 Rails 執行器並叫用已註冊的鉤子。查看 `Rails::Railtie.runner` 以了解更多資訊。

# File railties/lib/rails/engine.rb, line 461
def load_runner(app = self)
  run_runner_blocks(app)
  self
end

load_seed()

從 db/seeds.rb 檔案載入資料。它可以用於載入引擎的種子資料,例如:

Blog::Engine.load_seed

# File railties/lib/rails/engine.rb, line 560
def load_seed
  seed_file = paths["db/seeds.rb"].existent.first
  run_callbacks(:load_seed) { load(seed_file) } if seed_file
end

load_server(app = self)

叫用已註冊的伺服器鉤子。查看 `Rails::Railtie.server` 以了解更多資訊。

# File railties/lib/rails/engine.rb, line 485
def load_server(app = self)
  run_server_blocks(app)
  self
end

load_tasks(app = self)

載入 Rake 和 railties 任務,並叫用已註冊的鉤子。查看 `Rails::Railtie.rake_tasks` 以了解更多資訊。

# File railties/lib/rails/engine.rb, line 468
def load_tasks(app = self)
  require "rake"
  run_tasks_blocks(app)
  self
end

railties()

# File railties/lib/rails/engine.rb, line 495
def railties
  @railties ||= Railties.new
end

routes(&block)

定義此引擎的路由。如果將程式碼區塊提供給 routes,它會被附加到引擎。

# File railties/lib/rails/engine.rb, line 545
def routes(&block)
  @routes ||= config.route_set_class.new_with_config(config)
  @routes.append(&block) if block_given?
  @routes
end

私有實體方法

load_config_initializer(initializer)

# File railties/lib/rails/engine.rb, line 691
def load_config_initializer(initializer) # :doc:
  ActiveSupport::Notifications.instrument("load_config_initializer.railties", initializer: initializer) do
    load(initializer)
  end
end