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
andapplication.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
, javascripts
和stylesheets
,这些你在一个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 :articles
在config/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.js
和
app/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.rb
和lib/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_articles
和 blorgh_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_name
和User
对象关联起来。以确保保存主题时,主题相关的 author
也被同时保存了。同时我们需要为这个字段定义一个attr_accessor
。以方便我们读取或设置它的属性。
上述工作完成后,你需要为author_name
添加一个属性读写器(attr_accessor
),调用在app/models/blorgh/article.rb
的before_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_articles
和 users
表进行关联。因为通过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_accessor
和cattr_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.rb
中Blorgh
模块的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::Concern
。ActiveSupport::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::ArticlesController
的index
动作渲染视图时,它首先会在应用目录下去找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_app
的
routing proxy
路径代理调用方法,那么会根据调用源来指向引擎或Rails应用的根目录。
如果你引擎内的模板渲染想调用一个应用的路径帮助方法,这可能导致一个未定义的方法调用异常。如果你想解决这个问题,必须确保在引擎内部调用Rails应用的路径帮助方法时加上main_app
前缀。
6.4 渲染页面相关的Assets文件
引擎内部的Assets文件位置和Rails应用的的相似。因为引擎类是继承自Rails::Engine
的。应用会自动去引擎的aapp/assets
和lib/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.css
和admin.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
反馈
欢迎帮忙改善指南质量。
如发现任何错误,欢迎修正。开始贡献前,可先行阅读贡献指南:文档。
翻译如有错误,深感抱歉,欢迎 Fork 修正,或至此处回报。
文章可能有未完成或过时的内容。请先检查 Edge Guides 来确定问题在 master 是否已经修掉了。再上 master 补上缺少的文件。内容参考 Ruby on Rails 指南准则来了解行文风格。
最后,任何关于 Ruby on Rails 文档的讨论,欢迎到 rubyonrails-docs 邮件群组。