参考资料

扩展的分类

  • 类的扩展,主要是对 Model 与 Controller 进行修改. 其它像 Concern 与 Helper 都从属于 Model 与 Controller,一般直接改 Model 与 Controller 即可.
  • 视图的扩展,主要是对 Html 视图进行修改. JS 与 CSS 因为可以通过代码加载顺序来重写现有功能.

类的扩展

类的扩展的实现主要是基于 Ruby 的 Open classes 特性实现.

创建一个测试项目,请先参考 https://github.com/spree/spree 建立一个 Rails Project . 图省事,就不用 spree extension 命令建立一个 Rails engine ,而直接在 Rails Project 写代码测试.

示例一: 访问首页时在控制台打印文字

添加 app/controllers/spree/home_controller_decorator.rb 文件,文件内容如下:

module Spree
  HomeController.class_eval do
    alias_method :old_index, :index
    def index
      puts "#{'#'*100} index test"
      old_index
    end
  end
end

alias_method 是 Rails 的方法,用于重命名现有的方法并删除,方便重写方法时再调用老的方法.

Open classes 除了可以用 class_eval 这样来实现,还可以直接用 class A; end 这样的类定义语法来实现同样的功能.
之所以用 class_eval ,有两个个人能想到的优点:

  1. 用 class_eval 这种形式,肯定会先把原来的 class 给加载, 而用类定义语法就不一定了.
  2. 类定义语法,再次打开类,还需要记得原来的 class 的父类,如果不同的话,到时会报 superclass mismatch for class 错误.

文件名结尾一定要以 decorator 结尾,这样才能保证在开发模式时,每次自动请求会自动重新加载此文件.

decorator 分析

查看 Spree 源码的 core/lib/spree/core/engine.rb 文件,可以看到这样一段代码:

  config.to_prepare do
    # Load application's model / class decorators
    Dir.glob(File.join(File.dirname(__FILE__), '../../../app/**/*_decorator*.rb')) do |c|
      Rails.configuration.cache_classes ? require(c) : load(c)
    end
  end

to_prepare 为 Rails 的方法,此处用来加载 decorator 文件. glob 用来查找所有包含 _decorator 的文件.
Rails.configuration.cache_classes 判断是否开启类缓存, 开启的话,用require加载文件,可以防止重复加载.否则用load方法,这样能保证每次请求,decorator的代码都是最新的.

to_prepare 分析

在项目里的 config/application.rb 文件增加以下内容:

config.to_prepare do
  puts "#{'$'*100} to_prepare test"
end

重启 rails server, 可以看到在启动后,就执行了添加的回调. 但再次访问不会执行回调. 随便修改一个 controller 文件,可以看到回调再次执行了. 基于 to_prepare 方法,这样就可以保证被修改的类不会被漏加载.

视图的扩展

视图的扩展有两种实现方法

1. 基于 Rails view path的加载顺序实现

添加 app/views/spree/home/index.html 文件,内容随便写点,比如 hello

再次访问首页,可以看到首页的内容变成 hello 去了.

View Paths 这一章的文档刚好没有,所以个人简单的介绍一下.

在rails console运行 ActionController::Base.view_paths.each{|a| puts a.to_path}; nil , 可以看到所有视图目录, Rails 是在这些目录下一个一个找,找到了就停止查找. 可以看到, Rails Proejct 的目录是在最前面的.

这种方式会替换此视图,没办法像 Deface 可以根据 DOM 查找添加内容到指定位置,或删除指定节点.

删除 app/views/spree/home/index.html 文件,方便再测试.

2. 基于 Deface 实现

示例在首页的侧边添加一行 Hello world

在 Rails 项目里新建 app/overrides/add_hello_to_home.rb 文件,文件内容如下:

Deface::Override.new(
  :virtual_path => 'spree/home/index',
  :name => 'add_hello_to_home',
  :insert_after => "erb[silent]:contains('sidebar')",
  :text => "<p><%= 'hello world' * 10 %></p>"
)

之后访问首页,可以看到侧边顶部增加一行hello world.

执行 rake deface:precompile 命令,可以看到生成了 app/compiled_views/spree/home/index.html.erb 文件内容,内容如下:

<% content_for :sidebar do %><p><%= 'hello world' * 10 %></p>
  <div data-hook="homepage_sidebar_navigation">
    <%= render :partial => 'spree/shared/taxonomies' %>
  </div>
<% end %>

<div data-hook="homepage_products">
  <% cache(cache_key_for_products) do %>
    <%= render :partial => 'spree/shared/products', :locals => { :products => @products } %>
  <% end %>
</div>

而原始文件 frontend/app/views/spree/home/index.html.erb 内容如下:

<% content_for :sidebar do %>
  <div data-hook="homepage_sidebar_navigation">
    <%= render :partial => 'spree/shared/taxonomies' %>
  </div>
<% end %>

<div data-hook="homepage_products">
  <% cache(cache_key_for_products) do %>
    <%= render :partial => 'spree/shared/products', :locals => { :products => @products } %>
  <% end %>
</div>

重启 rails console,再运行 ActionController::Base.view_paths.each{|a| puts a.to_path}; nil 语句,可以看到, app/compiled_views 这个目录的顺序是在 app/views 前面,排在第一位,所以最终还是靠 view paths来实现的.

deface 的作用是用来修改 erb 文件,但它解决了 erb 不能通过 dom 树来查找的问题.

分析 deface 的源码发现, 在 lib/deface/parser.rb 此文件,可以知道 deface 只是简单的把 <%= %> <% %> 替换成 <erb loud> <erb silent> </erb> 这样的非标准的html标签,再通过 Nokogiri 解析,执行 deface override代码里的替换,替换完后再把erb标签替换回来.

结尾

示例项目源码: https://github.com/mangege/spree_hack_example

为类增加代码很简单,但删除就很麻烦.比如从 Model 移除一个属性的 validate ,这个时候需要分析Rails的validate的实现,再写hack代码.

单元测试非常重要,因为没有单元测试,你没有办法保证你的 hack 代码在下个版本的 spree 和 rails 还是能正常运行.