65.9K
CodeProject 正在变化。 阅读更多。
Home

ASP.NET MVC 捆绑内部机制

starIconstarIconstarIconstarIconstarIcon

5.00/5 (6投票s)

2014年2月16日

CPOL

5分钟阅读

viewsIcon

46921

ASP.NET MVC 捆绑内部机制

将多个脚本和样式文件最小化并合并到一个文件中的想法在 Web 开发人员中已经流行了相当长一段时间。随着 ASP.NET MVC 的第 4 版,Microsoft 引入了一个机制(称为捆绑)来允许 .NET 开发人员自动化和控制这个过程。尽管捆绑配置和使用起来相当简单,但有时它们可能不会按预期工作。在这篇文章中,我将向您介绍捆绑的内部机制,并向您展示如何排查它们可能引起的问题。

捆绑架构

要检查捆绑,让我们在 Visual Studio 2013 中创建一个默认的 ASP.NET MVC 项目。该项目应在 App_Start 文件夹中包含一个 BundleConfig.cs 文件,其中定义了一些捆绑路由,例如:

public class BundleConfig
{
    // For more information on bundling, visit http://go.microsoft.com/fwlink/?LinkId=301862
    public static void RegisterBundles(BundleCollection bundles)
    {
        bundles.Add(new ScriptBundle("~/bundles/jquery").Include(
                    "~/Scripts/jquery-{version}.js"));
        ...
    }
}

在上述代码从 Global.asaxApplication_Start 事件调用后,将创建一个新的路由,并且对 https://:8080/bundles/jquery.js?v=JzhfglzUfmVF2qo-weTo-kvXJ9AJvIRBLmu11PgpbVY1 的请求将渲染一个最小化的 jQuery 版本(除非 <compilation> 标签没有将 debug 属性设置为 true)。要理解它是如何工作的,让我们看看捆绑是如何与 ASP.NET 管道交互的。众所周知,发往 ASP.NET 应用程序的请求需要由一个处理程序来服务。首先,IIS 根据掩码(applicationhost.config 中的 handlers 标签)分配一个默认处理程序。然后,请求由配置文件中定义的所有 HTTP 模块处理(在集成模式下,还必须满足一个先决条件)。每个模块都有机会更改已分配的处理程序。最后,选择的处理程序处理请求。从 .NET 4 开始,还可以动态地将 HTTP 模块注入 ASP.NET 管道,从我们的应用程序代码中。为此,我们需要在我们的程序集中添加一个 PreApplicationStartMethodAttribute 属性。当 HTTP 运行时检测到带有此类属性的程序集时,它将在应用程序启动前执行该属性定义的方法。由于我们正在检查捆绑,让我们以 System.Web.Optimization.dll 程序集为例。它设置了以下属性:

[assembly: PreApplicationStartMethod(typeof (PreApplicationStartCode), "Start")]

并且 PreApplicationStartCode 类如下所示:

[EditorBrowsable(EditorBrowsableState.Never)]
public static class PreApplicationStartCode
{
  private static bool _startWasCalled;

  /// <summary>
  /// Hooks up the BundleModule
  /// </summary>
  public static void Start()
  {
    if (PreApplicationStartCode._startWasCalled)
      return;
    PreApplicationStartCode._startWasCalled = true;
    DynamicModuleUtility.RegisterModule(typeof (BundleModule));
  }
}

请注意,上面的代码在 ASP.NET 管道中注册了一个新的 BundleModule

    public class BundleModule : IHttpModule
    {
      ...
      private void OnApplicationPostResolveRequestCache(object sender, EventArgs e)
      {
        HttpApplication app = (HttpApplication) sender;
        if (BundleTable.Bundles.Count <= 0)
          return;
        BundleHandler.RemapHandlerForBundleRequests(app);
      }
      ...
    }

仅当名称与我们的捆绑包相同的 static 文件不存在时,才会发生重新映射。

internal static bool RemapHandlerForBundleRequests(HttpApplication app)
{
  HttpContextBase context = (HttpContextBase) new HttpContextWrapper(app.Context);
  string executionFilePath = context.Request.AppRelativeCurrentExecutionFilePath;
  VirtualPathProvider virtualPathProvider = HostingEnvironment.VirtualPathProvider;
  if (virtualPathProvider.FileExists(executionFilePath) || 
                virtualPathProvider.DirectoryExists(executionFilePath))
    return false;
  string bundleUrlFromContext = BundleHandler.GetBundleUrlFromContext(context);
  Bundle bundleFor = BundleTable.Bundles.GetBundleFor(bundleUrlFromContext);
  if (bundleFor == null)
    return false;
  context.RemapHandler((IHttpHandler) new BundleHandler(bundleFor, bundleUrlFromContext));
  return true;
}

在选择 BundleHandler 来处理给定请求后,它会创建一个捆绑操作的上下文,并在 BundleTable 中查找应该发送到浏览器的捆绑包。捆绑包是根据它们的哈希值进行缓存的,因此对同一捆绑包的后续调用比第一次调用执行得快得多。

IIS 中捆绑的配置

为了简单起见,我将只关注 IIS7+ 中的集成管道。您需要确保 ASP.NET 处理程序已为您的捆绑请求调用,否则它们将不会被服务。如果您使用的是 /bundlename?v=bundlehash 形式的 URL,那么 IIS 中的默认处理程序配置(如下所示)应该就可以了。

<handlers>
    ...
    <add name="ExtensionlessUrlHandler-Integrated-4.0" path="*." 
     verb="GET,HEAD,POST,DEBUG" type="System.Web.Handlers.TransferRequestHandler" 
     preCondition="integratedMode,runtimeVersionv4.0" />
    <add name="StaticFile" path="*" verb="*" 
     modules="StaticFileModule,DefaultDocumentModule,DirectoryListingModule" 
     resourceType="Either" requireAccess="Read" />
</handlers>

并且在 IIS 失败请求跟踪中,您应该会看到以下事件(我用红色标记了与捆绑相关的事件):

iis bundle request trace

请注意,IIS 最初分配的 ExtensionlessUrlHandler-Integrated-4.0 处理程序随后被 System.Web.Optimization.BundleHandler 替换。我们已经知道,这种替换是由 System.Web.Optimization.BundleModuleRESOLVE_REQUEST_CACHE 通知上(在图像中用红色标记)有序进行的。

故障排除

到目前为止,我们已经检查了捆绑的内部机制以及它们与 ASP.NET (IIS) 管道的正确交互。但是,如果出现问题,并且您看到的是压缩得很好的 JavaScript,而是收到 **404 HTTP 响应** 呢?我们有一个应用程序在生产环境中遇到了这样的问题。在部署了该应用程序的新版本后,捆绑一直无法工作(返回 404 代码)。我们发现的唯一解决方法是在部署后重新启动应用程序池。正如您可以想象的,这是一个不太理想的解决方案,所以我开始调查问题的原因。在测试过程中,我发现当应用程序在部署过程中被请求打断时(例如,被我们的负载均衡器检查应用程序是否响应),这个问题就会出现。我们应用程序中一个示例 JavaScript 捆绑包的路径是:bundle/Site.js?v=77xGE3nvrvjxqAXxBT1RWdlpxJyptHaSWsO7rRkN_KU1。您是否注意到此 URL 与 ASP.NET 示例应用程序中的 URL 之间有细微差别?是的,就是那个 .js 扩展名!URL 中这个小部分极大地改变了 IIS 处理捆绑请求的方式。直到应用程序准备就绪(完全部署)之前,IIS 都尝试使用 StaticFileHandler(这符合其处理程序掩码配置)来服务它们。此外,IIS 似乎会缓存为给定 URL 运行了哪些模块。因此,即使我们的应用程序已准备好服务捆绑请求,IIS 也不会在它们上运行 System.Web.Optimization.BundleModule。我们最终从捆绑 URL 中删除了 .js 扩展名。另一个解决方案可能是将 ExtensionlessUrlHandler-Integrated-4.0 的掩码更改为 *。这将强制 IIS 为应用程序的所有请求运行托管模块。

如果您想检查哪些文件被包含在一个捆绑包中,您可以通过修改 User-Agent 头为 Eureka/1 来篡改请求(例如,使用 fiddler),示例如下:

GET https://:8080/Content/css?v=WMr-pvK-ldSbNXHT-cT0d9QF2pqi7sqz_4MtKl04wlw1 HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Cache-Control: max-age=0
Accept: text/css,*/*;q=0.1
If-Modified-Since: Sat, 15 Feb 2014 15:52:46 GMT
User-Agent: Eureka/1
Referer: https://:8080/
Accept-Encoding: gzip,deflate,sdch
Accept-Language: en-US,en;q=0.8,pl;q=0.6,fr-FR;q=0.4,fr;q=0.2

以及响应:

HTTP/1.1 200 OK
Cache-Control: private
Content-Type: text/css; charset=utf-8
Vary: Accept-Encoding
Server: Microsoft-IIS/8.0
X-AspNet-Version: 4.0.30319
X-SourceFiles: =?UTF-8?B?YzpcdGVtcFxidW5kbGUtdGVzdFxDb250ZW50XGNzcw==?=
X-Powered-By: ASP.NET
Date: Sat, 15 Feb 2014 22:12:52 GMT
Content-Length: 14076

/* Bundle=System.Web.Optimization.Bundle;Boundary=MgAwADcANgAwADIAMwAyADUA; */
/* MgAwADcANgAwADIAMwAyADUA "~/Content/site.css" */
html {
    background-color: #e2e2e2;
    margin: 0;
    padding: 0;
}
...

摘要

我希望这篇文章能帮助您更好地理解 ASP.NET 捆绑。它们是一个很棒的机制,可以自动分组和最小化应用程序中的脚本和样式文件。如果您遇到任何问题,请记住 IIS 失败请求跟踪和 Eureka/1 用户代理。 :)

归类于:CodeProject, 诊断 ASP.NET

© . All rights reserved.