Rails Engine
Engine实际上是一个Rails Railtie。Railtie允许你在Rails的启动过程中添加自定义的文件等。
这样的话,就可以把一些具有完整功能的Rails应用,作为插件嵌入到你自己的应用中。
应用
Engine 可以想成是抽掉了某些功能的 Rails 应用程序: 微型的 Rails 应用程序 。可以安装到(mount)宿主(你想要扩展的那个应用程序),为宿主添加新功能。Rails 本身也是个 Engine,Rails 应用程序 Rails::Application
继承自 Rails::Engine
,其实 Rails 不过就是个“强大的” Engine。
Rails 还有插件功能,插件跟 Engine 很像。两者都有 lib
目录结构,皆采用 rails plugin new
来产生 Engine 与插件。Engine 可以是插件;插件也可是 Engine。但还是不太一样,Engine 可以想成是“完整的插件”。
下面会用一个 blorgh
Engine 的例子来讲解。这个 blorgh
给宿主提供了:新增文章(posts)、新增评论(comments),这两个功能。我们会先开发 Engine,再把 Engine 安装到应用程序。
假设路由里有 posts_path
这个 routing helper,宿主会提供这个功能、Engine 也会提供,这两者并不冲突。也就是说 Engine 可从宿主抽离出来。稍后会解释这是如何实现的。
记住!宿主的优先权最高,Engine 不过给宿主提供新功能。
以下皆是以 Rails Engine 实现的 RubyGems:
Devise 提供使用者验证功能。
Forem 提供论坛功能。
Spree 提供电子商务平台。
RefineryCMS 内容管理系统。
Rails Admin 内容管理系统。
Active Admin 内容管理系统。
历经了多少无数的编程夜晚,本来与 Rails 错综复杂各种核心功能,在 Rails 3.1 起,全都被抽离出来,变成 Rails::Engine
了,甚至 Rails 本身也是个 Engine:
require 'rails'
Rails::Application.superclass
=> Rails::Engine
Rails 3.2 Engine 正式当家。
产生 Engine
用 plugin 产生器来产生 Engine(加上 --mountable
选项):
$ rails plugin new blorgh --mountable
看看产生出来的 Engine 的目录结构:
.
├── app
├── bin
├── blorgh.gemspec
├── config
├── lib
├── test
├── Gemfile
├── Gemfile.lock
├── MIT-LICENSE
├── README.rdoc
└── Rakefile
--help
可查看完整说明:
$ rails plugin --help
让我们看看 --full
选项跟 --mountable
的差异,--mountable
多加了下列文件:
- Asset Manifest 文件(
application.css
、application.js
)。 - Controller
application_controller.rb
。 - Helper
application_helper.rb
。 - layout 的 view 模版:
application.html.erb
。 命名空间与 Rails 应用程序分离的
config/routes.rb
:--full
:Rails.application.routes.draw do end
--mountable
:Blorgh::Engine.routes.draw do end
lib/blorgh/engine.rb
:--full
:module Blorgh class Engine < ::Rails::Engine end end
--mountable
:module Blorgh class Engine < ::Rails::Engine isolate_namespace Blorgh end end
除了上述差异外,--mountable
还会把产生出来的 Engine 安装至 test/dummy
下的 Rails 应用程序,test/dummy/config/routes.rb
:
mount Blorgh::Engine, at: "blorgh"
Engine 里面有什么
Engine 目录结构:
.
├── app
├── bin
├── blorgh.gemspec
├── config
├── db
├── lib
├── test
├── Gemfile
├── Gemfile.lock
├── MIT-LICENSE
├── README.rdoc
└── Rakefile
重要的文件
blorgh.gemspec
当 Engine 开发完毕时,安装到宿主的时候,需要在宿主的 Gemfile 添加:
gem 'blorgh', path: "vendor/engines/blorgh"
运行 bundle install
安装时,Bundler 会去解析 blorgh.gemspec
,并安装其他相依的 Gems;同时,Bundler 会 require Engine lib
目录下的 lib/blorgh.rb
,这个文件又 require 了 lib/blorgh/engine.rb
,达到将 Engine 定义成 Module 的目的:
# lib/blorgh/engine.rb
module Blorgh
class Engine < ::Rails::Engine
isolate_namespace Blorgh
end
end
lib/blorgh/engine.rb
可以放 Engine 的全局设定。
Engine 继承自 Rails::Engine
,告诉 Rails 说:嘿!这个目录下有个 Engine 呢!Rails 便知道该如何安装这个 Engine,并把 Engine app
目录下的 model、mailers、controllers、views 加载到 Rails 应用程序的 load path 里。
isolate_namespace
方法非常重要!这把 Engine 的代码放到 Engine 的命名空间下,不与宿主冲突。
加了这行,在我们开发 Engine,产生 model 时 rails g model post
便会将 model 放在对的命名空间下:
$ rails g model Post
invoke active_record
create db/migrate/20130921084428_create_blorgh_posts.rb
create app/models/blorgh/post.rb
invoke test_unit
create test/models/blorgh/post_test.rb
create test/fixtures/blorgh/posts.yml
数据库的 table 名称也会更改成 blorgh_posts
。Controller 与 view 同理,都会被放在命名空间下。
想了解更多可看看 isolate_namespace
的源码:
app
目录
app
目录下有一般 Rails 应用程序里常见的 assets
、controllers
、helpers
、mailers
、models
、views
。
app/assets
目录
app/assets/
├── images
│ └── blorgh
├── javascripts
│ └── blorgh
│ └── application.js
└── stylesheets
└── blorgh
└── application.css
Engine 所需的 images
、javascripts
、stylesheets
,皆放在 blorgh
下(命名空间分离):
app/controllers
目录
Engine controller 的功能放这里。
app/controllers/
└── blorgh
└── application_controller.rb
注意到
module Blorgh
class ApplicationController < ActionController::Base
end
end
命名成 ApplicationController
的原因,是让你能够轻松的将现有的 Rails 应用程序,抽离成 Engine。
app/views
目录
app/views/
└── layouts
└── blorgh
└── application.html.erb
Engine 的 layout 放这里。Engine 单独使用的话,就可以在这里改 layout,而不用到 Rails 应用程序的 app/views/layouts/application.html.erb
下修改。
要是不想要使用 Engine 的 layout,删除这个文件,并在 Engine 的 controller 指定你要用的 layout。
bin
目录
bin
└── rails
这让 Engine 可以像原本的 Rails 应用程序一样,用 rails
相关的命令。
test
目录
test
├── blorgh_test.rb
├── dummy
│ ├── README.rdoc
│ ├── Rakefile
│ ├── app
│ ├── bin
│ ├── config
│ ├── config.ru
│ ├── db
│ ├── lib
│ ├── log
│ ├── public
│ └── tmp
├── fixtures
│ └── blorgh
├── integration
│ └── navigation_test.rb
├── models
│ └── blorgh
└── test_helper.rb
关于 Engine 的测试放这里。里面还附了一个 test/dummy
Rails 应用程序,供你测试 Engine,这个 dummy 应用程序已经装好了你正在开发的 Engine:
Rails.application.routes.draw do
mount Blorgh::Engine => "/blorgh"
end
test/integration
Engine 的整合测试(Integration test)放这里。其他相关的测试也可以放在这里,比如关于 controller 的测试(test/controller
)、关于 model (test/model
)的测试等。
4.4.2 配置 Engine
initializer、i18n、或是做其他的设定,在 Engine 里怎么做呢?Engine 其实就是个微型的 Rails 应用程序,所以可以像是在 Rails 里面那般设定。
要设定 initializer,在 Engine 目录 config/initializers
新增你的设定即可。关于 initializer 的更多说明请参考 Rails 官方文件的 Initalizers section
语系设定放在 Engine 目录下的 config/locales
即可。
就跟设定 Rails 应用程序一样。
Initalizer 例子:Devise devise_for
用过 Devise 的同学可能在 config/routes.rb
都看过 devise_for
,大概是怎么实现的呢?
以下代码仅做示意之用,并非实际 Devise 的代码:
# lib/devise/engine.rb
require 'devise/routing_extensions'
module Devise
class Engine < ::Rails::Engines
isolate_namespace Devise
initializer 'devise.new_routes', after: 'action_dispatch.prepare_dispatcher' do |app|
ActionDispatch::Routing::Mapper.send :include, Devise::RouteExtensions
end
end
end
# lib/devise/routing_extensions.rb
module Devise
module RouteExtensions
def devise_for
mount Devise::Engine => "/user"
get "sign_in", :to => "devise/sessions#new"
end
end
end
变更 Engine 默认的 ORM、模版引擎、测试框架
# lib/blorgh/engine.rb
module Blorgh
class Engine < ::Rails::Engines
isolate_namespace Blorgh
config.generators.orm :datamapper
config.generators.template_engine :haml
config.generators.test_framework :rspec
end
end
亦可:
# lib/blorgh/engine.rb
module Blorgh
class Engine < ::Rails::Engines
isolate_namespace Blorgh
config.generators do |c|
c.orm :datamapper
c.template_engine :haml
c.test_framework :rspec
end
end
end
Rails 3.1 以前请使用 config.generators
变更 Engine 的名称
Engine 的名称在两个地方会用到:
- routes
mount MyEngine::Engine => '/myengine'
有默认的 default 选项 :as
,
默认的名称便是 as: 'engine_name'
- 拷贝 migration 的 Rake task (如
myengine:install:migrations
)
如何变更?
module MyEngine
class Engine < Rails::Engine
engine_name "my_engine"
end
end
添加 Middleware 到 Engine 的 Middleware stack
# lib/blorgh/engine.rb
module Blorgh
class Engine < ::Rails::Engines
isolate_namespace Blorgh
middleware.use Rack::Cache,
:verbose => true,
:metastore => 'file:/var/cache/rack/meta',
:entitystore => 'file:/var/cache/rack/body'
end
end
撰写 Engine 的 Generator
让使用者轻松安装你的 Engine,比如:
$ rake generate blorgh:install
Generator 该怎么写呢?(示意)
# lib/generators/blorgh/install_generator.rb
module Blorgh
class InstallGenerator < Rails::Generator::Base
def install
run "bundle install"
route "mount Blorgh::Engine" => '/blorgh'
rake "blorgh:install:migrations"
...
end
end
end
路由优先权
将 Engine 安装至宿主之后,就会有 2 个 router。让我们看下面这个例子:
# host application
Rails.application.routes.draw do
mount MyEngine::Engine => "/blog"
get "/blog/omg" => "main#omg"
end
MyEngine
安装在 /blog
,/blog/omg
会指向宿主的 main
controller 的 omg
action。当有 /blog/omg
有 request 进来时,会先到 MyEngine
,要是 MyEngine
没有定义这条路由,则会转发给宿主的 main#omg
。
改写成这样:
Rails.application.routes.draw do
get "/blog/omg" => "main#omg"
mount MyEngine::Engine => "/blog"
end
则 Engine 只会处理宿主没有处理的 request。
重新命名 Engine Routing Proxy 方法
有两个地方可换 Engine 名字:
lib/blorgh/engine.rb
:
module Blorgh
class Engine < ::Rails::Engine
isolate_namespace Blorgh
engine_name "blogger"
end
end
- 在宿主或是使用 Engine 的(
test/dummy
)config/routes.rb
:
Rails.application.routes.draw do
mount Blorgh::Engine => "/blorgh", as: "blogger"
end