Gem Spree 扩展机制分析

cxh116 · 2015年09月20日 · 最后由 cxh116 回复于 2015年11月26日 · 4407 次阅读

原文地址:http://blog.mangege.com/tech/2015/09/20/1.html

参考资料

扩展的分类

  • 类的扩展,主要是对 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 还是能正常运行。

virtual_path 概念能具体说一下么,这个和i18n里面的有关联么?

Spree 本地开发环境太慢了,刷新一下 8、9 秒钟,有没有解决办法?

#2 楼 @tini8 印象中是因为数据库查询都太慢了,我估计 Spree 是想把性能优化任务留给开发者吧

#1 楼 @ericguo virtual_path 只是需要修改的 html 模板的相对 Rails.root 目录的路径。不包含文件类型后缀. 这个和 i18n 没有关联,只是修改 html 用的。

需要 登录 后方可回复, 如果你还没有账号请 注册新账号