在 ASP.NET MVC 中合并/压缩/最小化 JS 和 CSS 文件






4.73/5 (7投票s)
本文将介绍一种在 ASP.NET MVC 应用程序中向客户端交付 CSS 和 JavaScript 文件的方法。更具体地说,它将展示如何在 ASP.NET MVC 应用程序的上下文中交付压缩、最小化、合并和缓存的 CSS/JavaScript 文件。
引言
本文将介绍一种在 ASP.NET MVC 应用程序中向客户端交付 CSS 和 JavaScript 文件的方法。更具体地说,它将展示如何在 ASP.NET MVC 应用程序的上下文中交付压缩、最小化、合并和缓存的 CSS/JavaScript 文件。其目标是提高客户端的整体性能。同样,它还将展示我们如何强制客户端刷新相同的 CSS 和 JavaScript 文件(这意味着我们无需更改文件名)。当使用引入新功能和功能的 AJAX 技术时,这一点变得越来越重要。简而言之,我们可以确保我们的客户端拥有必要升级的 JavaScript 文件,以执行新功能,而不是浏览器缓存中存储的文件。
代码
本文的灵感来自于 Khaled Atashbahar 的文章。我使用了许多相同的技术,但主要区别在于我使用了一个自定义 Controller 和 ActionResult,而不是 HttpHandler
来处理最小化/合并/渲染。我想通过利用我的路由来保持与 MVC 模式的一致性。此外,我使用了 Structure Map 依赖注入。当我在标记中使用指向 HTTPHandler
的路径,而该路径未在我的 IContollerFactory
工厂中注册时,DI、MVC 和 HTTP handler 就无法很好地协同工作。为此,让我们直接进入代码,展示它是如何工作的。首先,在我的母版页中,我放置了以下标记。对于 CSS 文件,我有:
<link type="text/css" rel="Stylesheet"
href="https://codeproject.org.cn/cache/cachecontent/CSSInclude/
<%=GetApplicationVersionID() %>/css" />
对于 JavaScript 文件,我有:
<script type="text/javascript"
src="https://codeproject.org.cn/cache/cachecontent/JavaScriptInclude/
<%=GetApplicationVersionID() %>/javascript" />
这些是控制器 URL 路径,它将负责将文件写入客户端。现在,为了理解路径引用是如何工作的,让我们看一下我在 Global.asax 文件中定义的适用路由。
routes.Add(new
Route ("cache/{action}/{key}/{version}/{type}", new MvcRouteHandler()) {
Defaults = new RouteValueDictionary(new { controller = "Cache",
action = "CacheContent", key = "", version = "", type = ""
}),
}
);
那么这个路由是什么意思呢?它意味着当我们遇到格式为 /cache/cachecontent/CSSInclude/12345/css 的 URL 时,请求将被转交给 Cache 控制器,特别是 CacheContent
操作方法来处理该事件。关于 URL 格式的一些说明:
{action} = 控制器接收请求时调用的方法。
{key} = web.config 中保存所有要合并/压缩和最小化文件的逗号分隔值列表的键的名称。让我们看一个 web.config 文件中的示例片段。
<add key="CssInclude" value="~/Content/styles/globalStylesheet.css,
~/Content/styles/ie6.css"/>
<add key="JavaScriptInclude" value=
"~/Content/scripts/jquery=1.2.6.min.js,
~/Content/scripts/jquery-DatePickerUI.js,
~/Content/scripts/jQuery.BlockUI.js,
~/Content/scripts/jquery.selectboxes.min.js,
~/Content/scripts/jquery.swfobject.js,
~/Content/scripts/MicrosoftAjax.js,
~/Content/scripts/MvcAjax.js,~/Content/scripts/DataServices.js,
~/Content/scripts/s_code.js,
~/Content/scripts/template.js,
~/Content/scripts/ValidationServices.js" />
在这里,我们看到 CSS 和 JavaScript 都有一个键名。如果我们使用 URL 中的 JavaScriptInclude
,我们将合并值列表中的所有 JavaScript 文件。
{version} = 我们将在渲染文件时使用的版本号。我在 URL 路径中使用了一个数字,以便我们可以更改 URL,让浏览器认为它之前没有见过该文件。版本号可以来自数据库,甚至可以来自 web.config。关键在于我们可以利用版本值来控制客户端缓存。在开发中,我使用一个随机数,但在生产环境中,我使用在 web.config 中配置的某个值。使用随机数的原因是我可以确保我的浏览器始终使用最新、最棒的 JavaScript 文件版本。这样在开发过程中就不需要清除浏览器缓存了。我敢肯定,我们都曾因忘记在跟踪一个不起作用的功能时清除缓存而吃过苦头。
{type} = 一个名称值,用于告知我们正在处理 CSS 还是 JavaScript 请求。
接下来,让我们看看我们的缓存控制器。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Mvc.Ajax;
using System.Drawing;
using System.Web.Configuration;
using System.Reflection;
using System.Web.Routing;
using System.IO;
using System.Web.Caching;
namespace Cache
{
public class CacheResult : ActionResult
{
private string _keyname;
private string _version;
private string _type;
public CacheResult(string keyname, string version, string type)
{
this._keyname = keyname;
this._version = version;
if ( type.ToLower().Contains("css") )
{
this._type = @"text/css";
}
if ( type.ToLower().Contains("javascript") )
{
this._type = @"text/javascript";
}
}
public override void ExecuteResult(ControllerContext context)
{
if (context == null)
throw new ArgumentNullException("context");
ScriptCombiner myCombiner = new ScriptCombiner
( this._keyname, this._version, this._type);
myCombiner.ProcessRequest(context.HttpContext);
}
}
public static class CacheControllerExtensions
{
public static CacheResult RenderCacheResult
(string keyname, string version, string type)
{
return new CacheResult(keyname, version, type);
}
}
public class CacheController :Controller
{
#region Constructor Definitions
public CacheController()
: base()
{
}
#endregion
#region Method Definitions
#region public
public CacheResult CacheContent(string key, string version, string type)
{
return CacheControllerExtensions.RenderCacheResult(key, version, type);
}
public CacheResult ClearCache()
{
//LOGIC TO CLEAR OUT CACHE
}
#endregion
#endregion
} // End class CacheController
}
在这里,我们看到我们的 CacheContent
方法调用了一个名为 RenderCacheResult
的 ControllerExtension
方法。它传递了要渲染的文件键名、它应该使用的版本以及它将渲染的内容类型(CSS 或 JavaScript)。繁重的工作由自定义 CacheResult
类中的 ExecuteResult
方法完成。此方法使用了 Khaled Atashbahar 在文章开头提到的技术。为了完整起见,我将代码包含在下面。不过,我想指出我对他代码所做的一些更改。首先,我更改了类,使其能够通过向 CacheResult
构造函数添加类型参数来处理 CSS 和 JavaScript 文件。接下来,我删除了查询字符串参数的使用,转而使用传递给构造函数的值。我还更改了 GetScriptFileNames
,使其能够利用存储在 web.config 文件中的文件名,而不是通过查询字符串传递的值。最后,在 ProcessRequest
期间,我省略了最小化步骤,以便在调试模式下更容易调试 JavaScript 文件。以下是供您参考的代码。
using System;
using System.IO;
using System.IO.Compression;
using System.Text;
using System.Web;
using System.Web.Mvc;
namespace SomeNameSpace
{
public class ScriptCombiner
{
private readonly static TimeSpan CACHE_DURATION = TimeSpan.FromDays(30);
private System.Web.HttpContextBase context;
private string _ContentType;
private string _key;
private string _version;
public ScriptCombiner(string keyname, string version, string type)
{
this._ContentType = type;
this._key = keyname;
this._version = version;
}
public void ProcessRequest(System.Web.HttpContextBase context)
{
this.context = context;
HttpRequestBase request = context.Request;
// Read setName, version from query string
string setName = _key;
string version = _version;
string contentType = _ContentType;
// Decide if browser supports compressed response
bool isCompressed = this.CanGZip(context.Request);
// If the set has already been cached, write the response directly from
// cache. Otherwise generate the response and cache it
if (!this.WriteFromCache(setName, version, isCompressed, contentType))
{
using (MemoryStream memoryStream = new MemoryStream(8092))
{
// Decide regular stream or gzip stream based on
// whether the response can be compressed or not
//using (Stream writer = isCompressed ? (Stream)(new GZipStream
// (memoryStream, CompressionMode.Compress)) : memoryStream)
using (Stream writer = isCompressed ?
(Stream)(new GZipStream(memoryStream, CompressionMode.Compress)) :
memoryStream)
{
// Read the files into one big string
StringBuilder allScripts = new StringBuilder();
foreach (string fileName in GetScriptFileNames(setName))
allScripts.Append(File.ReadAllText(context.Server.MapPath(fileName)));
// Minify the combined script files and remove comments and white spaces
var minifier = new JavaScriptMinifier();
string minified = minifier.Minify(allScripts.ToString());
#if DEBUG
minified = allScripts.ToString();
#endif
byte[] bts = Encoding.UTF8.GetBytes(minified);
writer.Write(bts, 0, bts.Length);
}
// Cache the combined response so that it can be directly written
// in subsequent calls
byte[] responseBytes = memoryStream.ToArray();
context.Cache.Insert(GetCacheKey(setName, version, isCompressed),
responseBytes, null, System.Web.Caching.Cache.NoAbsoluteExpiration,
CACHE_DURATION);
// Generate the response
this.WriteBytes(responseBytes, isCompressed, contentType);
}
}
}
private bool WriteFromCache(string setName, string version,
bool isCompressed, string ContentType)
{
byte[] responseBytes = context.Cache[GetCacheKey
(setName, version, isCompressed)] as byte[];
if (responseBytes == null || responseBytes.Length == 0)
return false;
this.WriteBytes(responseBytes, isCompressed, ContentType);
return true;
}
private void WriteBytes(byte[] bytes, bool isCompressed, string ContentType)
{
HttpResponseBase response = context.Response;
response.AppendHeader("Content-Length", bytes.Length.ToString());
response.ContentType = ContentType;
if (isCompressed)
response.AppendHeader("Content-Encoding", "gzip");
else
response.AppendHeader("Content-Encoding", "utf-8");
context.Response.Cache.SetCacheability(HttpCacheability.Public);
context.Response.Cache.SetExpires(DateTime.Now.Add(CACHE_DURATION));
context.Response.Cache.SetMaxAge(CACHE_DURATION);
response.ContentEncoding = Encoding.Unicode;
response.OutputStream.Write(bytes, 0, bytes.Length);
response.Flush();
}
private bool CanGZip(HttpRequestBase request)
{
string acceptEncoding = request.Headers["Accept-Encoding"];
if (!string.IsNullOrEmpty(acceptEncoding) &&
(acceptEncoding.Contains("gzip") || acceptEncoding.Contains("deflate")))
return true;
return false;
}
private string GetCacheKey(string setName, string version, bool isCompressed)
{
return "HttpCombiner." + setName + "." + version + "." + isCompressed;
}
public bool IsReusable
{
get { return true; }
}
// private helper method that return an array of file names
// inside the text file stored in App_Data folder
private static string[] GetScriptFileNames(string setName)
{
var scripts = new System.Collections.Generic.List<string>();
string setDefinition =
System.Configuration.ConfigurationManager.AppSettings[setName] ?? "";
string[] fileNames = setDefinition.Split(new char[] { ',' },
StringSplitOptions.RemoveEmptyEntries);
foreach (string fileName in fileNames)
{
if (!String.IsNullOrEmpty(fileName))
scripts.Add(fileName);
}
return scripts.ToArray();
}
}
}
总结一下本文,我们介绍了一个名为 CacheController
的自定义控制器,以及该控制器中的一个路由,它允许我们在 ASP.NET MVC 应用程序中对 JavaScript 和 CSS 文件进行最小化/压缩/合并和缓存。我们还介绍了控制器中的一种机制,我们可以利用它来强制客户端刷新 JavaScript 和 CSS 文件。
希望这对您有所帮助。请告诉我您的想法或您可能有的改进此技术的想法。
历史
- 2009 年 3 月 8 日:初始版本