原文出处:http://www.codeproject.com
Using the FileResolver to allow virtual application paths ( ~ ) in any file
引言
如果不是很熟悉“~”在ASP.NET中的使用,你总是完全地忽略它。我们的开发环境经常是和成品的服务器是不同的。我们可以在开发的PC上使用一个虚拟路径来测试我们的应用程序,而使用一个单独的顶级网址来发布。
那么,当把image或者link加入到页面中的时候,你总是必须知道提供的路径的类型——相对、绝对等等。在ASP.NET中最好的技巧之一就是使用“~”。它实质上是HttpRuntime.AppDomainAppVirtual属性的一个快捷方式,引用的是虚拟应用程序的根,而不是web服务器的根。
“~”是快捷、简洁的,同时也十分受限制。比如,仅仅在设置那些Render之前知道怎么解析路径的控件的一些路径相关属性时,它是有用的。它只是在ASP.NET的预定义范围内有用,绝对不能使用在ASP.NET的控管外的文件中。至少目前还是这样。
我经常碰上的问题之一是:在非ASP.NET文件(如css文件)中设定一个路径。比如,我想在css文件中设定一个样式的背景(background-image)属性时,除非我使用一个相对路径,否则将是很头疼的事情。我通常的解决方法是直接增加背景属性到页面的标签里面而不是一个css文件里,我是首先承认这种做法是不令人满意的人。
两个解决办法在我的脑海中浮现。在结合它们之后,我构建了一个可靠的解决方案。我的思路包括使用一个HTTP处理机来截取任何资源请求,诸如css、js文件等等。该处理机负责分析上述的那些资源文件并且将解析后的文件返回给客户端。
基础
本文假定你对HTTP处理机有初步的了解。
代码的使用
针对本文,我打算用css文件来作为例子,当然这个技术也可以很容易的应用到任何其他需要处理的文件类型上。
假设当前有如下一个标签在a.aspx页面中来链接一个样式表。
<link rel="stylesheet" href="resources/stylesheet.css" />
一旦我实现了上面所述的解决方案,上面的标签将改成如下:
<link rel="stylesheet" href="~/resources/stylesheet.css.ashx" />
正如你所见到的,在文件路径上有一个小小的附加后缀“.ashx”,这是为了告诉文件解析器,需要解析css文件中任何的虚拟路径。
另外一个小小的变化是我们在css文件的路径上使用了“~”。一会你将看到HTTP处理机会自动地解析这个css文件路径。然而,它似乎不适用于ASP.NET1.1的场合,因此对于ASP.NET1.1,你可能必须使用一个真实的相对或绝对路径。
那么,让我们关注一下文件解析器的代码。
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是一个继承IHttpHander接口时必须实现的方法,方法里包含了大部分处理机的处理逻辑。在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;
}
至此,我们就基本完成了文件解析器。和所有处理机一样,它需要在你的web.config中加入一个附加的条目才能工作。不仅这个是必要的,而且我们还要做一些小小的设定来扩展文件解析器,使之支持任何我们所需的文件类型。
<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和js文件。
(以下略,翻译水平有限,欢迎大家指正,如有需要请看原文)
tag: asp.net ~ 文件解析器 路径