class Rails::Engine
Rails::Engine позволяет обернуть конкретное приложение Rails или подмножество функциональности и использовать их совместно с другими приложениями или внутри более крупного упакованного приложения. Каждое Rails::Application — это просто движок, что позволяет легко совместно использовать функции и приложения.
Любой Rails::Engine также является Rails::Railtie, поэтому в движках можно использовать те же методы (например, rake_tasks и generators) и параметры конфигурации, что и в railties.
Создание Engine
Если вы хотите, чтобы гем работал как движок, необходимо где-либо внутри папки 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/*/.
Configuration
Как и railties, движки могут обращаться к объекту config, содержащему конфигурацию, общую для всех railties и приложения. Кроме того, каждый движок может обращаться к параметрам autoload_paths, eager_load_paths и autoload_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
Generators
Вы можете настроить генераторы для движков с помощью метода 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/controllers, так и из lib/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
Имя движка используется в нескольких местах:
-
маршруты: при монтировании
Engineс помощьюmount(MyEngine::Engine => '/my_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, а url_helpers — из MyEngine::Engine.routes.
Следующее изменение в изолированных движках касается поведения маршрутов. Обычно при добавлении пространства имён контроллерам необходимо также добавить пространство имён связанным маршрутам. В изолированном движке пространство имён движка применяется автоматически, поэтому указывать его явно в маршрутах не нужно:
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 вне движка
Теперь, когда движок можно смонтировать в маршрутах приложения, у вас нет прямого доступа к url_helpers Engine внутри Application. При монтировании движка в маршрутах приложения создаётся специальный помощник, позволяющий это сделать. Рассмотрим такой сценарий:
# 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
Обратите внимание: параметр :as, передаваемый в mount, по умолчанию принимает значение engine_name, поэтому чаще всего его можно не указывать.
Наконец, если вы хотите сгенерировать URL для маршрута движка с помощью polymorphic_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]
Атрибуты
Открытые методы класса
# File railties/lib/rails/engine.rb, line 378 def endpoint(endpoint = nil) @endpoint ||= nil @endpoint = endpoint if endpoint @endpoint end
# 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 Находит движок по указанному пути.
# File railties/lib/rails/engine.rb, line 374 def find_root(from) find_root_with_flag "lib", from end
# 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 # 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 # 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
Открытые методы экземпляра
# 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 Возвращает лежащее в основе приложение Rack для этого движка.
# File railties/lib/rails/engine.rb, line 532 def call(env) req = build_request env app.call req.env end
Определяет Rack API для этого движка.
# File railties/lib/rails/engine.rb, line 551 def config @config ||= Engine::Configuration.new(self.class.find_root(self.class.called_from)) end
Определяет объект конфигурации для движка.
# 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
# File railties/lib/rails/engine.rb, line 527 def endpoint self.class.endpoint || routes end
Возвращает конечную точку этого движка. Если конечная точка не зарегистрирована, по умолчанию используется ActionDispatch::Routing::RouteSet.
# File railties/lib/rails/engine.rb, line 538
def env_config
@env_config ||= {}
end Определяет дополнительную конфигурацию окружения Rack, добавляемую при каждом вызове.
# 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 Возвращает модуль со всеми хелперами, определёнными для движка.
# File railties/lib/rails/engine.rb, line 510 def helpers_paths paths["app/helpers"].existent end
Возвращает все зарегистрированные пути к хелперам.
# File railties/lib/rails/engine.rb, line 453 def load_console(app = self) run_console_blocks(app) self end
Загружает консоль и вызывает зарегистрированные хуки. Подробнее см. в Rails::Railtie.console.
# 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
Загружает генераторы Rails и вызывает зарегистрированные хуки. Подробнее см. в Rails::Railtie.generators.
# File railties/lib/rails/engine.rb, line 460 def load_runner(app = self) run_runner_blocks(app) self end
Загружает runner Rails и вызывает зарегистрированные хуки. Подробнее см. в Rails::Railtie.runner.
# 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 Загружает данные из файла db/seeds.rb. Этот метод можно использовать для загрузки исходных данных движков, например:
Blog::Engine.load_seed
# File railties/lib/rails/engine.rb, line 484 def load_server(app = self) run_server_blocks(app) self end
Вызывает зарегистрированные хуки сервера. Подробнее см. в Rails::Railtie.server.
# File railties/lib/rails/engine.rb, line 467 def load_tasks(app = self) require "rake" run_tasks_blocks(app) self end
Загружает задачи Rake и railties, а также вызывает зарегистрированные хуки. Подробнее см. в Rails::Railtie.rake_tasks.
# File railties/lib/rails/engine.rb, line 494 def railties @railties ||= Railties.new end
# 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
Определяет маршруты для этого движка. Если методу routes передан блок, он добавляется к движку.
Закрытые методы экземпляра
# 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
© 2004–2021 David Heinemeier Hansson
Licensed under the MIT License.