# 引擎入門
本章節中您將學習有關引擎的知識,以及引擎如何通過簡潔易用的方式為Rails應用插上飛翔的翅膀。
通過學習本章節,您將獲得如下知識:
* 引擎是什么
* 如何生成一個引擎
* 為引擎添加特性
* 為Rails應用添加引擎
* 給Rails中的引擎提供重載功能
### Chapters
1. [引擎是什么?](#%E5%BC%95%E6%93%8E%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F-)
2. [生成一個引擎](#%E7%94%9F%E6%88%90%E4%B8%80%E4%B8%AA%E5%BC%95%E6%93%8E)
* [引擎探秘](#%E5%BC%95%E6%93%8E%E6%8E%A2%E7%A7%98)
3. [引擎功能簡介](#%E5%BC%95%E6%93%8E%E5%8A%9F%E8%83%BD%E7%AE%80%E4%BB%8B)
* [生成一個Article 資源](#%E7%94%9F%E6%88%90%E4%B8%80%E4%B8%AAarticle-%E8%B5%84%E6%BA%90)
* [生成評論資源](#%E7%94%9F%E6%88%90%E8%AF%84%E8%AE%BA%E8%B5%84%E6%BA%90)
4. [和Rails應用整合](#%E5%92%8Crails%E5%BA%94%E7%94%A8%E6%95%B4%E5%90%88)
* [整合前的準備工作](#%E6%95%B4%E5%90%88%E5%89%8D%E7%9A%84%E5%87%86%E5%A4%87%E5%B7%A5%E4%BD%9C)
* [建立引擎](#%E5%BB%BA%E7%AB%8B%E5%BC%95%E6%93%8E)
* [訪問Rails應用中的類](#%E8%AE%BF%E9%97%AErails%E5%BA%94%E7%94%A8%E4%B8%AD%E7%9A%84%E7%B1%BB)
* [配置引擎](#%E5%92%8Crails%E5%BA%94%E7%94%A8%E6%95%B4%E5%90%88-%E9%85%8D%E7%BD%AE%E5%BC%95%E6%93%8E)
5. [引擎測試](#%E5%BC%95%E6%93%8E%E6%B5%8B%E8%AF%95)
* [功能測試](#%E5%8A%9F%E8%83%BD%E6%B5%8B%E8%AF%95)
6. [引擎優化](#%E5%BC%95%E6%93%8E%E4%BC%98%E5%8C%96-)
* [重載模型和控制器](#%E9%87%8D%E8%BD%BD%E6%A8%A1%E5%9E%8B%E5%92%8C%E6%8E%A7%E5%88%B6%E5%99%A8)
* [視圖重載](#%E8%A7%86%E5%9B%BE%E9%87%8D%E8%BD%BD)
* [路徑](#%E8%B7%AF%E5%BE%84)
* [渲染頁面相關的Assets文件](#%E6%B8%B2%E6%9F%93%E9%A1%B5%E9%9D%A2%E7%9B%B8%E5%85%B3%E7%9A%84assets%E6%96%87%E4%BB%B6)
* [頁面資源文件分組和預編譯](#%E9%A1%B5%E9%9D%A2%E8%B5%84%E6%BA%90%E6%96%87%E4%BB%B6%E5%88%86%E7%BB%84%E5%92%8C%E9%A2%84%E7%BC%96%E8%AF%91)
* [其他Gem依賴項](#%E5%85%B6%E4%BB%96gem%E4%BE%9D%E8%B5%96%E9%A1%B9)
### 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](https://github.com/plataformatec/devise) ,一個為其宿主應用提供權限認證功能的引擎;[Forem](https://github.com/radar/forem), 一個提供論壇功能的引擎;[Spree](https://github.com/spree/spree),一個提供電子商務平臺功能的引擎。[RefineryCMS](https://github.com/refinery/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](#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](getting_started.html)某人是否有新主題發布等功能。
#### 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`),和一個fixture`test/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](#test-directory)訪問。這樣也可以讓引擎的資源與Rails應用隔離開來。具體的詳情虧參考[Routes](#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](configuring.html#initializers)中,它和一個應用中的`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`](http://edgeapi.rubyonrails.org/classes/ActiveSupport/Concern.html)。`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](asset_pipeline.html)
#### 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
```
### 反饋
歡迎幫忙改善指南質量。
如發現任何錯誤,歡迎修正。開始貢獻前,可先行閱讀[貢獻指南:文檔](http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html#contributing-to-the-rails-documentation)。
翻譯如有錯誤,深感抱歉,歡迎 [Fork](https://github.com/ruby-china/guides/fork) 修正,或至此處[回報](https://github.com/ruby-china/guides/issues/new)。
文章可能有未完成或過時的內容。請先檢查 [Edge Guides](http://edgeguides.rubyonrails.org) 來確定問題在 master 是否已經修掉了。再上 master 補上缺少的文件。內容參考 [Ruby on Rails 指南準則](ruby_on_rails_guides_guidelines.html)來了解行文風格。
最后,任何關于 Ruby on Rails 文檔的討論,歡迎到 [rubyonrails-docs 郵件群組](http://groups.google.com/group/rubyonrails-docs)。
- Ruby on Rails 指南 (651bba1)
- 入門
- Rails 入門
- 模型
- Active Record 基礎
- Active Record 數據庫遷移
- Active Record 數據驗證
- Active Record 回調
- Active Record 關聯
- Active Record 查詢
- 視圖
- Action View 基礎
- Rails 布局和視圖渲染
- 表單幫助方法
- 控制器
- Action Controller 簡介
- Rails 路由全解
- 深入
- Active Support 核心擴展
- Rails 國際化 API
- Action Mailer 基礎
- Active Job 基礎
- Rails 程序測試指南
- Rails 安全指南
- 調試 Rails 程序
- 設置 Rails 程序
- Rails 命令行
- Rails 緩存簡介
- Asset Pipeline
- 在 Rails 中使用 JavaScript
- 引擎入門
- Rails 應用的初始化過程
- Autoloading and Reloading Constants
- 擴展 Rails
- Rails 插件入門
- Rails on Rack
- 個性化Rails生成器與模板
- Rails應用模版
- 貢獻 Ruby on Rails
- Contributing to Ruby on Rails
- API Documentation Guidelines
- Ruby on Rails Guides Guidelines
- Ruby on Rails 維護方針
- 發布記
- A Guide for Upgrading Ruby on Rails
- Ruby on Rails 4.2 發布記
- Ruby on Rails 4.1 發布記
- Ruby on Rails 4.0 Release Notes
- Ruby on Rails 3.2 Release Notes
- Ruby on Rails 3.1 Release Notes
- Ruby on Rails 3.0 Release Notes
- Ruby on Rails 2.3 Release Notes
- Ruby on Rails 2.2 Release Notes