跳至内容 跳至搜索

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 内部的模型、控制器和帮助器,在 config/routes.rb 加载路由,在 config/locales/*/ 加载区域设置,并在 lib/tasks/*/ 加载任务。

配置

与 Railties 一样,引擎可以访问一个 config 对象,其中包含所有 Railties 和应用程序共享的配置。此外,每个引擎都可以访问作用域于该引擎的 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 应用程序。如果您有一个 Rack 应用程序,并且希望它具有 Engine 的某些功能,这可能很有用。

为此,请使用 ::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

通常,当您在引擎内部创建控制器、帮助器和模型时,它们被视为在应用程序本身内部创建。这意味着应用程序中的所有帮助器和命名路由也将对引擎的控制器可用。

但是,有时您希望将引擎与应用程序隔离,特别是当您的引擎有自己的路由器时。为此,您只需调用 ::isolate_namespace。此方法要求您传递一个模块,所有控制器、帮助器和模型都应该嵌套在该模块中

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

对于这样的引擎,MyEngine 模块中的所有内容都将与应用程序隔离。

考虑这个控制器

module MyEngine
  class FooController < ActionController::Base
  end
end

如果 MyEngine 引擎被标记为隔离,FooController 只能访问 MyEngine 的帮助器,以及 MyEngine::Engine.routesurl_helpers

隔离引擎中改变的下一件事是路由的行为。通常,当您为控制器命名空间时,您还需要为相关的路由命名空间。使用隔离引擎,引擎的命名空间会自动应用,因此您无需在路由中显式指定它

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

如果 MyEngine 是隔离的,则上述路由将指向 MyEngine::ArticlesController。您也无需使用更长的 URL 帮助器,例如 my_engine_articles_path。相反,您应该简单地使用 articles_path,就像您在主应用程序中所做的那样。

为了使这种行为与框架的其他部分保持一致,隔离引擎还会影响 ActiveModel::Naming。在普通的 Rails 应用程序中,当您使用命名空间模型(例如 Namespace::Article)时,ActiveModel::Naming 将生成带有前缀“namespace”的名称。在隔离引擎中,为了方便起见,URL 帮助器和表单字段中的前缀将被省略。

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 外部使用引擎的路由

由于您现在可以将引擎挂载到应用程序的路由中,因此您无法在 Application 内部直接访问 Engineurl_helpers。当您在应用程序的路由中挂载引擎时,会创建一个特殊的帮助器来允许您这样做。考虑以下场景

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

现在,您可以在应用程序内部使用 my_engine 帮助器

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

还有一个 main_app 帮助器,允许您在引擎内部访问应用程序的路由

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

请注意,mount 给定的 :as 选项默认使用 engine_name,因此大多数时候您可以简单地省略它。

最后,如果您想使用 polymorphic_url 生成到引擎路由的 URL,您还需要传递引擎帮助器。假设您想创建一个指向引擎路由之一的表单。您只需将帮助器作为数组中的第一个元素,以及 URL 的属性一起传递即可

form_with(model: [my_engine, @user])

此代码将使用 my_engine.user_path(@user) 生成正确的路由。

隔离引擎的帮助器

有时您可能希望隔离一个引擎,但使用为其定义的帮助器。如果您只想共享几个特定的帮助器,您可以将它们添加到 ApplicationController 中的应用程序帮助器中

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

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

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

它将包含引擎目录中的所有帮助器。请注意,这不包括使用 helper_method 或其他类似解决方案在控制器中定义的帮助器,只包括在 helpers 目录中定义的帮助器。

迁移和种子数据

引擎可以有自己的迁移。迁移的默认路径与应用程序中完全相同:db/migrate

要在应用程序中使用引擎的迁移,您可以使用下面的 rake 任务,它会将它们复制到应用程序的目录中

$ rake ENGINE_NAME:install:migrations

请注意,如果应用程序中已经存在同名迁移,某些迁移可能会被跳过。在这种情况下,您必须决定是保留该迁移,还是重命名应用程序中的迁移并重新运行复制迁移。

如果您的引擎有迁移,您可能还希望在 db/seeds.rb 文件中为数据库准备数据。您可以使用 load_seed 方法加载数据,例如

MyEngine::Engine.load_seed

加载优先级

为了更改引擎的优先级,您可以在主应用程序中使用 config.railties_order。它将影响视图、帮助器、资产以及与引擎或应用程序相关的所有其他文件的加载优先级。

# 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
包含的模块

Attributes

[RW] called_from
[RW] isolated
[RW] isolated?

类公共方法

endpoint(endpoint = nil)

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

find(path)

查找给定路径的引擎。

# File railties/lib/rails/engine.rb, line 423
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 374
def find_root(from)
  find_root_with_flag "lib", from
end

inherited(base)

# File railties/lib/rails/engine.rb, line 360
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 384
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 439
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 515
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 532
def call(env)
  req = build_request env
  app.call req.env
end

config()

定义引擎的配置对象。

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

eager_load!()

# File railties/lib/rails/engine.rb, line 489
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 527
def endpoint
  self.class.endpoint || routes
end

env_config()

定义每次调用时添加的额外 Rack env 配置。

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

helpers()

返回一个包含为引擎定义的所有帮助器的模块。

# File railties/lib/rails/engine.rb, line 499
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 510
def helpers_paths
  paths["app/helpers"].existent
end

load_console(app = self)

加载控制台并调用已注册的钩子。有关更多信息,请参阅 Rails::Railtie.console

# File railties/lib/rails/engine.rb, line 453
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 475
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 460
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 559
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 484
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 467
def load_tasks(app = self)
  require "rake"
  run_tasks_blocks(app)
  self
end

railties()

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

routes(&block)

定义此引擎的路由。如果给路由一个块,它会附加到引擎。

# File railties/lib/rails/engine.rb, line 544
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 690
def load_config_initializer(initializer) # :doc:
  ActiveSupport::Notifications.instrument("load_config_initializer.railties", initializer: initializer) do
    load(initializer)
  end
end