clean up rails view

作者:周星 发布:2017-10-07

个别同学可能看到这个标题就已经开始喷我了,“大哥,这都啥年代了,前后端都分离了,你还扯 Rails 的 View 干啥”?

这里的 View 不只是渲染 HTML,还包括提供API,不管是渲染页面还是提供API,Rails 的 View 很容易变得庞大臃肿,逻辑乱飞,大多数 Rails 程序员喜欢这么做:

1. 逻辑都放到 controller

2. 逻辑都放到 helper

3. 瘦 controller 胖 model,把逻辑尽可能都放到 model 上

第一种做法的缺点显而易见,controller里去拼大量的实例变量,方便在 View 中使用,现在已经很少有人这么干了,笔者“有幸”接触过这样的项目,大家可以感受一下

#encoding: utf-8
class MeetingPadsController < ApplicationController
  layout "padtemplate"
  before_filter :checkPlatform
  def show
    groupid = 0
    if params[:id] !=nil
      groupid = params[:id]
    end
    meeting_rooms = MeetingRoom.where("GroupId="+groupid.to_s)
    t = Time.new
    date = t.strftime("%Y-%m-%d")
    time = t.strftime("%H:%M:%S")
    @room1=""
    @status1=""
    @room2=""
    @status2=""
    if meeting_rooms!=nil && meeting_rooms.length==2
      @room1 = meeting_rooms[0].RoomName + "-"+meeting_rooms[0].MaxAttending.to_s+"-"+meeting_rooms[0].Device
      @status1 =ActiveSupport::SafeBuffer.new(getmeetingbook(meeting_rooms[0].id,date,time))
      @room2 = meeting_rooms[1].RoomName + "-"+meeting_rooms[1].MaxAttending.to_s+"-"+meeting_rooms[1].Device
      @status2 =ActiveSupport::SafeBuffer.new(getmeetingbook(meeting_rooms[1].id,date,time))
      @roomid1=meeting_rooms[0].id
      @roomid2=meeting_rooms[1].id
    end
    @displaymeetings = displayrooms
    @displayAll = displayschedules
    @displaydate = displayDates
    respond_to do |format|
      format.html # show.html.erb
    end
  end
end


把逻辑放到 helper 里要比放在 controller 里好得多,因为 helper 天生就是设计给 View 用的,在 View 里可以随意调用 helper 里的方法,而 controller 和 model 不行(当然你也可以通过一些手段这么做),但是这样做存在一些不妥之处,看下面这个例子:

# app/helpers/apples_helper.rb
module ApplesHelper
  def produce_date(apple)
    apple.produce_date.strftime '%Y-%m-%d'
  end
end

# app/views/apples/show.html.erb
<span><%= produce_date @apple %></span>

* produce_date 方法只是给 apple 来用的,但是却污染了全局空间

* produce_date 方法接收一个 apple 参数,从 domain model 上来说,强行把功能方法放在了面向对象的 domain model 里,读起来也很奇怪,更合理的应该是 produce_date 是 apple 的一个属性/方法

* 有些时候通过读 View 层的代码,你很难知道 produce_date 这个方法是从哪里来的,helper? View 里定义的?还是某个 gem 提供的?

* Ruby 的弱类型决定了任何东西都可以传给 produce_date 方法,比如有个 user 对象也可以被当作参数传给 produce_date,如果恰好 user 也有一个 produce_date 属性,在 user 自己渲染的 View 层可能会产生意料之外的 Bug。


原生的 Rails 只提供了MVC三层结构,“瘦 controller 胖 model” 是我在一开始接触 Rails 时,有经验的开发告诉我的 Rails “最佳实践”,model 在 MVC 中处于最底层,model 上的方法可以在各处调用,从面向对象来看,这么做有时并不合理,看下面这个例子:

# app/models/apple.rb
class Apple < ActiveRecord::Base
  def is_fresh?
    produce_date > 2.days.ago 
  end
end

# app/controllers/apples_controller.rb
class ApplesController < ApplicationController
  def show
    @apple = Apple.find(params[:id])
  end
end

# app/views/apples/show.html.erb
<% if @apple.is_fresh? %>
  <%= image_tag(@apple.image) %>
<% end %>

is_fresh? 方法的存在只是为了 View 的渲染,这样一来我们的 model 和 view 就耦合在了一起,尤其是当需求增多了之后,仅仅为了渲染,就把 model 变得很肥,使得 model 承担了超过它本身应该承担的责任,在 Rails 里组织 view 层的代码可以有很多种方法,partial 和 helper 是 Rails 里的标准方法,同样 Decorator 也可以把 View 的逻辑从 Model 上分离出来,同样上面的例子,我们试试用 Decorator 来尝试一下解决这个问题。


首先在 app 目录下直接创建 decorators 文件夹,Rails4 默认自动加载这个文件夹下的文件(未考证 Rails3),然后创建 apple_decorator.rb 文件

# app/decorators/apple_decorator.rb
class AppleDecorator
  attr_reader :produce_date
  
  def initialize(apple)
    @apple = apple
  end
  
  def is_fresh?
    @apple.produce_date > 2.days.ago
  end
end


这样展示的逻辑就被抽到了 decorator 里面,下面我们要在 controller 里使用它实例化变量,并在 View 中使用使用这个变量

# app/controllers/apples_controller.rb
class ApplesController < ApplicationController
  def show
    apple = Apple.find(params[:id])
    @apple_decorator = AppleDecorator.new(apple)
  end
end

# app/views/apples/show.html.erb
<%= if @apple_decorator.is_fresh? %>
  <%= image_tag(@apple_decorator.image) %>
<%= end %>


看起来似乎不错,但是,wait!,程序报错:在 apple_decorator 上找不到 image 方法,作为一个 decorator,它还没有把原来 model 上的方法/属性代理到原 model 上,这时就需要定义 method_missing 来转发这些方法/属性,注意,复写 method_missing 方法时一定要定义 respond_to_missing? 方法,否则可能会使用者产生疑惑

# app/decorators/apple_decorator.rb
class AppleDecorator
  ...
  def method_missing(method_name, *args, &block)
    @apple.send(method_name, *args, &block)
  end
  
  def respond_to_missing?(method_name, include_private = false)
    @apple.respond_to?(method_name, include_private) || super
  end
end


这样我们就纯手写了一个 decorator,除此之外你也可以用 ruby 自带的 SimpleDecorator 或者 Decorator 来更好的实现它,其实这里我们其实是用 Decorator 实现了 Presenter,Decorator 让用户在不改变原有对象的结构下,给其添加方法。从项目经验来说,https://github.com/drapergem/draper 是一个成熟的方案,它的诞生就是为了解决 view-model 这个问题,GitHub 首页上有详细的文档,这里就不多叙述,但是同样它有几点让我有一点点不大喜欢

* 可能会促成一些 bad smell,有的时候你会很惊讶为什么一个 apple.produce_date 返回了一个格式化后的东西,甚至是一段 html。

* 你不得不使用它提供的方法来实例化对象,@apple = Apple.find(1).decorate,在搜索代码时你会很疑惑哪些实例被 decorate 了。

* draper 使用了一些魔法来在 Rails 上面,强制 Rails 接受 non-model 对象,并且把方法代理到原生对象上,这在做分页或者其它操作时需要你做一些额外的操作。

* draper 提供的东西太多,但是一般来说,大多数的功能我们根本不需要,引入了一个庞大的东西进来,但是做了很少的事情。


使用 Plain old Ruby Objects (PORO) 来实现 presenter 也是一种可行的方案,优点是灵活度高,可以根据自己的需要来设计,网上有很多文章可供参考。


如何充分的利用 partial、helper、presenter,让它们各尽其职呢?下面是我的个人实践:

1. 用 partial 来拆分很大大的 View,比如拆分出 side_bar,menu_list 或者 navigation_bar 等

2. 公共的 View 逻辑放到 helper 里,比如构造一个 link;或者与 model 关系不大的方法,比如构造一个 form 等。

3. 和 model 相关,但是仅针对于 View 渲染的逻辑放到 presenter


总结:​

* presenter 是一个独立的,完全和 model 分开的概念

* presenter 本质不过是 View Model

* PORO 或者 Decorator

* 保持 helper、partial 整洁合理

支付宝扫码赞助博主


评论(0)