這篇是我在 RubyConfChina 2013 的 Talk:Maintainable Rails View。
當初會整理這個 Talk 的原因是因為長久以來:相對於 View,在一個 Project 裡面,設計出乾淨的 Model 與 Controller,是相對簡單的。但事情一跑到 View,就會變得相當複雜。很難有一個基礎簡單的思路去整理這些糾結的線條。
所以我最後決定釋出一份這樣的整理指南。這其實也是我們 Rocodev 目前在用的 Rails View 整理技巧。
前情提要
要了解這些用法中間的轉折,首先我必須先解釋幾個前提,這是這些「整理方法」之所以被發明的原因:
- 在 View 裡面有 Logic 糾纏 ( if / else & other syntax )是不好的,這會導致 View 很難修改以及維護
- 在 View 裡面有 Logic 糾纏是不好的,這會導致 View Performance 下降 ( pure logic )。
- 在 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
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
或者是
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 系列文目錄