當(dāng)出現(xiàn)同一個(gè)功能模塊需要被多個(gè) Rails 項(xiàng)目使用時(shí), Rails 引擎(engine) 就是一個(gè)很好的選擇。
關(guān)于 Rails Engine, 官方文檔已經(jīng)寫(xiě)的很詳細(xì)了。
這里我記錄一下開(kāi)發(fā)一個(gè)引擎時(shí)各部分的寫(xiě)法。
初始化
rails plugin new foo --mountable
這會(huì)創(chuàng)建一個(gè)引擎的基本骨架,這里 foo 就是引擎的名字,由于使用了 --mountable 選項(xiàng)。
Foo 這個(gè)單詞會(huì)作為命名空間的存在,非常重要。
目錄結(jié)構(gòu)
一個(gè)引擎的目錄簡(jiǎn)單來(lái)說(shuō)就是一個(gè)加了命名空間的 rails 項(xiàng)目。其 controller model view 包括 assets layout 都要加命名空間(rails 中命名空間就對(duì)應(yīng)文件目錄)。
一個(gè)常規(guī)的 rails 項(xiàng)目本身也是一個(gè)引擎,宿主和引擎之間沒(méi)什么本質(zhì)的區(qū)別,存在的差異只是因?yàn)槁氊?zé)的不同而已。
由于引擎是依附于宿主 rails 應(yīng)用的,因此引擎有自己的 gemspec(e.g foo.gemspec),引擎以 gem 的形式存在。
只不過(guò)相比于普通的 gem,它更加全面,mvc 三層都被覆蓋到了。
不過(guò)引擎一般沒(méi)有自己的持久化配置,因?yàn)橐媸且凰拗髡{(diào)用的,因此數(shù)據(jù)庫(kù)配置一般都是由宿主決定的,引擎只提供邏輯代碼。
另外引擎可以有自己的 migration,宿主可以使用命令 rake foo:install:migrations 來(lái)把存在于引擎 foo 中的 migration 復(fù)制到自己的 db/migrate 目錄下,便于執(zhí)行。
與宿主的交互
在編寫(xiě)引擎的邏輯時(shí)和一般的 rails 應(yīng)用沒(méi)什么區(qū)別,重點(diǎn)就是如何和宿主應(yīng)用進(jìn)行交互。
路由
一個(gè)最簡(jiǎn)單的引擎只需要被宿主掛載就可以了,只要在宿主的路由中加入一行:
mount Foo::Engine => "/foo", 意為引擎 Foo 的 base url 就是 /foo 。
如果不用這個(gè) base url 把路由區(qū)分開(kāi)來(lái)的話(huà),就有可能出現(xiàn)路由被覆蓋的情況。
注意這里的 base url 和引擎的命名空間完全沒(méi)有關(guān)系, 因此引擎當(dāng)中的所有內(nèi)部跳轉(zhuǎn) 必須 全部使用
具名路由來(lái)表示,這樣才不會(huì)被 base url 所影響。
assets
引擎的 assets 是完全獨(dú)立的,要設(shè)定 precompile 的話(huà)需要在 lib/foo/engine.rb 中定義:
initializer "foo.assets.precompile" do |app|
app.config.assets.precompile += %w(foo/admin.css foo/admin.js)
end
assets 的路徑(包含 css,js,圖片等)都要使用 rails 提供的 helper 方法(assets_path 等)來(lái)指定。否則會(huì)出現(xiàn)和路由一樣的問(wèn)題。
view
view 層是最不通用的一層,用過(guò) devise 的都知道, view 層是幾乎一定會(huì)被 overwrite 的。
所以寫(xiě)引擎的時(shí)候盡量把 view 層寫(xiě)的簡(jiǎn)潔一些,不要有復(fù)雜邏輯,這樣 overwrite 的時(shí)候比較方便。
注意 layout 文件的位置是 layouts/foo/xxx 而不是 foo/layouts/xxx 哦。
model
引擎中的模型都是帶有命名空間的(它們對(duì)應(yīng)的表名也有命名空間的前綴), 在調(diào)用時(shí)最好加上命名空間(不然可能出現(xiàn)找不到定義的情況)。這點(diǎn)可以參考 Shoppe,包括在定義模型的關(guān)系時(shí),也最好指定帶有命名控件的 Class Name。
controller
這是有點(diǎn)麻煩的一層,跟宿主的耦合比較嚴(yán)重,比如說(shuō)如果我想要復(fù)用宿主應(yīng)用中的權(quán)限管理(在宿主的 app/controller/admin/base_controller.rb 中),那么我不得不讓引擎的 base_controller 都繼承 ::Admin::BaseController 才行,這是非常嚴(yán)重的耦合。
rails guide 是這么寫(xiě)的:
An easy way to provide this access is to change the engine's scoped ApplicationController to inherit from the main application's ApplicationController.
意思是說(shuō)哪怕是非常簡(jiǎn)單的獲得 current_user 的方法,我也要定義在宿主應(yīng)用的 application_controller 中才能讓引擎調(diào)用到,我寫(xiě)在 base_controller 中就不行了。
目前看起來(lái)這個(gè)問(wèn)題無(wú)解。。。好在自己開(kāi)發(fā)引擎的情況絕大多數(shù)都是給內(nèi)部應(yīng)用使用的,所以這也不算很大的問(wèn)題。
tips
獨(dú)立數(shù)據(jù)源
如果引擎需要自己的獨(dú)立數(shù)據(jù)庫(kù),可以讓引擎中的 model 都繼承一個(gè) base_model, 在其中配置數(shù)據(jù)源:
module Foo
class BaseModel < ActiveRecord::Base
self.abstract_class = true
establish_connection "foo_#{Rails.env}".to_sym
end
end
ps: 如果宿主使用的不是 active_record,而引擎是的話(huà),這種情況可以不指定 establish_conecton, 一樣可以做到引擎有獨(dú)立的數(shù)據(jù)源, 因?yàn)榭蚣苓€是會(huì)讀取 config/database.yml,然后根據(jù)使用的持久化框架來(lái)自動(dòng)指定對(duì)應(yīng)的數(shù)據(jù)庫(kù)。
元編程
在引擎中難免需要和宿主中的類(lèi)和對(duì)象進(jìn)行交互,這樣就需要?jiǎng)討B(tài)的修改宿主程序。
這就要用到元編程了,在 ruby 語(yǔ)言中,使用元編程更是家常便飯了。
在引擎中一般都在 lib/foo.rb 中個(gè)引擎同名文件中進(jìn)行一些初始化工作,當(dāng)然也包含元編程。
這里需要引入 decorators 這個(gè) gem,然后就可以開(kāi)始對(duì)宿主的類(lèi)和對(duì)象開(kāi)刀啦。
要使用 decorators,要先在 lib/foo/engine.rb 中初始化:
class << self
attr_accessor :root
def root
@root ||= Pathname.new(File.expand_path('../../../', __FILE__))
end
end
config.to_prepare do
Decorators.register! Engine.root, Rails.root
end
常見(jiàn)的例子就是給宿主的 User 類(lèi)添加一些和引擎的邏輯相關(guān)的方法。
require 'decorators'
module Foo
mattr_accessor :user_class
class << self
def decorate_user_class!
Foo.user_class.class_eval do
def say_hello
puts "hello from foo"
end
end
end
def user_class
Object.const_get(@@user_class)
end
end
end
順便說(shuō)一句,這里的 User(用戶(hù))的類(lèi)名是可以在宿主里指定的,只要在宿主初始化的時(shí)候定義諸如:
Foo.user_class = "Student" 這樣的一行代碼就行了。