ASP.NET MVC 捆绑内部机制





5.00/5 (6投票s)
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.asax 的 Application_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 最初分配的 ExtensionlessUrlHandler-Integrated-4.0
处理程序随后被 System.Web.Optimization.BundleHandler
替换。我们已经知道,这种替换是由 System.Web.Optimization.BundleModule
在 RESOLVE_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