Rails 引擎初探

作者: 编程青年 | 来源:发表于2016-02-11 07:54 被阅读787次

    当出现同一个功能模块需要被多个 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 引擎初探

        本文链接:https://www.haomeiwen.com/subject/dbqukttx.html