更多内容请访问 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,引擎的助手将包含在应用程序的控制器中。

强烈建议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/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 子命令和生成器。这意味着您可以通过运行以下命令非常轻松地为该引擎生成新的控制器和模型

当然,请记住,在 Engine 类中包含 isolate_namespace 的引擎中,使用这些命令生成的任何内容都将具有命名空间。

$ bin/rails generate model

2.1.4. test 目录

test 目录是引擎的测试文件存放的地方。为了测试引擎,其中嵌入了一个简化版的 Rails 应用程序,位于 test/dummy。此应用程序将在 test/dummy/config/routes.rb 文件中挂载引擎

此行将引擎挂载到路径 /blorgh,使其只能通过该路径在应用程序中访问。

Rails.application.routes.draw do
  mount Blorgh::Engine => "/blorgh"
end

在测试目录中有一个 test/integration 目录,引擎的集成测试应该放在这里。也可以在 test 目录中创建其他目录。例如,您可能希望为模型测试创建一个 test/models 目录。

3. 提供引擎功能

本指南涵盖的引擎提供文章提交和评论功能,并遵循 入门指南 的类似思路,并有一些新的变化。

对于本节,请确保在 blorgh 引擎目录的根目录中运行命令。

3.1. 生成文章资源

为博客引擎生成的第一件事是 Article 模型和相关的控制器。要快速生成它,您可以使用 Rails 脚手架生成器。

此命令将输出以下信息

$ bin/rails generate scaffold article title:string text:text

脚手架生成器做的第一件事是调用 active_record 生成器,它为资源生成一个迁移和一个模型。然而,请注意,这里的迁移名为 create_blorgh_articles,而不是通常的 create_articles。这是由于在 Blorgh::Engine 类的定义中调用了 isolate_namespace 方法。这里的模型也具有命名空间,由于 Engine 类中的 isolate_namespace 调用,它位于 app/models/blorgh/article.rb 而不是 app/models/article.rb

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

接下来,为此模型调用 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 对象而不是 YourApp::Application 类绘制的。这样做的目的是将引擎路由限制在引擎本身,并且可以像 测试目录 部分所示那样在特定点挂载。它还会导致引擎的路由与应用程序中的路由隔离。本指南的 路由 部分详细介绍了它。

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

接下来,调用 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 模块中

ArticlesController 类继承自 Blorgh::ApplicationController,而不是应用程序的 ApplicationController

module Blorgh
  class ArticlesController < ApplicationController
    # ...
  end
end

app/helpers/blorgh/articles_helper.rb 中的助手也具有命名空间

这有助于防止与可能也有文章资源的其他引擎或应用程序发生冲突。

module Blorgh
  module ArticlesHelper
    # ...
  end
end

您可以通过在引擎的根目录运行 bin/rails db:migrate 来运行脚手架生成器生成的迁移,然后运行 bin/rails servertest/dummy 中,来查看引擎目前的功能。当您打开 https://:3000/blorgh/articles 时,您将看到已生成的默认脚手架。点击查看!您刚刚生成了第一个引擎的第一个功能。

如果您更喜欢在控制台中使用,bin/rails console 也像 Rails 应用程序一样工作。请记住:Article 模型具有命名空间,因此要引用它,您必须将其称为 Blorgh::Article

最后一件事是,此引擎的 articles 资源应该是引擎的根。每当有人进入引擎挂载的根路径时,都应该向他们显示文章列表。如果将此行插入到引擎内部的 config/routes.rb 文件中,则可以实现这一点

irb> Blorgh::Article.find(1)
=> #<Blorgh::Article id: 1 ...>

现在人们只需要访问引擎的根目录即可查看所有文章,而无需访问 /articles。这意味着现在您只需要访问 https://:3000/blorgh,而不是 https://:3000/blorgh/articles

root to: "articles#index"

现在人们只需要访问引擎的根目录即可查看所有文章,而无需访问 /articles。这意味着现在您只需要访问 https://:3000/blorgh,而不是 https://:3000/blorgh/articles

3.2. 生成评论资源

现在引擎可以创建新文章了,再添加评论功能也很有意义。为此,您需要生成一个评论模型,一个评论控制器,然后修改文章脚手架以显示评论并允许人们创建新评论。

从引擎根目录,运行模型生成器。告诉它生成一个 Comment 模型,相关表有两个列:一个 article 引用列和一个 text 文本列。

$ bin/rails generate model Comment article:references 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://: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://: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,以便定义其设置器和获取器方法。

要完成所有这些操作,您需要在 app/models/blorgh/article.rb 中添加 author_nameattr_accessor、作者的关联以及 before_validation 调用。作者关联将暂时硬编码为 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 模型切换到此新设置。将此模型中的 belongs_to 关联 (app/models/blorgh/article.rb) 更改为

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/[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.cssadmin.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

有关更多信息,请阅读 资产管道指南

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


回到顶部