使用 FileResolver 在任何文件中允许使用虚拟应用程序路径(~)






4.66/5 (16投票s)
2006 年 1 月 10 日
5分钟阅读

132255

645
介绍了一种在非 ASP.NET 文件中使用虚拟应用路径的解决方案。
引言
如果您不熟悉 ASP.NET 中的波浪号用法,那么您就错过了很多。通常,我们的开发环境与生产服务器完全不同。我们可能会在开发机上使用虚拟目录测试我们的应用程序,但可能会将其发布到专用的根站点。
因此,在添加图像或链接时,您必须始终注意您提供的路径类型 - 相对路径、绝对路径等。嗯,ASP.NET 最好的小技巧之一就是波浪号(~)。这本质上是 HttpRuntime.AppDomainAppVirtualPath
属性的快捷方式,它指的是虚拟应用程序根目录,而不是 Web 服务器的根目录。
虽然波浪号又快又整洁,但它也有很大的局限性。首先,它仅在设置知道如何在渲染前解析路径的某些控件的路径时才有用。因此,它不仅在 ASP.NET 的预定义区域有用,而且在任何不由 ASP.NET 进程管理的文件的外部绝对无法使用。直到现在。
我经常遇到的一个问题是在为非 ASP.NET 文件(如 CSS 文件)中的某些内容设置路径时。例如,我想为 CSS 文件中的样式设置 background-image 属性,但除非我使用相对路径,否则我就完蛋了。我通常会直接将 background-image 属性添加到标签中,而不是将其放入 CSS 文件。我第一个承认这是一个蹩脚的解决方法。
我想到了一些解决这个问题的方法。经过一些协作,一个可靠的解决方案已经建立。该想法包括使用 HTTP Handler 来拦截对 CSS 或 JavaScript 文件等资源的任何请求。然后,Handler 负责解析该文件并将“已解析”的文件发送给客户端。
背景
本文假定您对 HTTP Handler 有基本了解。
使用代码
为了本文的目的,我将使用 CSS 文件作为示例,尽管此技术可以轻松应用于任何需要解析其内容的文件类型。
目前,您可能会使用以下标签将样式表链接到 .aspx 页面。
<link rel="stylesheet" href="resources/stylesheet.css" />
在我实现了下面的解决方案后,相同的 link
标签将如下所示:
<link rel="stylesheet" href="~/resources/stylesheet.css.ashx" />
正如您所见,在文件路径上附加 ".ashx" 有细微的改变。这告诉 FileResolver 解析此 CSS 文件中的任何虚拟应用路径。
另一个小改动是,我们现在可以在 CSS 文件的路径中使用波浪号。正如您稍后将看到的,HTTP Handler 将自动为 CSS 文件解析此路径。然而,在 ASP.NET 1.1 中似乎并非如此。因此,对于 ASP.NET 1.1,您可能必须使用实际的相对或绝对路径。
那么,让我们看看 FileResolver Handler 的内部。
namespace FileResolverDemoWeb
{
public class FileResolver : IHttpHandler
{
/// <summary>
/// File cache item used to store file
/// content & date entered into cache
/// </summary>
internal class FileCacheItem
{
internal string Content;
internal DateTime DateEntered = DateTime.Now;
internal FileCacheItem(string content)
{
this.Content = content;
}
}
private FileCacheItem UpdateFileCache(HttpContext context,
string filePath)
{
string content;
using(FileStream fs = new FileStream(filePath,
FileMode.Open, FileAccess.Read))
{
using(StreamReader sr = new StreamReader(fs))
{
content = sr.ReadToEnd();
sr.Close();
}
fs.Close();
}
//Get absolute application path
string relAppPath = HttpRuntime.AppDomainAppVirtualPath;
if(!relAppPath.EndsWith("/"))
relAppPath += "/";
//Replace virtual paths w/ absolute path
content = content.Replace("~/", relAppPath);
FileCacheItem ci = new FileCacheItem(content);
//Store the FileCacheItem in cache
//w/ a dependency on the file changing
CacheDependency cd = new CacheDependency(filePath);
context.Cache.Insert(filePath, ci, cd);
return ci;
}
public void ProcessRequest(HttpContext context)
{
string absFilePath =
context.Request.PhysicalPath.Replace(".ashx", "");
//If a tilde was used in the page
//to this file, replace it w/ the app path
if(absFilePath.IndexOf("~\\") > -1)
absFilePath = absFilePath.Replace("~",
"").Replace("\\\\", "\\");
if(!File.Exists(absFilePath))
{
context.Response.StatusCode = 404;
return;
}
FileCacheItem ci = (FileCacheItem)context.Cache[absFilePath];
if(ci != null)
{
if(context.Request.Headers["If-Modified-Since"] != null)
{
try
{
DateTime date = DateTime.Parse(
context.Request.Headers["If-Modified-Since"]);
if(ci.DateEntered.ToString() == date.ToString())
{
//Don't do anything, nothing
//has changed since last request
context.Response.StatusCode = 304;
context.Response.StatusDescription =
"Not Modified";
context.Response.End();
return;
}
}
catch(Exception){}
}
else
{
//In the event that the browser doesn't
//automatically have this header, add it
context.Response.AddHeader("If-Modified-Since",
ci.DateEntered.ToString());
}
}
else
{
//Cache item not found, update cache
ci = UpdateFileCache(context, absFilePath);
}
context.Response.Cache.SetLastModified(ci.DateEntered);
context.Response.ContentType = "text/" +
GetContentType(Path.GetExtension(absFilePath));
context.Response.Write(ci.Content);
context.Response.End();
}
/// <summary>
/// Gets the appropriate content type for a specified extension
/// </summary>
private string GetContentType(string ext)
{
switch(ext.ToLower())
{
case ".css":
return "css";
break;
case ".xml":
return "xml";
break;
case ".js":
return "javascript";
break;
default:
return "plain";
break;
}
}
#region IHttpHandler Members
public bool IsReusable
{
get
{
return true;
}
}
#endregion
}
}
现在,让我们看一下前面代码的每个部分。
CacheItem
是一个内部类,用于保存 CSS 文件已解析的内容。它还有一个 DateEntered
属性,与内容相关联,该属性设置为内容的最后更新日期和时间。这对于确定我们是否需要向客户端提供新、最新的 CSS 文件内容版本非常有用。
ProcessRequest
是任何实现 IHttpHandler
的类都必须实现的方法。它还包含此 Handler 的大部分逻辑。
在 ProcessRequest
方法中,我们可以通过 HttpContext.Request.PhysicalPath
属性知道正在处理哪个文件。我们将进行初始检查,以确保文件的路径已被解析。一旦我们获得了文件的物理映射路径,我们就会进行初步检查,以确保文件仍然存在于文件系统中。
string absFilePath = context.Request.PhysicalPath.Replace(".ashx", "");
//If a tilde was used in the page to this file, replace it w/ the app path
if(absFilePath.IndexOf("~\\") > -1)
absFilePath = absFilePath.Replace("~", "").Replace("\\\\", "\\");
if(!File.Exists(absFilePath))
{
context.Response.StatusCode = 404;
return;
}
验证后,我们需要检查页面的缓存,看是否已添加相关的 CacheItem
。一旦向此 CSS 文件发出初始请求,就会创建一个 CacheItem
并进行存储。
如果 CacheItem
存在,我们将 DateEntered
值与请求中的 If-Modified-Since
标头值进行比较。如果日期匹配,那么我们就知道客户端已缓存了该文件的最新版本。如果日期不匹配或找不到合适的标头,我们将尝试添加标头并向客户端写入新内容。
FileCacheItem ci = (FileCacheItem)context.Cache[absFilePath];
if(ci != null)
{
if(context.Request.Headers["If-Modified-Since"] != null)
{
try
{
DateTime date = DateTime.Parse(
context.Request.Headers["If-Modified-Since"]);
if(ci.DateEntered.ToString() == date.ToString())
{
//Don't do anything, nothing has
//changed since last request
context.Response.StatusCode = 304;
context.Response.StatusDescription = "Not Modified";
context.Response.End();
return;
}
}
catch(Exception){}
}
else
{
//In the event that the browser doesn't
//automatically have this header, add it
context.Response.AddHeader("If-Modified-Since",
ci.DateEntered.ToString());
}
}
else
{
//Cache item not found, update cache
ci = UpdateFileCache(context, absFilePath);
}
context.Response.Cache.SetLastModified(ci.DateEntered);
context.Response.ContentType = "text/" +
GetContentType(Path.GetExtension(absFilePath));
context.Response.Write(ci.Content);
context.Response.End();
如果找不到 CacheItem
,我们需要使用新的 CacheItem
更新缓存。这包括从 CSS 文件读取内容,并用真实的应用程序路径替换波浪号快捷方式的任何实例。然后,我们将新内容打包到 CacheItem
中并将其存储在页面缓存中。
private FileCacheItem UpdateFileCache(HttpContext context,
string filePath)
{
string content;
using(FileStream fs = new FileStream(filePath,
FileMode.Open, FileAccess.Read))
{
using(StreamReader sr = new StreamReader(fs))
{
content = sr.ReadToEnd();
sr.Close();
}
fs.Close();
}
//Get absolute application path
string relAppPath = HttpRuntime.AppDomainAppVirtualPath;
if(!relAppPath.EndsWith("/"))
relAppPath += "/";
//Replace virtual paths w/ absolute path
content = content.Replace("~/", relAppPath);
FileCacheItem ci = new FileCacheItem(content);
//Store the FileCacheItem in cache
//w/ a dependency on the file changing
CacheDependency cd = new CacheDependency(filePath);
context.Cache.Insert(filePath, ci, cd);
return ci;
}
这就是它的全部内容。像所有 Handler 一样,除非您在 web.config 中添加一些额外的条目,否则它们将无法正常工作。这不仅是必要的,而且是我们进行一些配置以扩展 FileResolver 以支持我们想要的任何文件类型的关键。
<configuration>
<system.web>
<httpHandlers>
<add verb="GET" path="*.css.ashx"
type="FileResolverDemoWeb.FileResolver,FileResolverDemoWeb" />
<add verb="GET" path="*.js.ashx"
type="FileResolverDemoWeb.FileResolver,FileResolverDemoWeb" />
</httpHandlers>
</system.web>
</configuration>
在此场景中,我选择了解析 CSS 和 JavaScript 文件的内容。很棒。
致谢
感谢 Jon Gilkison 为此解决方案提供的协作努力。