Rails: 重构、视图、辅助函数:它们如何相互配合?

17

注意:我是新手。

我知道这是一个微不足道的主题,但我在尝试弄清楚如何通过将视图的某些部分移入助手来简化它们方面遇到了很多困难。例如,我一直读到视图中的条件语句是提取为助手的主要候选项,但我无法找到这方面的示例,并且我的尝试失败了。

例如,假设我有以下内容:

#index.html.erb

<% for beast in @beasts do -%>
  <% if beast.dead? -%>
    <%= beast.body %>
    <%= link_to "bury", bury_beast_path( :id => beast.id ) %>
  <% else -%>
    <%= beast.body %>
    <%= link_to "kill!", kill_beast_path( :id => beast.id ) %>
  <% end -%>
<% end -%>

这个在我的视图里有点让我烦心,但是我该如何将它移动到一个helper里面?如果可能的话,进一步简化它。(我看过某处的条件语句很糟糕,但就算是在我之外,也无法理解怎样能编写任何东西而没有它们。)

另一个例子:我需要使用格式controller_actionbody标签进行id设置。目前最好的方案是:

#index.html.erb

<body id="<%= controller_action %>">

...并且...

#application_helper.rb

def controller_action
  @id = @controller.controller_name + "_" + @controller.action_name
end

我不是专家,但即使是对我来说,那也太丑了。

更加复杂的是,Ryan Singer说了一些我喜欢的话:将ERB视为图像标记,使用帮助程序“揭示意图”。然后在下一句中说,你不应该在帮助程序中有任何HTML,否则就会走向地狱。这两件事情怎么兼容呢?如果已经到了可以在视图中声明行为的程度,那么肯定有很多HTML需要在后台渲染吧?我无法理解。

所以,基本上就是这样。如果有人能分享一些对此的想法,或将我指向一些关于这个主题的深入阅读材料,我会非常感激 - 我发现网络上对此的覆盖率真的很弱。我已经用尽了谷歌搜索,但谁知道呢。


小提示:使用 for 循环是可以的,但使用迭代器更符合 Ruby 的惯例,例如 <% @beasts.each do |beast| %> - Sarah Vessels
@Sarah:谢谢。我只是真的喜欢使用“for”时的自然语言方面。 - San Diago
4个回答

27
重构使得视图更易于维护,问题在于要选择将重构代码放到哪里。
你有两个选择,一个是局部文件(partials),另一个是帮助方法(helpers)。没有硬性规定哪个应该用在哪里。有一些指导方针,比如说建议不要在帮助方法中包含 HTML 标签。
通常情况下,局部文件更适合重构 HTML/ERB/HAML 比 Ruby 代码多的部分。而帮助方法则用于包含最少 HTML 或从参数生成简单 HTML 的 Ruby 代码块。
然而,我不同意帮助方法完全不应该包含 HTML 的观点。多少都没关系,只是不要过度使用。帮助方法的处理方式会影响它们生成大量 HTML 的使用。这就是为什么建议您的帮助方法包含最少量的 HTML。如果您查看 Rails 自带的帮助方法的源代码,您会注意到其中大多数都生成了 HTML。而那些没有生成 HTML 的少数帮助方法,则主要用于生成参数并评估常见条件。
例如,任何表单帮助方法或 link_to 变体都属于第一种帮助方法形式。而各种身份验证模型提供的 url_for 和 logged_in? 等内容则属于第二种。
这是我用来确定将代码从视图重构为局部文件还是帮助方法的决策链。
1. 重复或几乎相同的语句生成单个浅层 HTML 标记? => 帮助方法。 2. 作为另一个帮助方法的参数使用的常见表达式? => 帮助方法。 3. 用作另一个帮助方法参数的长表达式(超过 4 个项)? => 帮助方法。 4. 4 行或更多 Ruby 代码(未转换为 HTML)? => 帮助方法。 5. 其他所有情况 => 局部文件。
我将使用您要重构的代码作为示例:
我会按照以下方式重构问题中的视图:
app/helpers/beast_helper.rb:
def beast_action(beast)
  if beast.dead?
    link_to "bury", bury_beast_path(beast)
  else
    link_to "kill!", kill_beast_path(beast)
  end
end

app/views/beasts/_beast.html.erb:

<%= beast.body %>
<%= beast_action(beast) %>

app/views/beasts/index.html.erb:

<%= render :partial => "beast", :collection => @beasts %>

从技术上讲,这更加复杂,因为它涉及3个文件和10行代码,而不是1个文件和10行代码。视图现在只有3行代码,分散在2个文件中。最终结果是你的代码更加干净、更具可重用性,可以在其他控制器/操作/视图中最小限度地添加复杂性来重用其部分或全部。

至于你的 body 标签 ID,你应该真正使用 content_for/yield 来实现这样的功能。

app/views/layouts/application.html.erb

...
<body id="<%= yield(:body_id) %>">
...

应用程序视图文件夹中的beasts/index.html.erb

<% content_for :body_id, controller_action %>
...

这将允许您在需要的任何视图中覆盖body标签的id,例如:

app/views/users/preferences.html.erb

<% content_for :body_id, "my_preferences" %>

只要局部文件位于 beasts/_beast.html.erb,你可以直接使用 render @beasts - Garrett
@Garrett:这只适用于Rails 2.3或更新版本。我不知道为什么新用户会使用旧版本。但我觉得那个版本更安全。 - EmFi
好的,随着Rails 3的即将到来,我认为现在开始推动render @beasts是安全的。 - Garrett
谢谢!你的回答非常详细,我会经常回来查看你建议的决策链,直到它深入我的脑海。我还需要尝试你的代码,但我想它会起作用。它看起来肯定比之前好多了。再次感谢。 - San Diago

8
我会首先做以下事情:

第一件事是:

#index.html.erb
<%= render @beasts %>

#_beast.html.erb
<%= beast.body %>
<%= link_to_next_beast_action(beast) %>    

#beast_helper.rb
def link_to_next_beast_action(beast)
  if beast.dead?
    link_to "bury", bury_beast_path( :id => beast.id ) 
  else
    link_to "kill!", kill_beast_path( :id => beast.id )
  end
end

我所做的是将野兽的呈现分离成一个使用集合语义的部分。
然后,我将显示杀死/埋葬链接的逻辑移动到了野兽助手中。这样,如果您决定添加另一个操作(例如,“从死亡中带回”),您只需要更改您的助手。
这有帮助吗?

你甚至可以执行 render @beasts,因为它会意识到这是一个集合。 - Garrett
看起来你也进行了同样的重构。我在撰写回答时没有注意到有新的答案。 - EmFi
2
@EmFi,我给你的答案点赞是因为你更详细地解释了何时使用部分视图/帮助程序。 - jonnii
我很感激,但这并不必要。我已经超越了为声望回答问题的阶段。 - EmFi
即便如此,你仍然值得认可 =) - jonnii

1
第三种选择是使用Cells gem中的视图模型。这是一个非常流行的框架,将面向对象引入了Rails的视图层。
# app/cells/beast/cell.rb

class Beast::Cell < Cell::Concept
  def show
    return dead if model.dead?
    kill
  end

private
  def dead
    link_to "bury", bury_beast_path( :id => model.id ) 
    # you could render a view here, too!
  end

  def kill
    link_to "kill!", kill_beast_path( :id => model.id )
  end
end

您可以使用帮助程序(在视图或控制器中)来呈现视图模型。
# app/views/beasts/index.erb

<%= concept(:beast, @beast).call %>
<%-# this returns the link content %>

就这些!您可以在单独的测试中对此单元格进行测试。单元格还提供视图渲染、视图继承和许多其他功能。

例如,您可以使用视图来处理kill链接。

# app/cells/beast/cell.rb

class Beast::Cell < Cell::Concept

  # ..

  def kill
    render :kill
  end
end

这将呈现单元格的 killer 视图。
# app/cells/beast/views/index.erb

<%= link_to "kill!", kill_beast_path( :id => model.id ) %>

注意视图的位置,它被很好地封装到单元目录中。
是的,单元可以使用HAML和任何其他AbstractController支持的模板引擎。

0

另一种策略是完全不使用模板和帮助程序。对于渲染,您可以:

  1. 直接从控制器中使用render(:inline => )来渲染视图。如果您仍然想要正式分离视图和控制器,可以创建模块/混合并将其包含到控制器中。
  2. 或者创建自己的视图类并使用它们来渲染响应。

这背后的思想是,帮助程序和Rails erb模板系统没有利用面向对象编程(OOP)的优势,因此最终您无法定义通用行为,而是根据每个控制器/请求的需求进行特殊化;往往会重写非常相似的代码块,这在维护方面不太好。

然后,如果您仍然需要一些帮助程序方法(例如form_tag、h、raw等),您只需将它们包含在控制器/专用视图类中即可。

请参见:rails-misapprehensions-helpers-are-shit,这是一篇有趣但有用的文章。

编辑:为了不显得完全自大,我会说实现这个取决于你的应用程序有多大,以及你将要多频繁地更新你的代码。此外,如果你将设计委托给一个非程序员,他/她可能需要在深入研究模板语法之前参加一些编程课程,这显然比使用模板语法更难理解。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接