更多内容请访问 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 方法值得特别注意。此调用负责将控制器、模型、路由和其他内容隔离到它们自己的命名空间中,远离应用程序内部的类似组件。没有它,引擎的组件可能会“泄漏”到应用程序中,导致意外的干扰,或者应用程序内部同名内容可能会覆盖重要的引擎组件。助手就是此类冲突的一个示例。如果没有调用 isolate_namespace,引擎的助手将包含在应用程序的控制器中。

强烈建议在 Engine 类定义中保留 isolate_namespace 行。如果没有它,在引擎中生成的类**可能**与应用程序发生冲突。

命名空间隔离意味着,通过调用 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 脚手架生成器。

$ 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

脚手架生成器首先调用 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)和一个辅助程序(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.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 对象关联起来。

首先,需要将 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.expect(article: [:title, :text, :author_name])
end

然后,Blorgh::Article 模型应该包含一些代码,用于将 author_name 字段转换为实际的 User 对象,并将该对象与该文章的 author 关联起来,然后再保存文章。它还需要为该字段设置 attr_accessor,以便为它定义 setter 和 getter 方法。

要执行所有这些操作,您需要将 author_nameattr_accessor、作者的关联和 before_validation 调用添加到 app/models/blorgh/article.rb 中。目前,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 相似,但它在模块上提供了具有指定名称的 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

覆盖 *重新打开* 类或模块非常重要。使用 `class` 或 `module` 关键字将在内存中不存在时定义它们,这是不正确的,因为定义存在于引擎中。如上所示使用 `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/assets` 和 `lib/assets` 目录中查找资产。

与引擎的所有其他组件一样,资产应该被命名空间。这意味着,如果您有一个名为 `style.css` 的资产,它应该放置在 `app/assets/stylesheets/[engine name]/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.css` 或 `admin.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

有关更多信息,请阅读 Asset Pipeline 指南

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 "other_engine/engine"
require "yet_another_engine/engine"

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


返回顶部