Rails 引擎初探

當(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", 意為引擎 Foobase 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" 這樣的一行代碼就行了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容