ASP.NET MVC:由AJAX调用的控制器应返回JSON还是呈现的HTML?

23

我在纠结一个由AJAX调用的控制器操作应该返回一个部分视图还是“原始”JSON。

返回一个已呈现HTML的部分视图使得JavaScript可以更轻松地使用返回的HTML更新当前DOM。然而,它限制了JavaScript客户端对使用Web服务返回的HTML所能做的事情。

另一方面,控制器操作返回JSON则要求调用的JavaScript基于返回的JSON“手动”创建标记。

因此,像往常一样,每种方法都有其优点和缺点。每种方法还有其他的优缺点吗?


在评论区的讨论中:总之,有很多方法可供选择。使用你喜欢的任何一种。MVC 真是太棒了!!! - IsmailS
7个回答

18
在我看来,返回JSON数据然后让客户端处理可能会变得混乱,因为有以下限制:
  1. JavaScript没有标准的模板语言。最糟糕的情况下,你会被诱惑去连接字符串以形成所需的HTML。
  2. 没有简单的方法来单元测试通过连接代码生成的HTML。
  3. JavaScript缺乏IntelliSense支持,这意味着你容易犯更多的错误。
为了解决这个问题,我的做法是返回呈现的HTML,但是使用部分视图来返回呈现的HTML。这样可以兼顾两方面的好处。你可以得到服务器端的模板和 IntelliSense 支持。
这是一个例子:
这是我的Ajax调用,如你所见,它只是替换无序列表的HTML:
FilterRequests: function() {
    $.post("/Request.aspx/GetFilteredRequests", { }, function(data) {
        $('ul.requests').html(data);
    });
},

这是我的控制器上的操作:

public ActionResult GetFilteredRequests(string filterJson)
{
    var requests = _requestDao.LoadAll();

    return PartialView("FilteredRequests", requests);
}

最后这是我的部分视图(没必要理解这个,我只是想展示一下在现实世界应用中渲染可以变得多么复杂。如果用JavaScript来做这件事,我会感到害怕的。你还会注意到,我的部分视图反过来也调用了其他部分视图。):

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<IEnumerable<Request>>" %>
<%@ Import Namespace="Diangy.HelpDesk.Models.Lookups"%>
<%@ Import Namespace="Diangy.HelpDesk.Models.Requests"%>
<%@ Import Namespace="System.Web.Mvc.Html"%>

<% foreach (var request in ViewData.Model) %>
<%{%>
    <li class="request">
        <h2>#<%= request.Id %>: <%= request.InitialInteraction().Description %></h2>
        <p>from <%= request.Customer.FullName %> (<%= request.Customer.EmailAddress %>), <%= request.InitialInteraction().UsableTimeStamp %></p>

        <h3>Custom Fields & Lookups</h3>
        <div class="tabs">
            <ul>
            <li><a href="#customfields<%= request.Id %>">Custom Fields</a></li>
            <% foreach (var lookupDefinition in (List<LookupDefinition>)ViewData["LookupDefinitions"]) %>
            <%{%>
            <li><a href="#<%= lookupDefinition.Name.ToLowerInvariant().Replace(" ", "") + request.Id %>"><%= lookupDefinition.Name %></a></li>
            <%}%>
        </ul>
        <% Html.RenderPartial("CustomFields", request); %>
    </div>

    <% Html.RenderPartial("Discussion", request); %>
    <% Html.RenderPartial("Attachment", request); %>
    </li>
<%}%>

1
完全不同意。使用JSON需要做更多的工作,但网络流量更小,呈现逻辑保留在“智能”客户端(jQuery、ExtJS等)。新的MS AJAX将具备模板引擎。 - Hrvoje Hudo
7
按照这种逻辑,我们应该让所有控制器操作仅返回视图数据。在那个世界里,视图引擎就没用了。我认为Praveen的意思是,在更新多个DOM元素时,通过视图引擎渲染HTML是最理想的方法。这种方法与在操作中调用“View(...)”、“PartialView()”渲染HTML片段和渲染HTML文档的“View()”完全相同。除非你想放弃视图引擎的概念,否则我不确定你如何能如此强烈地反对。 - Dane O'Connor
完全不同意。看看http://code.google.com/closure/templates/docs/helloworld_js.html。这是一个非常成熟和快速的模板系统,没有必要使用丑陋的字符串拼接。将此逻辑分开允许您编写另一个客户端(iPhone、Swing、Flash)而无需重写服务器端代码,因为您将服务器委托执行其最佳操作——提供数据——并将视图部分移动到客户端。Google的模板系统还允许您在服务器端运行模板,以防需要为不支持JavaScript的客户端提供服务。 - Ruan Mendes

16
如果你正在使用MVC模式,控制器应该返回数据(JSON),并让视图自己进行排序,就像它的工作是在服务器端在模型中查找/适应数据并将其传递给视图一样。
你可以通过以下方式获得额外的奖励:
- 保留逻辑和UI之间的分离。 - 使你的ajax操作可测试(祝你好运测试从该操作返回的HTML...)
这可能有点更加复杂,但它很适合。
你可以使用客户端模板系统(例如现在在MS Ajax Toolkit中提供的)来帮助减轻负担并在客户端保留逻辑/呈现分离。
因此,我会说JSON,当然,你的情况可能有所不同。

他说的没错。即使你不关心MVC模式,JSON在客户端上操作和验证会更容易。 - Gromer
几乎所有我的控制器操作都返回json/xml。但是,每个页面都有一个返回html页面的操作。你可以使用完全静态的客户端,并使所有操作返回json,但我更喜欢将初始数据作为嵌入式json发送到html中,以节省往返时间,因为客户端必须连接到服务器才能查看文件是否已被修改。 - Ruan Mendes

4

我希望让调用应用程序来决定。我已经组合了一个MultiViewController(其中大部分代码是我在网上找到的,当我找到时,我会尽力更新它的来源),根据操作扩展名,将返回适当的格式。例如:

myapp.com/api/Users/1 - defaults to html based on route
myapp.com/api/Users.html/1 - html
myapp.com/api/Users.json/1 - json
myapp.com/api/Users.xml/1 - xml
myapp.com/api/Users.partial/1 - returns a partial view of action name (see code)
myapp.com/api/Users.clean/1 - partial html without styling, etc...

我的控制器继承自MultiViewController,而不是使用"return view(Model);",我只需调用"return FormatView(Model);"或者"FormatView("ViewName",Model);"。如果需要将特定的视图应用于结果而不是暗示的视图,则使用后者。

MultiViewController的代码如下。请特别注意FormatView,它返回一个操作结果:

    public abstract class MultiViewController : Controller
{
    private const string FORMAT_KEY = "format";
    public enum FileFormat {Html, Json, Xml, Partial, Clean}

    protected MultiViewController()
    {
        RequestedFormat = FileFormat.Html;
    }
    protected FileFormat RequestedFormat { get; private set; }

    protected override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        base.OnActionExecuting(filterContext);
        var routeValues = filterContext.RouteData.Values;
        if (routeValues.ContainsKey(FORMAT_KEY))
        {
            var requestedFormat = routeValues[FORMAT_KEY].ToString();
            if (isValidFormat(requestedFormat))
            {
                RequestedFormat = (FileFormat)Enum.Parse(typeof(FileFormat), requestedFormat, true);
            }
        }
    }

    private bool isValidFormat(string requestedFormat)
    {
        return Enum.GetNames(typeof (FileFormat)).Any(format => format.ToLower() == requestedFormat.ToLower());
    }

    protected ActionResult FormatView(string viewName, object viewModel)
    {
        switch (RequestedFormat)
        {
            case FileFormat.Html:
                if (viewName != string.Empty)
                {
                    return View(viewName,viewModel);
                }
                return View(viewModel);
            case FileFormat.Json:
                return Json(viewModel);
            case FileFormat.Xml:
                return new XmlResult(viewModel);
            case FileFormat.Partial:
            //return View(this.ControllerContext.RouteData.Values["action"] + "Partial");
            return PartialView(this.ControllerContext.RouteData.Values["action"] + "Partial");
            case FileFormat.Clean:
                if (viewName != string.Empty)
                {
                    return View(viewName, "~/Views/Shared/Clean.master", viewModel);
                }
                var v = View(viewModel);
                v.MasterName = "~/Views/Shared/Clean.Master";
                return v;
            default:
                throw new FormatException(string.Concat("Cannot server the content in the request format: ", RequestedFormat));
        }
    }
    protected ActionResult FormatView(object viewModel)
    {
        return FormatView("", viewModel);
    }




}

Clean.master是一个简单的主页面,不包含任何其他的html - 它采用视图(以便我可以合并任何部分类),并使用干净的html直接呈现。

如果我想要json - 控制器构建我的视图模型,然后将该视图模型作为json返回,而不是发送到默认视图 - .xml也是同样的情况。

部分视图有点有趣,按照惯例,所有的主视图都被拆分成部分视图,这样特定的部分视图就可以单独请求 - 这对于使用jquery模仿updatepanel的功能而不带有与updatepanel相关的所有垃圾非常方便。


相当有趣。很像Rails所允许的操作。对于许多情况来说都是合理的。 - Charlie Flowers

2
为了保持关注点的分离,您应该返回JSON。
当您返回HTML时,会限制您可以使用返回数据做的事情。如果您需要数据列表并希望以不同的方式呈现它,请使用JSON,否则您将不得不在服务器上拥有不同的方法来获取相同数据的不同呈现方式。

2
为什么不同时支持json和html呢? 在当前项目中,我们正在制作路由,因此您可以从前端选择最适合某些情况的格式...那么为什么不创建两个控制器,第一个将以json返回预期的数据,而另一个控制器将返回相同的数据但是以html格式...这样您就可以从jQuery中选择想要的内容和格式,并且在何时何地使用它...最好的事情是,对于不同的格式,您只需要调用不同的地址... 换句话说,让它变得更加RESTful!:) 干杯

这也可以通过请求的接受内容类型来处理,因为您实际上正在查看相同的资源(URI),但是不同的表示。我不知道ASP.net MVC是否可以根据此路由到不同的操作,这对我来说似乎比在操作处理程序中使用巨大的switch case更好... - Denis Troller
我认为这样做更清晰:{控制器}/{操作}/{...}如果你需要JSON格式,可以从jQuery中使用:json/list/....如果需要HTML:html/list/...干杯 - Marko

2
关于我个人而言,我选择数据驱动的方法。以下是两种方法的一小组特性:
数据驱动:
1. 从服务器获取JSON(控制器直接将模型发送到响应) 2. JS模板绑定(可能需要更多时间在客户端) 3. 低带宽负载 4. 真的太酷了!
HTML驱动:
1. 从服务器获取HTML(控制器将模型发送到某些部分视图,它呈现结果并返回结果——可能需要更多时间在服务器端) 2. JS绑定不会过载 3. 高带宽负载 4. 不酷,不要不要 :)
因此,您可以使用HTML驱动的方法来维护MVC,虽然这可能会有点困难。

0
如果您使用JQuery,应该使用JSON或XML,因为它更容易修改,但是如果您的ajax调用仅返回例如列表框的项目,则也可以使用html。
我的个人偏好是JSON或XML,因为经常使用JQuery。

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