引擎入门

优质
小牛编辑
135浏览
2023-12-01

引擎入门

本章节中您将学习有关引擎的知识,以及引擎如何通过简洁易用的方式为Rails应用插上飞翔的翅膀。

通过学习本章节,您将获得如下知识:

  • 引擎是什么
  • 如何生成一个引擎
  • 为引擎添加特性
  • 为Rails应用添加引擎
  • 给Rails中的引擎提供重载功能

1 引擎是什么?

引擎可以被认为是一个可以为其宿主提供函数功能的中间件。一个Rails应用可以被看作一个"超级给力"的引擎,因为Rails::Application 类是继承自 Rails::Engine的。

从某种意义上说,引擎和Rails应用几乎可以说是双胞胎,差别很小。通过本章节的学习,你会发现引擎和Rails应用的结构几乎是一样的。

引擎和插件也是近亲,拥有相同的lib目录结构,并且都是使用rails plugin new命令生成。不同之处在于,一个引擎对于Rails来说是一个"发育完全的插件"(使用命令行生成引擎时会加--full选项)。在这里我们将使用几乎包含--full选项所有特性的--mountable 来代替。本章节中"发育完全的插件"和引擎是等价的。一个引擎可以是一个插件,但一个插件不能被看作是引擎。

我们将创建一个叫"blorgh"的引擎。这个引擎将为其宿主提供添加主题和主题评论等功能。刚出生的"blorgh"引擎也许会显得孤单,不过用不了多久,我们将看到她和自己的小伙伴一起愉快的聊天。

引擎也可以离开他的应用宿主独立存在。这意味着一个应用可以通过一个路径助手获得一个articles_path方法,使用引擎也可以生成一个名为articles_path的方法,而且两者不会冲突。同理,控制器,模型,数据库表名都是属于不同命名空间的。接下来我们来讨论该如何实现。

你心里须清楚Rails应用是老大,引擎是老大的小弟。一个Rails应用在他的地盘里面是老大,引擎的作用只是锦上添花。

可以看看下面的一些优秀引擎项目,比如Devise ,一个为其宿主应用提供权限认证功能的引擎;Forem, 一个提供论坛功能的引擎;Spree,一个提供电子商务平台功能的引擎。RefineryCMS, 一个 CMS 引擎 。

最后,大部分引擎开发工作离不开James Adam,Piotr Sarnacki 等Rails核心开发成员,以及很多默默无闻付出的人们。如果你见到他们,别忘了向他们致谢!

2 生成一个引擎

为了生成一个引擎,你必须将生成插件命令和适当的选项配合使用。比如你要生成"blorgh"应用 ,你需要一个"mountable"引擎。那么在命令行终端你就要敲下如下代码:

$ bin/rails plugin new blorgh --mountable

生成插件命令相关的帮助信息可以敲下面代码得到:

$ bin/rails plugin --help

--mountable 选项告诉生成器你想创建一个"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选项多做的事情有:

  • 生成若干资源文件(application.js and application.css)
  • 添加一个命名空间为ApplicationController 的子集
  • 添加一个命名空间为ApplicationHelper 的子集
  • 添加 一个引擎的布局视图模版
  • config/routes.rb中声明独立的命名空间 ;

    Blorgh::Engine.routes.draw do
    end

lib/blorgh/engine.rb中声明独立的命名空间:

```ruby
module Blorgh
  class Engine < ::Rails::Engine
    isolate_namespace Blorgh
  end
end
```

除此之外,--mountable选项告诉生成器在引擎内部的 test/dummy 文件夹中创建一个简单应用,在test/dummy/config/routes.rb中添加简单应用的路径。

mount Blorgh::Engine, at: "blorgh"

2.1 引擎探秘

2.1.1 文件冲突

在我们刚才创建的引擎根目录下有一个blorgh.gemspec文件。如果你想把引擎和Rails应用整合,那么接下来要做的是在目标Rails应用的Gemfile文件中添加如下代码:

gem 'blorgh', path: "vendor/engines/blorgh"

接下来别忘了运行bundle install命令,Bundler通过解析刚才在Gemfile文件中关于引擎的声明,会去解析引擎的blorgh.gemspec文件,以及lib文件夹中名为lib/blorgh.rb的文件,然后定义一个Blorgh模块:

require "blorgh/engine"

module Blorgh
end

提示: 某些引擎会使用一个全局配置文件来配置引擎,这的确是个好主意,所以如果你提供了一个全局配置文件来配置引擎的模块,那么这会更好的将你的模块的功能封装起来。

lib/blorgh/engine.rb文件中定义了引擎的基类。

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

因为引擎继承自Rails::Engine类,gem会通知Rails有一个引擎的特别路径,之后会正确的整合引擎到Rails应用中。会为Rails应用中的模型,控制器,视图和邮件等配置加载引擎的app目录路径。

isolate_namespace方法必须拿出来单独谈谈。这个方法会把引擎模块中与控制器,模型,路径等模块内的同名组件隔离。如果没它的话,可能会把引擎的内部方法暴露给其它模块,这样会破坏引擎的封装性,可能会引发不可预期的风险,比如引擎的内部方法被其他模块重载。举个例子,如果没有用命名空间对模块进行隔离,各模块的helpers方法会发生冲突,那么引擎内部的helper方法会被Rails应用的控制器所调用。

提示:强烈建议您使用isolate_namespace方法定义引擎的模块,如果没使用它,这可能会在一个Rails应用中和其它模块冲突。

命名空间对于执行像bin/rails g model的命令意味者什么呢? 比如bin/rails g model article,这个操作不会产生一个Article,而是Blorgh::Article。此外,模型的数据库表名也是命名空间化的,会用blorgh_articles 代替articles。与模型的命名空间类似,控制器中的 ArticlesController会被Blorgh::ArticlesController取代。而且和控制器相关的视图也会从app/views/articles变成app/views/blorgh/articles,邮件模块也是如此。

总而言之,路径同引擎一样也是有命名空间的,命名空间的重要性将会在本指南中的Routes继续讨论。

2.1.2 app 目录

app内部的结构和一般的Rails应用差不多,都包含 assets, controllers, helpers, mailers, models and views 等文件。helpers, mailers and models 文件夹是空的,我们就不详谈了。我们将会在将来的章节中讨论引擎的模型的时候,深入介绍。

app/assets文件夹包含images, javascriptsstylesheets,这些你在一个Rails应用中应该很熟悉了。不同在于,它们每个文件夹下包含一个和引擎同名的子目录,因为引擎是命名空间化的,那么assets也会遵循这一规定 。

app/controllers文件夹下有一个blorgh文件夹,他包含一个名为application_controller.rb的文件。这个文件为引擎提供控制器的一般功能。blorgh文件夹是专属于blorgh引擎的,通过命名空间化的目录结构,可以很好的将引擎的控制器与外部隔离起来,免受其它引擎或Rails应用的影响。

提示:在引擎内部的ApplicationController类命名方式和Rails 应用类似是为了方便你将Rails应用和引擎整合。

最后,app/views 文件夹包含一个layouts文件。他包含一个blorgh/application.html.erb文件。这个文件可以为你的引擎定制视图。如果这个引擎被当作独立的组件使用,那么你可以通过这个视图文件来定制引擎的视图,就和Rails应用中的app/views/layouts/application.html.erb一样、

如果你不希望强制引擎的使用者使用你的布局样式,那么可以删除这个文件,使用其他控制器的视图文件。

2.1.3 bin 目录

这个目录包含了一个bin/rails文件,它为你像在Rails应用中使用rails 等命令提供了支持,比如为该引擎生成模型和视图等操作:

$ bin/rails g model

必须要注意的是,在引擎内部使用命令行工具生成的组件都会自动调用 isolate_namespace方法,以达到组件命名空间化的目的。

2.1.4 test目录

test目录是引擎执行测试的地方,为了方便测试,test/dummy内置了一个精简版本的Rails 应用,这个应用可以和引擎整合,方便测试,他在test/dummy/config/routes.rb 中的声明如下:

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

mounts这行的意思是Rails应用只能通过/blorgh路径来访问引擎。

在测试目录下面有一个test/integration子目录,该子目录是为了实现引擎的的交互测试而存在的。其它的目录也可以如此创建。举个例子,你想为你的模型创建一个测试目录,那么他的文件结构和test/models是一样的。

3 引擎功能简介

本章中创建的引擎需要提供发布主题, 主题评论,关注Getting Started Guide某人是否有新主题发布等功能。

3.1 生成一个Article 资源

一个博客引擎首先要做的是生成一个Article 模型和相关的控制器。为了快速生成这些,你可以使用Rails的generator和 scaffold命令来实现:

$ 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
invoke    test_unit
create      test/controllers/blorgh/articles_controller_test.rb
invoke    helper
create      app/helpers/blorgh/articles_helper.rb
invoke      test_unit
create        test/helpers/blorgh/articles_helper_test.rb
invoke  assets
invoke    js
create      app/assets/javascripts/blorgh/articles.js
invoke    css
create      app/assets/stylesheets/blorgh/articles.css
invoke  css
create    app/assets/stylesheets/scaffold.css

scaffold生成器做的第一件事情是执行生成active_record操作,这将会为资源生成一个模型和迁移集,这里要注意的是,生成的迁移集的名字是 create_blorgh_articles而非Raisl应用中create_articles。这归功于Blorgh::Engine类中isolate_namespace方法。这里的模型也是命名空间化的,本来应该是app/models/article.rb,现在被 app/models/blorgh/article.rb取代。

接下来,模型的单元测试test_unit生成器会生成一个测试文件test/models/blorgh/article_test.rb(有别于test/models/article_test.rb),和一个fixturetest/fixtures/blorgh/articles.yml文件

接下来,该资源作为引擎的一部分会被插入config/routes.rb中。该引擎的资源resources :articlesconfig/routes.rb的声明如下:

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

这里需要注意的是该资源的路径已经和引擎Blorgh::Engine 关联上了,就像普通的YourApp::Application一样。这样访问引擎的资源路径就被限制在特定的范围。可以提供给test directory访问。这样也可以让引擎的资源与Rails应用隔离开来。具体的详情亏参考Routes

接下来,scaffold_controller生成器被触发了,生成一个名为Blorgh::ArticlesController的控制器(app/controllers/blorgh/articles_controller.rb),以及和控制器相关的视图app/views/blorgh/articles。这个生成器同时也会自动为控制器生成一个测试用例(test/controllers/blorgh/articles_controller_test.rb)和帮助方法(app/helpers/blorgh/articles_controller.rb)。

生成器创建的所有对象几乎都是命名空间化的,控制器的类被定义在Blorgh模块中:

module Blorgh
  class ArticlesController < ApplicationController
    ...
  end
end

提示:Blorgh::ApplicationController类继承了ApplicationController类,而非Rails应用的ApplicationController类。

app/helpers/blorgh/articles_helper.rb中的helper模块也是命名空间化的: ruby module Blorgh module ArticlesHelper ... end end 这样有助于避免和其它引擎或应用的同名资源发生冲突。

最后,生成该资源相关的样式表和js脚本文件,文件路径分别是app/assets/javascripts/blorgh/articles.jsapp/assets/stylesheets/blorgh/articles.css。稍后你将了解如何使用它们。

一般情况下,基本的样式表并不会应用到引擎中,因为引擎的布局文件app/views/layouts/blorgh/application.html.erb并没载入。如果要让基本的样式表文件对引擎生效。必须在<head>标签内插入如下代码:

<%= stylesheet_link_tag "scaffold" %>

现在,你已经了解了在引擎根目录下使用 scaffold 生成器进行数据库创建和迁移的整个过程,接下来,在test/dummy目录下运行rails server 后,用浏览器打开http://localhost:3000/blorgh/articles 后,随便浏览一下,刚才你生成的第一个引擎的功能。

如果你喜欢在控制台工作,那么rails console就像一个Rails应用。记住:Article是命名空间化的,所以你必须使用Blorgh::Article来访问它。

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

最后要做的一件事是让articles资源通过引擎的根目录就能访问。比如我打开http://localhost:3000/blorgh后,就能看到一个博客的主题列表。要实现这个目的,我们可以在引擎的config/routes.rb中做如下配置:

root to: "articles#index"

现在人们不需要到引擎的/articles目录下浏览主题了,这意味着http://localhost:3000/blorgh获得的内容和http://localhost:3000/blorgh/articles是相同的。

3.2 生成评论资源

现在,这个引擎可以创建一个新主题,那么自然需要能够评论的功能。为了实现这个功能,你需要生成一个评论模型,以及和评论相关的控制器,并修改主题的结构用以显示评论和添加评论。

在Rails应用的根目录下,运行模型生成器,生成一个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表进行操作:

$ rake db:migrate

为了在主题中显示评论,需要在app/views/blorgh/articles/show.html.erb的 "Edit" 按钮之前添加如下代码:

<h3>Comments</h3>
<%= render @article.comments %>

上述代码需要为评论在Blorgh::Article模型中添加一个"一对多"(has_many)的关联声明。为了添加上述声明,请打开app/models/blorgh/article.rb,并添加如下代码:

has_many :comments

修改过的模型关系是这样的:

module Blorgh
  class Article < ActiveRecord::Base
    has_many :comments
  end
end

提示: 因为 一对多(has_many) 的关联是在Blorgh 内部定义的,Rails明白你想为这些对象使用Blorgh::Comment模型。所以不需要特别使用类名来声明。

接下来,我们需要为主题提供一个表单提交评论,为了实现这个功能,请在 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_for [@article, @article.comments.build] do |f| %>
  <p>
    <%= f.label :text %><br>
    <%= f.text_area :text %>
  </p>
  <%= f.submit %>
<% end %>

当表单被提交后,它将通过路径/articles/:article_id/comments给引擎发送一个POST请求。现在这个路径还不存在,所以我们可以修改config/routes.rb中的resources :articles的相关路径来实现它:

resources :articles do
  resources :comments
end

给表单请求创建一个和评论相关的嵌套路径。

现在路径创建好了,相关的控制器却不存在,为了创建它们,我们使用命令行工具来创建它们:

$ bin/rails g 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
create      test/helpers/blorgh/comments_helper_test.rb
invoke  assets
invoke    js
create      app/assets/javascripts/blorgh/comments.js
invoke    css
create      app/assets/stylesheets/blorgh/comments.css

表单通过路径/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.require(:comment).permit(: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 %>获取的。这个变量是评论计数器,用来显示评论总数。

现在,我们完成一个带评论功能的博客引擎后,接下来我们将介绍如何将引擎与Rails应用整合。

4 和Rails应用整合

在Rails应用中可以很方便的使用引擎,本节将介绍如何将引擎和Rails应用整合。当然通常会把引擎和Rails应中的User类关联起来。

4.1 整合前的准备工作

首先,引擎需要在一个Rails应用中的Gemfile进行声明。如果我们无法知道Rails应用中是否有这些声明,那么我们可以在引擎目录之外创建一个新的Raisl应用:

$ rails new unicorn

一般而言,在Gemfile声明引擎和在Rails应用的一般Gem声明没有区别:

gem 'devise'

但是,假如你在自己的本地机器上开发blorgh引擎,那么你需要在Gemfile中特别声明:path项:

gem 'blorgh', path: "/path/to/blorgh"

运行bundle命令,安装gem 。

如前所述,在Gemfile中声明的gem将会与Rails框架一起加载。应用会从引擎中加载 lib/blorgh.rblib/blorgh/engine.rb等与引擎相关的主要文件。

为了在Rails应用内部调用引擎,我们必须在Rails应用的config/routes.rb中做如下声明:

mount Blorgh::Engine, at: "/blog"

上述代码的意思是引擎将被整合到Rails应用中的"/blog"下。当Rails应用通过 rails server启动时,可通过http://localhost:3000/blog访问。

提示: 对于其他引擎,比如 Devise ,它在处理路径的方式上稍有不同,可以通过自定义的助手方法比如devise_for来处理路径。这些路径助理方法工作千篇一律,为引擎大部分功能提供预定义路径的个性化支持。

4.2 建立引擎

和引擎相关的两个blorgh_articlesblorgh_comments表需要迁移到Rails应用数据库中,以保证引擎的模型能正确查询。迁移引擎的数据可以使用下面的命令:

$ rake blorgh:install:migrations

如果你有多个引擎需要数据迁移,可以使用railties:install:migrations命令来实现:

$ rake railties:install:migrations

第一次运行上述命令的时候,将会从引擎中复制所有的迁移集。当下次运行的时候,他只会迁移没被迁移过的数据。第一次运行该命令会显示如下信息:

Copied migration [timestamp_1]_create_blorgh_articles.rb from blorgh
Copied migration [timestamp_2]_create_blorgh_comments.rb from blorgh

第一个时间戳([timestamp_1])将会是当前时间,接着第二个时间戳([timestamp_2]) 将会是当前时间+1妙。这样做的原因是之前已经为引擎做过数据迁移操作。

在Rails应用中为引擎做数据迁移可以简单的使用rake db:migrate 执行操作。当通过http://localhost:3000/blog访问引擎的时候,你会发现主题列表是空的。这是因为在应用中创建的表与在引擎中创建的表是不同的。接下来你将发现应用中的引擎和独立环境中的引擎有很多不同之处。

如果你只想对某一个引擎执行数据迁移操作,那么可以通过SCOPE声明来实现:

rake db:migrate SCOPE=blorgh

这将有利于你的引擎执行数据迁移的回滚操作。 如果想让引擎的数据回到原始状态,那么可以执行下面的操作:

rake db:migrate SCOPE=blorgh VERSION=0

4.3 访问Rails应用中的类

4.3.1 访问Rails应用中的模型

当一个引擎创建之后,那么就需要Rails应用提供一个专属的类,将引擎和Rails应用关联起来。在本例中,blorgh引擎需要Rails应用提供作者来发表主题和评论。

一个典型的Rails应用会有一个User类来实现发布主题和评论的功能。也许某些应用里面会用Person类来做这些事情。因此,引擎不应该硬编码到一个User类中。

为了简单起见,我们的应用将会使用User类来实现和引擎的关联。那么我们可以在应用中使用命令:

rails g model user name:string

在这里执行rake db:migrate命令是为了我们的应用中有users表,以备将来使用。

为了简单起见,主题表单也会添加一个新的字段author_name,这样方便用户填写他们的名字。 当用户提交了他们的名字后,引擎将会判断是否存在该用户,如果不存在,就将该用户添加到数据库里面,并通过User对象把该用户和主题关联起来。

首先需要在引擎内部的app/views/blorgh/articles/_form.html.erb文件中添加 author_name项。这些内容可以添加到title之前,代码如下:

<div class="field">
  <%= f.label :author_name %><br>
  <%= f.text_field :author_name %>
</div>

接下来我们需要更新Blorgh::ArticleController#article_params方法接受参数的格式:

def article_params
  params.require(:article).permit(:title, :text, :author_name)
end

模型Blorgh::Article需要添加一些代码把author_nameUser对象关联起来。以确保保存主题时,主题相关的 author也被同时保存了。同时我们需要为这个字段定义一个attr_accessor。以方便我们读取或设置它的属性。

上述工作完成后,你需要为author_name添加一个属性读写器(attr_accessor),调用在app/models/blorgh/article.rbbefore_save方法以便关联。author 将会通过硬编码的方式和User关联:

attr_accessor :author_name
belongs_to :author, class_name: "User"

before_save :set_author

private
  def set_author
    self.author = User.find_or_create_by(name: author_name)
  end

author关联的User类,成了引擎和Rails应用之间联系的纽带。与此同时,还需要把blorgh_articlesusers 表进行关联。因为通过author关联,那么需要给blorgh_articles表添加一个author_id字段来实现关联。

为了生成这个新字段,我们需要在引擎中执行如下操作:

$ bin/rails g migration add_author_id_to_blorgh_articles author_id:integer

提示:假如数据迁移命令后面跟了一个字段声明。那么Rails会认为你想添加一个新字段到声明的表中,而无需做其他操作。

这个数据迁移操作必须在Rails应用中执行,为此,你必须保证是第一次在命令行中执行下面的操作:

$ rake blorgh:install:migrations

需要注意的是,这里只会发生一次数据迁移,这是因为前两个数据迁移拷贝已经执行过迁移操作了。

NOTE Migration [timestamp]_create_blorgh_articles.rb from blorgh has been
skipped. Migration with the same name already exists. NOTE Migration
[timestamp]_create_blorgh_comments.rb from blorgh has been skipped. Migration
with the same name already exists. Copied migration
[timestamp]_add_author_id_to_blorgh_articles.rb from blorgh

运行数据迁移命令:

$ rake db:migrate

现在所有准备工作都就绪了。上述操作实现了Rails应用中的User表和作者关联,引擎中的blorgh_articles表和主题关联。

最后,主题的作者将会显示在主题页面。在app/views/blorgh/articles/show.html.erb文件中的Title之前添加如下代码:

<p>
  <b>Author:</b>
  <%= @article.author %>
</p>

使用<%= 标签和to_s方法将会输出@article.author。默认情况下,这看上去很丑:

#<User:0x00000100ccb3b0>

这不是我们希望看到的,所以最好显示用户的名字。为此,我去需要给Rails应用中的User类添加to_s方法:

def to_s
  name
end

现在,我们将看到主题的作者名字 。

4.3.2 与控制器交互

Rails应用的控制器一般都会和权限控制,会话变量访问模块共享代码,因为它们都是默认继承自 ApplicationController类。Rails的引擎因为是命名空间化的,和主应用独立的模块。所以每个引擎都会有自己的ApplicationController类。这样做有利于避免代码冲突,但很多时候,引擎控制器需要调用主应用的ApplicationController。这里有一个简单的方法是让引擎的控制器继承主应用的ApplicationController。我们的Blorgh引擎会在app/controllers/blorgh/application_controller.rb中实现上述操作:

class Blorgh::ApplicationController < ApplicationController
end

一般情况下,引擎的控制器是继承自Blorgh::ApplicationController,所以,做了上述改变后,引擎可以访问主应用的ApplicationController了,也就是说,它变成了主应用的一部分。

上述操作的一个必要条件是:和引擎相关的Rails应用必须包含一个ApplicationController类。

4.4 配置引擎

本章节将介绍如何让User类可配置化。下面我们将介绍配置引擎的细节。

4.4.1 配置应用的配置文件

接下来的内容我们将讲述如何让应用中诸如User的类对象为引擎提供定制化的服务。如前所述,引擎要访问应用中的类不一定每次都叫User,所以我来实现可定制化的访问,必须在引擎里面设置一个名为author_class和应用中的User类进行交互。

为了定义这个设置,你将在引擎的Blorgh 模块中声明一个mattr_accessor方法和author_class关联。在引擎中的lib/blorgh.rb代码如下:

mattr_accessor :author_class

这个方法的功能和它的兄弟attr_accessorcattr_accessor功能类似,但是特别提供了一个方法,可以根据指定名字来对类或模块访问。我们使用它的时候,必须加上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.rbBlorgh 模块的author_class的get方法,确保在获取返回值之前调用constantize方法: ruby 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

为了让这些配置在应用中生效,必须使用一个初始化器。使用初始化器可以保证这种配置在Rails应用调用引擎模块之前就生效,因为应用和引擎交互时也许需要用到某些配置。

在应用中的config/initializers/blorgh.rb添加一个新的初始化器,并添加如下代码:

Blorgh.author_class = "User"

警告:使用String版本的类对象要比使用类对象本身更好。如果你使用类对象,Rails会尝试加载和类相关的数据库表。如果这个表不存在,就会抛出异常。所以,稍后在引擎中最好使用String类型,并且把类用constantize方法转换一下。

接下来我们创建一个新主题,除了让引擎读取config/initializers/blorgh.rb中的类信息之外,你将发现它和之前没什么区别,

这里对类没有严格的定义,只是提供了一个类必须做什么的指导。引擎也只是调用find_or_create_by方法来获取符合条件的类对象。当然这个对象也可以被其他对象引用。

4.4.2 配置引擎

在引擎内部,有很多配置引擎的方法,比如initializers, internationalization和其他配置项。一个Rails引擎和一个Rails应用具有很多相同的功能。实际上一个Rails应用就是一个超级引擎。

如果你想使用一个初始化器,必须在引擎载入之前使用,配置文件在config/initializers 目录下。这个目录的详细使用说明在Initializers section中,它和一个应用中的config/initializers文件相对目录是一致的。可以把它当作一个Rails应用中的初始化器来配置。

关于本地文件,和一个应用中的目录类似,都在config/locales目录下。

5 引擎测试

生成一个引擎后,引擎内部的test/dummy目录下会生成一个简单的Rails应用。这个应用被用来给引擎提供集成测试环境。你可以扩展这个应用的功能来测试你的引擎。

test目录将会被当作一个典型的Rails测试环境,允许单元测试,功能测试和交互测试。

5.1 功能测试

在编写引擎的功能测试时,我们会假定这个引擎会在一个应用中使用。test/dummy目录中的应用和你引擎结构差不多。这是因为建立测试环境后,引擎需要一个宿主来测试它的功能,特别是控制器。这意味着你需要在一个控制器功能测试函数中下如下代码:

get :index

这似乎不能称为函数,因为这个应用不知道如何给引擎发送的请求做响应,除非你明确告诉他怎么做。为此,你必须在请求的参数中加上:use_route选项来声明:

get :index, use_route: :blorgh

上述代码会告诉Rails应用你想让它的控制器响应一个GET请求,并执行index动作,但是你最好使用引擎的路径来代替。

另外一种方法是在你的测试总建立一个setup方法,把Engine.routes赋值给变量@routes

setup do
  @routes = Engine.routes
end

上诉操作也同时保证了引擎的url助手方法在你的测试中正常使用。

6 引擎优化

本章节将介绍在Rails应用中如何添加或重载引擎的MVC功能。

6.1 重载模型和控制器

应用中的公共类可以扩展引擎的模型和控制器的功能。(因为模型和控制器类都继承了Rails应用的特定功能)应用中的公共类和引擎只是对模型和控制器根据需要进行了扩展。这种模式通常被称为装饰模式。

举个例子,ActiveSupport::Concern类使用Class#class_eval方法扩展了他的功能。

6.1.1 装饰器的特点以及加载代码

因为装饰器不是引用Rails应用本身,Rails自动载入系统不会识别和载入你的装饰器。这意味着你需要用代码声明他们。

这是一个简单的例子:

# lib/blorgh/engine.rb
module Blorgh
  class Engine < ::Rails::Engine
    isolate_namespace Blorgh

    config.to_prepare do
      Dir.glob(Rails.root + "app/decorators/**/*_decorator*.rb").each do |c|
        require_dependency(c)
      end
    end
  end
end

上述操作不会应用到当前的装饰器,但是在引擎中添加的内容不会影响你的应用。

6.1.2 使用 Class#class_eval 方法实现装饰模式

添加 Article#time_since_created方法:

# MyApp/app/decorators/models/blorgh/article_decorator.rb

Blorgh::Article.class_eval do
  def time_since_created
    Time.current - created_at
  end
end
# Blorgh/app/models/article.rb

class Article < ActiveRecord::Base
  has_many :comments
end

重载 Article#summary方法:

# MyApp/app/decorators/models/blorgh/article_decorator.rb

Blorgh::Article.class_eval do
  def summary
    "#{title} - #{truncate(text)}"
  end
end
# Blorgh/app/models/article.rb

class Article < ActiveRecord::Base
  has_many :comments
  def summary
    "#{title}"
  end
end
6.1.3 使用ActiveSupport::Concern类实现装饰模式

使用Class#class_eval方法可以应付一些简单的修改。但是如果要实现更复杂的操作,你可以考虑使用ActiveSupport::ConcernActiveSupport::Concern管理着所有独立模块的内部链接指令,并且允许你在运行时声明模块代码。

添加 Article#time_since_created方法和重载 Article#summary方法:

# MyApp/app/models/blorgh/article.rb

class Blorgh::Article < ActiveRecord::Base
  include Blorgh::Concerns::Models::Article

  def time_since_created
    Time.current - created_at
  end

  def summary
    "#{title} - #{truncate(text)}"
  end
end
# Blorgh/app/models/article.rb

class Article < ActiveRecord::Base
  include Blorgh::Concerns::Models::Article
end
# Blorgh/lib/concerns/models/article

module Blorgh::Concerns::Models::Article
  extend ActiveSupport::Concern

  # 'included do' causes the included code to be evaluated in the
  # context where it is included (article.rb), rather than being
  # executed in the module's context (blorgh/concerns/models/article).
  included do
    attr_accessor :author_name
    belongs_to :author, class_name: "User"

    before_save :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 视图重载

Rails在寻找一个需要渲染的视图时,首先会去寻找应用的app/views目录下的文件。如果找不到,那么就会去当前应用目录下的所有引擎中找app/views目录下的内容。

当一个应用被要求为Blorgh::ArticlesControllerindex动作渲染视图时,它首先会在应用目录下去找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.3 路径

引擎中的路径默认是和Rails应用隔离开的。主要通过Engine类的isolate_namespace方法 实现的。这意味着引擎和Rails应可以拥有同名的路径,但却不会冲突。

引擎内部的config/routes.rb中的Engine类是这样绑定路径的:

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

因为拥有相对独立的路径,如果你希望在应用内部链接到引擎的某个地方,你需要使用引擎的路径代理方法。如果调用普通的路径方法,比如articles_path等,将不会得到你希望的结果。

举个例子。下面的articles_path方法根据情况自动识别,并渲染来自应用或引擎的内容。

<%= link_to "Blog articles", articles_path %>

为了确保这个路径使用引擎的articles_path方法,我们必须使用路径代理方法来实现:

<%= link_to "Blog articles", blorgh.articles_path %>

如果你希望在引擎内部访问Rails应用的路径,可以使用main_app方法:

<%= link_to "Home", main_app.root_path %>

如果你在引擎中使用了上诉方法,那么这将一直指向Rails应用的根目录。如果你没有使用main_approuting proxy路径代理调用方法,那么会根据调用源来指向引擎或Rails应用的根目录。

如果你引擎内的模板渲染想调用一个应用的路径帮助方法,这可能导致一个未定义的方法调用异常。如果你想解决这个问题,必须确保在引擎内部调用Rails应用的路径帮助方法时加上main_app前缀。

6.4 渲染页面相关的Assets文件

引擎内部的Assets文件位置和Rails应用的的相似。因为引擎类是继承自Rails::Engine的。应用会自动去引擎的aapp/assetslib/assets目录搜索和页面渲染相关的文件。

像其他引擎组件一样,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 blorgh/style
*/

提示: 如果你使用的是Sass或CoffeeScript语言,那么需要在你的引擎的.gemspec文件中设定相对路径。

6.5 页面资源文件分组和预编译

在某些情况下,你的引擎内部用到的资源文件,在Rails应用宿主中是不会用到的。举个例子,你为引擎创建了一个管理页面,它只在引擎内部使用,在这种情况下,Rails应用宿主并不需要用到admin.cssadmin.js文件,只是gem内部的管理页面需要用到它们。那么应用宿主就没必要添加"blorgh/admin.css"到他的样式表文件中 ,这种情况下,你可以预编译这些文件。这会在你的引擎内部添加一个rake assets:precompile任务。

你可以在引擎的engine.rb中定义需要预编译的资源文件:

initializer "blorgh.assets.precompile" do |app|
  app.config.assets.precompile += %w(admin.css admin.js)
end

想要了解更多详情,可以参考 Asset Pipeline guide

6.6 其他Gem依赖项

一个引擎的相关依赖项会在引擎的根目录下的.gemspec中声明。因为引擎也许会被当作一个gem安装到Rails应用中。如果在Gemfile中声明依赖项,那么这些依赖项就会被认为不是一个普通Gem,所以他们不会被安装,这会导致引擎发生故障。

为了让引擎被当作一个普通的Gem安装,需要声明他的依赖项已经安装过了。那么可以在引擎根目录下的.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