原文地址:http://blog.mangege.com/tech/2015/09/20/1.html
类的扩展的实现主要是基于 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 ,有两个个人能想到的优点:
superclass mismatch for class
错误。文件名结尾一定要以 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 的代码都是最新的。
在项目里的 config/application.rb 文件增加以下内容:
config.to_prepare do
puts "#{'$'*100} to_prepare test"
end
重启 rails server, 可以看到在启动后,就执行了添加的回调。但再次访问不会执行回调。随便修改一个 controller 文件,可以看到回调再次执行了. 基于 to_prepare 方法,这样就可以保证被修改的类不会被漏加载。
视图的扩展有两种实现方法
添加 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 文件,方便再测试。
示例在首页的侧边添加一行 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 还是能正常运行。