Rails 引擎初探

当出现同一个功能模块需要被多个 Rails 项目使用时, Rails 引擎(engine) 就是一个很好的选择。

关于 Rails Engine, 官方文档已经写的很详细了。

这里我记录一下开发一个引擎时各部分的写法。

初始化

rails plugin new foo --mountable

这会创建一个引擎的基本骨架,这里 foo 就是引擎的名字,由于使用了 --mountable 选项。
Foo 这个单词会作为命名空间的存在,非常重要。

目录结构

一个引擎的目录简单来说就是一个加了命名空间的 rails 项目。其 controller model view 包括 assets layout 都要加命名空间(rails 中命名空间就对应文件目录)。

一个常规的 rails 项目本身也是一个引擎,宿主和引擎之间没什么本质的区别,存在的差异只是因为职责的不同而已。

由于引擎是依附于宿主 rails 应用的,因此引擎有自己的 gemspec(e.g foo.gemspec),引擎以 gem 的形式存在。

只不过相比于普通的 gem,它更加全面,mvc 三层都被覆盖到了。

不过引擎一般没有自己的持久化配置,因为引擎是要被宿主调用的,因此数据库配置一般都是由宿主决定的,引擎只提供逻辑代码。

另外引擎可以有自己的 migration,宿主可以使用命令 rake foo:install:migrations 来把存在于引擎 foo 中的 migration 复制到自己的 db/migrate 目录下,便于执行。

与宿主的交互

在编写引擎的逻辑时和一般的 rails 应用没什么区别,重点就是如何和宿主应用进行交互。

路由

一个最简单的引擎只需要被宿主挂载就可以了,只要在宿主的路由中加入一行:
mount Foo::Engine => "/foo", 意为引擎 Foobase url 就是 /foo
如果不用这个 base url 把路由区分开来的话,就有可能出现路由被覆盖的情况。
注意这里的 base url 和引擎的命名空间完全没有关系, 因此引擎当中的所有内部跳转 必须 全部使用
具名路由来表示,这样才不会被 base url 所影响。

assets

引擎的 assets 是完全独立的,要设定 precompile 的话需要在 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 等)来指定。否则会出现和路由一样的问题。

view

view 层是最不通用的一层,用过 devise 的都知道, view 层是几乎一定会被 overwrite 的。
所以写引擎的时候尽量把 view 层写的简洁一些,不要有复杂逻辑,这样 overwrite 的时候比较方便。
注意 layout 文件的位置是 layouts/foo/xxx 而不是 foo/layouts/xxx 哦。

model

引擎中的模型都是带有命名空间的(它们对应的表名也有命名空间的前缀), 在调用时最好加上命名空间(不然可能出现找不到定义的情况)。这点可以参考 Shoppe,包括在定义模型的关系时,也最好指定带有命名控件的 Class Name。

controller

这是有点麻烦的一层,跟宿主的耦合比较严重,比如说如果我想要复用宿主应用中的权限管理(在宿主的 app/controller/admin/base_controller.rb 中),那么我不得不让引擎的 base_controller 都继承 ::Admin::BaseController 才行,这是非常严重的耦合。
rails guide 是这么写的:

An easy way to provide this access is to change the engine's scoped ApplicationController to inherit from the main application's ApplicationController.

意思是说哪怕是非常简单的获得 current_user 的方法,我也要定义在宿主应用的 application_controller 中才能让引擎调用到,我写在 base_controller 中就不行了。
目前看起来这个问题无解。。。好在自己开发引擎的情况绝大多数都是给内部应用使用的,所以这也不算很大的问题。

tips

独立数据源

如果引擎需要自己的独立数据库,可以让引擎中的 model 都继承一个 base_model, 在其中配置数据源:

module Foo
  class BaseModel < ActiveRecord::Base
    self.abstract_class = true
    establish_connection "foo_#{Rails.env}".to_sym
  end
end

ps: 如果宿主使用的不是 active_record,而引擎是的话,这种情况可以不指定 establish_conecton, 一样可以做到引擎有独立的数据源, 因为框架还是会读取 config/database.yml,然后根据使用的持久化框架来自动指定对应的数据库。

元编程

在引擎中难免需要和宿主中的类和对象进行交互,这样就需要动态的修改宿主程序。
这就要用到元编程了,在 ruby 语言中,使用元编程更是家常便饭了。
在引擎中一般都在 lib/foo.rb 中个引擎同名文件中进行一些初始化工作,当然也包含元编程。
这里需要引入 decorators 这个 gem,然后就可以开始对宿主的类和对象开刀啦。
要使用 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

常见的例子就是给宿主的 User 类添加一些和引擎的逻辑相关的方法。

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

顺便说一句,这里的 User(用户)的类名是可以在宿主里指定的,只要在宿主初始化的时候定义诸如:
Foo.user_class = "Student" 这样的一行代码就行了。

你可能感兴趣的:(Rails 引擎初探)