【问题标题】:Rails MVC Architecture - View with multiple controller sourcesRails MVC 架构 - 具有多个控制器源的视图
【发布时间】:2021-04-27 23:33:01
【问题描述】:

我的 Rails 应用程序中有一个页面,它是一个控制器的显示操作,但它包含一个提交以创建另一个控制器的表单。这对于简单地将创建表单提交到第二个控制器来说很好,但是因为表单包含在备用控制器的显示操作中,所以我不能轻易地在表单上显示验证错误。这是结构的基本概念:

class VendorsController < ApplicationController
  def show
    @vendor = […]
  end
end
<!-- vendors_controller/show -->
<h1><%= @vendor.name %></h1>

<%= form_with model: Order, local: true do |f| %>
  <%= f.hidden_field :vendor_id, value: @vendor.id %>

  […]
<% end %>
class OrdersController < ApplicationController
  def create
    @order = […]
    if @order.save
      […]
    else
      render 'new' # Not where user came from, actually want to go to vendors_controller/show
    end
  end
end

我发现在 vendors_controller/show 的“新”上下文中处理创建订单的唯一方法是将 OrdersController#create 移动到 VendorsController 内的自定义 create_order 方法,但这会将核心 REST 逻辑移动到一个模型到不同的控制器,感觉就像一个主要的代码气味。来自移动工程背景,我们可以将屏幕拆分为多个子视图,并使用独立的控制器来处理不同的元素,但这当然在 Web 上并不可行,因为单个控制器处理传入的请求和响应。

构建此行为的正确方法是什么,以便我可以保留OrdersController 的大部分 REST 功能,同时还将新操作“嵌入”到另一个控制器拥有的视图中?我想将Order 相关逻辑保留在OrdersController 中,但重定向回VendorsController#show 会破坏验证中的错误,将来我可能还想要一个具有相同逻辑但与VendorsController 分开的OrdersController#new 路由。

【问题讨论】:

  • 从概念上讲,您实际上可以将视图拆分为不同的“控制器”,但在 Web 上使用 JavaScript 和 XHR 请求完成。

标签: ruby-on-rails ruby model-view-controller architecture controller


【解决方案1】:

实际上,您没有针对资源的单独new 操作是一种相当常见的情况。而且您在 Rails 术语中对它的推理是错误的,因为它实际上只是关于您正在呈现和发送回什么视图以响应请求。

您可以在此处处理无效的表单提交,方法是呈现新视图(可能不是您要查找的 UX)或使用 AJAX 提交表单并替换元素:

// app/views/orders/new.js.erb
document.querySelector("#order_form")
        .innerHTML("<%= j(render(partial: 'form', locals: { order: @order })) %>");
# app/views/orders/_form.html.erb
# removed "local: true" so thats its sent as a XHR request
<%= form_with model: order, html: { id: "order_form" } do |f| %>
  <%= f.hidden_field :vendor_id %> # smelly AF - use a nested route instead
<% end %>
class OrdersController < ApplicationController
  def create
    @order = […]
    if @order.save
      […]
    else
      respond_to do |f|
        f.js { render :new }
      end
    end
  end
end

如果您不想走 Server Side Concerns(又名 js.erb spagetti)路线,您可以执行 JSON 响应并处理在客户端呈现错误。

虽然可以通过 OrdersController#create 方法渲染 vendors/show.html.erb 视图,但如果验证失败,这是一个完全不合适的提议,因为它使控制器负责渲染完全不同的资源。

虽然在这个简单的示例中这并不是真正的问题,但如果您将功能添加到“供应商/展示”视图,您将不得不在两个地方复制设置您传递给视图的数据的逻辑。

即使您确实通过使用水平继承(顾虑)减少了潜在的代码重复问题,您仍然在践踏单一职责原则。

class OrdersController < ApplicationController
  # POST /vendors/:vendor_id/orders
  def create
    @vendor = Vendor.find(params[:vendor_id])
    @order = […]
    if @order.save
      […]
    else
      render "vendors/show"
    end
  end
end

让您的 VendorsController 处理创建订单更糟糕,甚至不应该考虑。

【讨论】:

  • 这里还有一些其他问题 - 您使用的是 model: Order 并传递了类而不是 Order 的实例。坏主意,因为您必须通过侧面传递所有用户输入。您还使用了隐藏输入,您应该在其中使用嵌套路由。 guides.rubyonrails.org/routing.html#nested-resources
  • 我希望避免使用 Ajax,所以关注是一个很好的折衷方案。我希望有一个传统呈现的 HTML 方法,但看起来关注点是处理此类问题的正确方法。
  • 我不确定我是否真的会以正确的方式称呼它。
  • “最不臭”。这种方法确实需要额外的视图文件,但这种方法的关注点分离要好得多。
  • 但要记住的一点是,这将重新加载页面并滚动到顶部,这可能是比 ajax 更“笨拙”的用户体验。
【解决方案2】:

我会调查accepts_nested_attributes_for

There's a railscasts episode on this 解释了如何在保持 RESTful 的同时进行嵌套表单

你或多或少会做这样的事情:

<!-- vendors_controller/show -->

<h1><%= @vendor.name %></h1>

<%= fields_for :orders do |builder| %>
  <%= render 'order_fields', f: builder %>
<% end %>

然后将订单表单字段放入一个新的部分中:

<!-- app/views/vendors/_order_fields.html.erb -->

<%= f.hidden_field :vendor_id, value: @vendor.id %>

<!-- and the rest of the order fields, too... -->

别忘了在你的 vendor#show controller logic 中加入像 @vendor.orders.build 这样的行

【讨论】:

  • 虽然我确实意识到您努力编写和回答嵌套属性在这里并不适用。这并不是您需要在单个表单提交中创建嵌套资源的情况 - 而是一个相当常见的场景,您的表单不在单独的 /new 路由上,而是嵌入在另一个资源的视图中.
  • 我将深入研究 railscast,但根据 max 的评论,看起来这可能无法完全解决我的问题。我已经对某些表单使用了部分,这更多的是我担心的控制器逻辑:如何避免在 VendorsController 中重复属于 OrdersController 的逻辑,同时保持 new 视图(或至少是几个视图之一) new views) 在由VendorsController 管理的视图中。
  • 是的,这里肯定解决不了任何问题。如果您想在单个表单提交中创建供应商和订单,嵌套属性将是合适的。您还需要考虑到 RailsCasts 已经不复存在将近 10 年了,虽然它们在当时非常出色,但现在已经过时了。
猜你喜欢
  • 1970-01-01
  • 2014-12-06
  • 2023-03-21
  • 2013-12-06
  • 1970-01-01
  • 2016-06-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多