almost 4 years ago

這篇是我在 RubyConfChina 2013 的 Talk:Maintainable Rails View

當初會整理這個 Talk 的原因是因為長久以來:相對於 View,在一個 Project 裡面,設計出乾淨的 Model 與 Controller,是相對簡單的。但事情一跑到 View,就會變得相當複雜。很難有一個基礎簡單的思路去整理這些糾結的線條。

所以我最後決定釋出一份這樣的整理指南。這其實也是我們 Rocodev 目前在用的 Rails View 整理技巧。

前情提要

要了解這些用法中間的轉折,首先我必須先解釋幾個前提,這是這些「整理方法」之所以被發明的原因:

  1. 在 View 裡面有 Logic 糾纏 ( if / else & other syntax )是不好的,這會導致 View 很難修改以及維護
  2. 在 View 裡面有 Logic 糾纏是不好的,這會導致 View Performance 下降 ( pure logic )。
  3. 在 View 裡面有 Logic 糾纏是不好的,這會導致 View Performance 嚴重下降 ( with data query )。而這包含在 Helper 裡面 perform data query。

這個 Talk 會包含以下幾個主題:

  • Helper Best Pratices
  • Partial Best Pratices
  • 除了 Helper 與 Partial 之外的整理武器
  • Object-Oriented View

我會在這篇文章裡面,介紹 18 個整理手法。

值得注意的是,這些手法是「循序漸進」的,也就是前面的手法未必是「最好」的,而是在「初期整理階段」是一個好的手法,而事情變得複雜的時候,你才需要越後面的技巧去協助整理。

1. Move logic to Helper

這是一段經常在 View 裡面直覺寫出來的判斷式。

<% if current_user && current_user == post.user %>
  <%= link_to("Edit", edit_post_path(post))%>
<% end %>
  • 如果只有一個條件,如 if current_user,則不用進行整理
  • 如果在第一次撰寫時,就發現會有兩個條件,則在最初撰寫時,就使用一個簡易的 helper 整理。

例:

<% if editable?(post) %>
  <%= link_to("Edit", edit_post_path(post))%>
<% end %>



i.e. editable?(post) 並不是一個好的名字,不過可以先標上打上 # TODO: REFACTOR,之後再回來整理。

2. Pre-decorate with Helper (常用欄位預先使用 Helper 整理)

在設計 Application 時,常常會遇到某些欄位,其實在初期設計時,就會不斷因為規格擴充,一直加上 helper 裝飾。比如 Topic 的 content :

   <%= @topic.content %>

在幾次的擴充之下,很快就會變成這樣:

   <%= auto_link(truncate(simple_format(topic.content), :lenth => 100)) %>

而這樣的內容,整個 Application 可能有 10 個地方。每經過一次規格擴充,developer 就要改十次,還可能改漏掉。

針對這樣的情形,我們是建議在第一次在進行 Application 設計時,就針對這種「可能馬上就會被大幅擴充」的欄位進行 Helper 包裝。而不是「稍候再整理」

   <%= render_topic_content(@topic) %>

常見的情形如:

  • render_post_author
  • render_post_published_date
  • render_post_title
  • render_post_content

3. Use Ruby in Helper ALL THE TIME ( 全程在 Helper 裡面使用 Ruby )

有時候會因為要對 View 進行裝飾的原因,會被迫在 Helper 裡面設計出這種 Code

double quote
def post_tags_tag(post, opts = {})
  tags = post.tags
  raw tags.collect { |tag|  "<a href=\"#{posts_path(:tag => tag)}\" class=\"tag\">#{tag}</a>" }.join(", ")
end

或者是

single quote
def post_tags_tag(post, opts = {})
  tags = post.tags
  raw tags.collect { |tag| "<a href='#{posts_path(:tag => tag)}' class='tag'>#{tag}</a>" }.join(", ")
end

這是 非常不好 的設計手法,在 Ruby Helper 裡面穿插純 HTML 與 quote 記號,會很容易因為少關一個 quote,就導致 syntax error。另外一個潛在副作用是:Helper 被這樣一污染,Developer 因為害怕程式碼爆炸,很容易就降低了重構的意願。

因此,嚴格禁止在 Ruby Helper 裡面穿插任何 HTML 標記。請使用任何可以生成 HTML 的 Ruby Helper 取代。

def post_tags_tag(post, opts = {})
  tags = post.tags
  raw tags.collect { |tag| link_to(tag,posts_path(:tag => tag)) }.join(", ")
end

4. mix Helper & Partial (混合使用 Helper 與 Partial )

穿插 HTML 在 Helper 裡面還有另外一個後遺症。Helper 的輸出最後往往要用 raw / .html_safe 實作 HTML unescape。

def render_post_title(post)
  str = ""
  str += "<li>"
  str += link_to(post.title, post_path(post))
  str += "</li>"
  return raw(str) 
end

從而造成了一個非常巨大的 security issue。Ruby on Rails 的標準預設是 HTML escape,避免了非常多會被 XSS 攻擊的可能。穿插 HTML 在 Helper 的設計,導致了一個巨大的曝險地位。

因此,只要遇到需要穿插稍微複雜 HTML 的場景,請不吝惜使用 Helper 與 Partial 穿插的技巧實作。如修改成以下的程式碼:

def render_post_title(post)
  render :partial => "posts/title_for_helper", :locals => { :title => post.title }
end

常見的設計情境如:

  • category in list
  • post title in breadcrumb
  • user name with glyphicons

5. Tell, Don't ask

有些時候,開發者會在 New Relic 發現某個 view 的 Performance 低落,但是卻抓不出來實際的問題在哪裡。這是因為是慢在 helper 裡面。

這是一個相當經典的範例:

def render_post_taglist(post, opts = {})
  tags = post.tags
  tags.collect { |tag| link_to(tag,posts_path(:tag => tag)) }.join(", ")
end
 <% @posts.each do |post| %>
  <%= render_post_taglist(post) %>
<% end %>

這是因為在 View / Helper 裡面被 query 的資料是不會 cache 起來的。在 helper 裡面才 tags 出來,這樣的設計容易造成 N+1 問題,也會造成 template rendering 的效率低落。

改進方法:盡量先在外部查詢,再傳入 Helper 裡面「裝飾」
def render_post_taglist(tags, opts = {})
  tags.collect { |tag| link_to(tag,posts_path(:tag => tag)) }.join(", ")
end
 <% @posts.each do |post| %>
  <%= render_post_taglist(post.tags) %>
<% end %>

   def index
    @posts = Post.recent.includes(:tags)
  end

6. Wrap into a method ( 包裝成一個 model method )

有時候,我們會寫出這種 Helper code :

def render_comment_author(comment)
  if comment.user.present?
    comment.user.name
  else
    comment.custom_name
  end
end

這段程式碼有兩個問題:

  • Ask, Not Tell
  • 問 name 的責任其實不應放在 Helper 裡面

可以作以下整理,搬到 Model 裡面,這樣 author_name 也容易實作 cache :

def render_comment_author(comment)
  comment.author_name
end
class Comment < ActiveRecord::Base
  def author_name
    if user.present?
      user.name
    else
      custom_name
    end
  end
end


Maintainable Rails View 系列文目錄

← 先做軟體,不要先做平台 Maintainable Rails View (2) →
 
comments powered by Disqus