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

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.66/5 (16投票s)

2006 年 1 月 10 日

5分钟阅读

viewsIcon

132255

downloadIcon

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 为此解决方案提供的协作努力。

© . All rights reserved.