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

xLibrary - 使用自定义 IHttpHandler 访问嵌入式 JavaScript

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.33/5 (4投票s)

2007年7月4日

CPOL

15分钟阅读

viewsIcon

45584

downloadIcon

485

本文介绍了如何创建一个自定义 IHttpHandler 来使用嵌入在程序集中的 www.cross-browser.com 的 X Library。

目录

引言

本文介绍了如何创建一个自定义的 IHttpHandler 来使用嵌入在程序集中的 www.cross-browser.com 的 X Library。

使用的关键技术

  • 将资源嵌入到程序集中。
  • 使用 IHttpHandler 对请求进行自定义处理。

特点

X 库的所有 JavaScript 和 XML 文档都将嵌入到程序集中。

自定义的 IHttpHandler 将返回使用所需 X Library 函数所需的所有必要方法。

程序集易于更新。只要 XML 文档文件的格式不变,要更新到新版本的 X Library,您只需在项目中更新 .js/.xml 文件并重新编译即可。

问题

来自 www.cross-browser.com 的 X Library 是一个非常有用的工具。它对许多 JavaScript 函数进行了标准化,以便在不同浏览器中使用。X Library 最棒的地方在于它是模块化的。每个函数都有自己的 .js 文件,因此您不必向浏览器发送不必要的内容。这对于最大限度地减少流量非常重要。为了避免我们必须在单独的 <script> 块中引用每个脚本,它有一个名为 XC 的有用工具,可以将您使用的所有脚本编译成一个 .js 文件。这是一个很棒的工具,但我很懒,不想每次更新页面时都运行它。

解决方案

我决定为此库创建一个 ASP.NET 包装器,该包装器将仅提供我需要的脚本,而无需使用上述 XC 工具进行编译,也无需为每个函数使用单独的 <script> 块。所有脚本和文档都将嵌入到这个新的 xLibrary 程序集中。它将有一个实现 IHttpHandler 接口的类,并且将仅为每个请求提供所需的脚本。

代码

概述

xLibrary 程序集包含三个类以及嵌入的 JavaScript 和 XML。这些类如下所示:

  • xFunction
  • 一个简单的对象,用于保存函数的名称、源和依赖项。

  • xLibraryHttpHandler
  • IHttpHandler 接口的实现,它将处理 X 函数的请求。

  • xLibraryLoader
  • 一个辅助对象,它将嵌入的 JavaScript 加载到内存中,以加快 X 函数的请求速度。

将 JavaScript 和 XML 嵌入到程序集中

首先,您需要将从 www.cross-browser.com 下载的 .zip 文件中 x\lib 文件夹下的所有 .xml.js 文件添加到项目中的 lib 文件夹。选择所有这些文件,然后在“属性”窗口中,将 “生成操作” 属性更改为 “嵌入的资源”。当 Visual Studio 构建程序集时,此设置将导致编译器将所有这些文件嵌入到程序集中。每个嵌入的资源将命名如下:

[assemblyName].[relative-path-to-file].[filename].[extn]

在这种情况下,程序集名称是 xLibrary,路径是 lib。因此,lib 文件夹中的 xaddclass.js 文件将被命名为嵌入式名称 xLibrary.lib.xaddclass.js。如果文件位于单独的文件夹中,例如“lib\xml”和“lib\js”,则 xaddclass.js 将被命名为 xLibrary.lib.js.xaddclass.js,而 xaddclass.xml 将被命名为 xLibrary.lib.xml.xaddclass.xml

实现

一旦它们被嵌入到程序集中,我们就需要能够从代码中访问它们。这就是 xLibraryLoader 类发挥作用的地方。我将首先介绍 GetResourceStream 方法。

private Stream GetResourceStream( string resourceName )
{
   return Assembly.GetExecutingAssembly().GetManifestResourceStream( resourceName );
}

这是一个非常简单的方法,用于将嵌入的资源检索为 Stream。传入一个资源名称,如 xLibrary.lib.xaddclass.js,它将返回该文件的流。

下一个方法是 GetResource

private string GetResource( string resourceName )
{
   return new StreamReader( GetResourceStream( resourceName ) ).ReadToEnd();
}

此方法与 GetResourceStream 类似,但它不是返回 Stream,而是将整个 Stream 转换为 string 返回给调用者。

为了节省请求时的处理时间(注意:这会以内存使用量为代价),当我们初始化 xLibraryLoader 类时,它会加载一个 Hashtable,其中包含它在程序集中找到的所有函数。然后,可以使用 Hashtable 来返回所需的脚本,而不是依赖于反射。为了实现这一点,我们使用 FillHashtable

此方法首先创建 Hashtable 并获取当前程序集的引用。如果需要,我们可以将资源嵌入到另一个程序集中,并修改此方法和 GetResource 方法。一旦它获取了对必要程序集的引用,它就会获取该程序集中所有嵌入资源的列表。然后,我们开始循环遍历所有找到的资源。

private void FillHashtable ()
{
   xLibraryData = new Hashtable();

   Assembly      asm         = Assembly.GetExecutingAssembly();
   string[]      resources   = asm.GetManifestResourceNames();

   foreach ( string resource in resources )
   {

我们首先要做的是确保我们处理的是 XML 文件而不是 JavaScript 文件。我们只需要处理这两种文件,因为这就是我们程序集中的所有内容。如果我们有除此处使用的文件之外的 XML 文件,则需要额外的检查来验证我们是否拥有 X Library XML 文档文件。然后,我们设置将保存有关 XML 文档文件指定的函数信息的变量。

if ( String.Compare( Path.GetExtension( resource ), ".xml", true ) == 0 )
{
    string    id            = string.Empty;
    string    source        = string.Empty;
    ArrayList dependencyIds = new ArrayList();

现在我们知道我们有一个 XML 文档文件,我们需要解析它以获取所需的信息。首先,我们创建一个 System.Xml.XmlDocument 实例,并将其加载到嵌入文件的 Stream 中。接下来,使用 XPath,我们选择根元素 x_symbol (/x_symbol) 的 ID 属性 (/@id)。我们将此属性的值赋给之前设置的 id 变量。

接下来,我们选择 /x_symbol/sources/src/file 元素。每个 XML 文件应该只有一个这样的元素,但如果存在多个,它将只选择第一个实例。因为它是一个元素,所以它没有 Value 属性,但有一个 InnerXml 属性。我们使用 string.Format 将文件名格式化为 GetResourceGetResourceStream 可以用来查找我们的 JavaScript 文件的格式。

最后,我们获取此 X 函数的所有依赖项列表。SelectNodes 方法返回一个 XmlNodeList,其中包含所有匹配 XPath 表达式 /x_symbol/dependencies/dep 的节点。我们遍历其中的每一个,并将此函数所依赖的 X 函数的名称添加到 ArrayList 中。

XmlDocument doc = new XmlDocument();
doc.Load( GetResourceStream( resource ) );

XmlNode x_symbolId = doc.SelectSingleNode( "/x_symbol/@id" );
id                 = x_symbolId.Value;
x_symbolId         = null;

XmlNode srcFile    = doc.SelectSingleNode( "/x_symbol/sources/src/file" );
source             = GetResource(
                         string.Format( "xLibrary.lib.{0}", srcFile.InnerXml )
                     );
srcFile            = null;

XmlNodeList deps = doc.SelectNodes( "/x_symbol/dependencies/dep" );
foreach( XmlNode dep in deps )
{
    dependencyIds.Add( dep.InnerXml );
}
deps = null;

现在,我们将收集到的 idsourcedependencyIds 放入一个简单的对象来保存它们。如果您从未用过 ArrayList 类的 ToArray 方法,那么它非常方便,但它是一对一的复制。请记住,如果您处理的是非常大的数组(这里不是这种情况),则复制可能需要一些时间。

然后,我们将创建的 xFunction 添加到 Hashtable 中,并按其名称进行索引。最后,完成循环并处理下一个。

         xFunction function =
            new xFunction( id, source, dependencyIds.ToArray( typeof(string) ) as string[] );

         xLibraryData.Add( function.Name.ToLower(), function );
      }
   }
}

IHttpHandler

我们还有两个方法,但在介绍它们之前,我想谈谈 IHttpHandler 接口和 xLibraryHttpHandler 类。

当我第一次开始接触 IHttpHandler 时,我对它的工作方式和作用感到有些困惑。所以,为了开始学习,让我们举个例子。IHttpHandler 最常见的实现是 System.Web.UI.Page 类。IHttpHandler 的实现只有一个方法(ProcessRequest)和一个属性(IsReusable)。IsReusable 属性不言自明。如果返回 true,则 ASP.NET 进程将为每个请求重用相同的实例。如果 IsReusable 返回 false,则为每个请求创建一个新实例。ProcessRequest 方法接受一个参数:HttpContext 的实例。HttpContext 对象包含对 HttpRequest 对象(其中包含查询字符串等)和 HttpResponse 对象(用于将信息从服务器流式传输到客户端)的引用。当用户请求 ASPX 页面时,ASP.NET 进程通过调用其 ProcessRequest 方法,将请求的责任移交给 System.Web.UI.Page 类的实例。然后,Page 确定请求地址实际对应的页面。然后解析该页面,并将所有内容返回到 Response 对象的流中。(注意:这是实际发生情况的简化版本。有关更多信息,请参阅 .NET Framework 文档中的 ASP.NET 请求处理,或者要了解 Microsoft 的实现方式,请使用 Lutz Roeder 的 Reflector 来查看 System.Web 程序集。)

我们将做一些与 Page 对象类似但更简单的事情。因为 xLibraryHttpHandler 实现 IHttpHandler,所以它具有 IsReusable 属性和 ProcessRequest 方法。

private xLibraryLoader loader;

public xLibraryHttpHandler()
{
   loader = new xLibraryLoader();
}

我们在构造函数中所做的第一件事是创建一个 xLibraryLoader 类的新实例。请注意,xLibraryLoader 构造函数调用上面介绍的 FillHashtable 方法。

public bool IsReusable
{
   get
   {
      return true;
   }
}

由于 xLibraryLoader 构造函数调用 FillHashtable 方法,因此我们不希望它在每次请求时都进行解析,否则繁忙的服务器会很快崩溃。这就是为什么我们将 IsReusable 设置为返回 true。这样,ASP.NET 工作进程将在每次请求时重用 xLibraryHttpHandler 的同一实例。(注意:我没有对哪种方式更快进行基准测试,而是凭直觉进行的。在投入生产使用之前,我建议实际测试这两种方法,并决定哪种方法更适合您的使用。)

public void ProcessRequest ( HttpContext context )
{
   HttpResponse Response = context.Response;
   HttpRequest  Request  = context.Request;

   Response.ContentType  = "application/x-javascript";

   string[] functions    = Request.QueryString["functions"].Split( ',' );
   string   script       = loader.GetScript( functions, true );

   Response.Write( "// Copyright (C) 2007 Brodrick Bassham..." );
   Response.Write( script );
}

xLibraryHttpHandler 的最后一部分是 ProcessRequest 方法。它首先创建 ResponseRequest 对象的别名,以便更容易使用(Page 对象也这样做)。然后,我们将 ContentType 标头设置为 “application/x-javascript”。关于是使用 “application/x-javascript” 还是 “text/javascript” 或其他值,存在很多争论。标准是 “application/x-javascript”,而 Internet Explorer 不支持 “application/x-javascript”,无论您将其设置为何值,它都会忽略它。下一行应该会给您一个关于我们如何使用它的提示。它获取 QueryString 并查找其中的 “functions” 部分(例如:https:///xLibrary.xlib?functions=func1,func2 将返回 “func1,func2”),然后以逗号为分隔符将其拆分为一个数组。我们的 xLibraryLoader 实例的 GetScript 方法正是这样做;它会构建使用给定数组中列出的函数所需的脚本。我将在下面讨论该方法。ProcessRequest 方法最后要做的就是将脚本发送到客户端的 Response 流。

public string GetScript ( string[] functions, bool alertOnError )
{
   ArrayList     added  = new ArrayList();
   StringBuilder output = new StringBuilder();

这是 xLibraryLoader 中的 GetScript 方法。它接受两个参数:一个名为 functions 的字符串数组和一个名为 alertOnError 的布尔值。functions 参数是要输出的函数名称数组。如果我们请求了一个我们没有的函数,则使用 alertOnError 参数显示一个 JavaScript 警报框。我们创建的 ArrayList 是我们已经添加到输出中的所有函数名称的列表。

foreach( string function in functions )
{
    if ( !added.Contains( function ) )
    {

首先检查我们是否已经添加了该函数。我们不希望输出中出现重复项。

xFunction func = xLibraryData[ function.ToLower() ] as xFunction;

Hashtable 中提取请求的函数。

if ( func != null )
{
    output.Append( func.Source );
    added.Add( func.Name );
    output.Append( GetScript( func.Dependencies, alertOnError, added ) );
}

如果找到函数,则将源追加到输出,然后获取所有其依赖项的源。

else
{
    output.AppendFormat( "// X Function {0} was not found.\n", function );

    if ( alertOnError )
    {
       output.AppendFormat( "alert('X Function {0} was not found.');\n", function );
    }
}

此代码将一个注释追加到输出,以表明找不到所需的函数。如果我们应该向用户发出错误警报,则追加代码以显示带有消息的 JavaScript 警报框。

      }
   }

   return output.ToString();
}

循环完所有请求的 function 后,返回生成的脚本。

private string GetScript ( string[] functions, bool alertOnError, ArrayList added )
{
   StringBuilder output = new StringBuilder();

   foreach( string function in functions )
   {
      if ( !added.Contains( function ) )
      {
         xFunction func = xLibraryData[ function.ToLower() ] as xFunction;

         if ( func != null )
         {
            output.Append( func.Source );

            added.Add( func.Name );

            output.Append( GetScript( func.Dependencies, alertOnError, added ) );
         }
         else if ( alertOnError )
         {
            output.AppendFormat( ERROR_ALERT, function );
         }
      }
   }

   return output.ToString();
}

此版本的 GetScript 方法也在 xLibraryLoader 类中。这个方法是一个递归方法,它会不断调用自身,直到找到所有需要的依赖项。它首先由非递归的 GetScript 方法调用。

如何使用这一切

现在我们已经回顾了代码的实际作用,我们如何使用它?嗯,我们构建的程序集需要放到我们使用的 Web 应用程序的 bin 文件夹中。接下来,我们有两个选择:我们可以修改 web.config 文件,或者我们可以创建一个自定义 .ashx 文件。

选项 1 - Web.config

我们先从 web.config 文件开始。更改将应用于以下部分(如果不存在,则需要添加此部分)

<configuration>
   <system.web>
      <httpHandlers>
         <!-- Option 1 - Custom extension -->
         <add verb="GET" path="*.xlib" type="xLibrary.xLibraryHttpHandler,xLibrary" />

         <!-- Option 2 - Specific .ashx file -->
         <add verb="GET" path="xLibrary.ashx" type="xLibrary.xLibraryHttpHandler,xLibrary" />
      </httpHandlers>
   <system.web>
<configuration>

上面的第一个 web.config 选项有点复杂。因为 IIS 进程不知道 .xlib 文件,所以它不知道需要以任何方式处理它。如果我们不进行任何更改,那么任何不存在于服务器上的 .xlib 文件都会返回 404(未找到)。如果文件确实存在于服务器上,它将向浏览器提供该文件,而不会执行我们想要的处理。为了纠正这一点,我们需要告诉 IIS 进程关于 .xlib 文件类型。

首先,右键单击“我的电脑”并选择“管理”,打开 IIS 管理控制台。展开树中的“服务和应用程序”项。钻取到您的 Web 应用程序;在本例中是“默认网站”。如下图所示,右键单击 Web 应用程序名称并选择“属性”。

Computer Management Window

在此,单击“主目录”选项卡并选择“配置”。

Default Website Properties Window

接下来,您将看到“应用程序配置”对话框。在这里,您将找到处理服务器上不同文件的程序。如果此列表中未指定扩展名,它将被视为常规文件。选择“添加”按钮继续。

Application Configuration Window

最后,我们看到“添加/编辑应用程序扩展映射”对话框。浏览到“aspnet_isapi.dll”。它应该位于“C:\Windows\Microsoft.NET\Framework\v1.0.3705\”目录中(注意:将“Windows”替换为您的 Windows 安装文件夹,“v1.0.3705”替换为您使用的 .NET Framework 版本)。

Add/Edit Application Extension Mapping Window

在 Windows XP 中,“添加/编辑应用程序扩展映射”对话框可能会有点棘手。扩展名字段必须以句点开头。例如:.xlib。如果您不需要向具有此扩展名的任何文件发布数据,则可以将动词限制为 GET。如果您不想费心创建实际文件(IHttpHandler 实现将处理所有必要的输出),则取消选中“检查文件是否存在”。注意:在 Windows XP 中,有时“确定”按钮会保持灰色。如果是这种情况,请确保所有设置都符合您的要求,然后单击包含“aspnet_isapi.dll”位置的文本框。这将展开文本框的内容并启用“确定”按钮。

此过程会告诉 IIS,当请求具有此扩展名的页面时——而不是发送文件的内容并让浏览器处理——我们希望 ASP.NET 进程对其进行处理并确定如何操作。当我们将在 web.config 文件的 <HttpHandlers> 部分中添加 xLibraryHttpHandler 时,它会告诉 ASP.NET 进程使用哪个类来处理请求。

因此,现在当浏览器请求任何具有 .xlib 扩展名的文件时,服务器不会查找文件本身,而是调用 xLibrary 程序集中的 xLibrary.xLibraryHttpHandler 类的 ProcessRequest 方法。

第二个 web.config 选项告诉应用程序,对“xLibrary.ashx”的每个请求都应通过 xLibrary 程序集中的 xLibrary.xLibraryHttpHandler 类进行路由。优点是,IIS 默认知道它必须让 ASP.NET 处理 .ashx 文件。通过对 web.config 文件进行此更改,ASP.NET 进程知道,如果请求“xLibrary.ashx”,则需要调用 xLibraryHttpHandler

对于那些无法访问管理控制台的用户(例如许多共享托管环境)来说,此选项要容易得多。

选项 2 - 自定义 .ashx 文件

最后一个也是可能是最简单的选项是创建一个自定义 .ashx 文件。我之前说过,IIS 和 ASP.NET 默认都知道它们需要处理对 .ashx 文件的任何请求。如果我们创建一个名为 “xLib.ashx” 的文件并将其留空,则在请求该文件时会发生错误。这是因为 ASP.NET 知道它需要处理它,但还没有被告知如何处理。为了解决这个问题,我们在文件中添加此行:

<%@ WebHandler Class="xLibrary.xLibraryHttpHandler,xLibrary" %>

这就像 .aspx 文件中的 @Page 指令一样。它告诉 ASP.NET 进程,为了处理请求,它必须调用 xLibrary 程序集中的 xLibrary.xLibraryHttpHandler 类。

实际使用

假设您上面创建了自定义 .xlib 扩展名并使用了相应的 web.config 条目,则可以使用嵌入的 JavaScript 如下:

<script
   type="text/javascript"
   src="xLibrary.xlib?functions=[comma,sparated,list,of,needed,functions]">
</script>

示例

<script
   type="text/javascript"
   src="xLibrary.xlib?functions=xAddEventListener,xGetElementsByClassName">
</script>

关注点

.NET 框架提供了一种将所有 JavaScript 和 XML 文件嵌入程序集中的方法,以便于维护。我喜欢将所有松散文件都这样嵌入,因为它创建了更整洁的目录结构,并且允许您检查应用程序中设置的文件的自定义权限。因为一些共享托管提供商很难编辑,所以您可以使用自定义 IHttpHandler 来实现相同的目标。

通过添加自定义扩展名和 IHttpHandler 实现,您可以创建自己的处理服务器文件的方式。这包括自定义解析器。

演示网站使用 Visual Web Developer Express 构建,源解决方案使用 Visual C# Express 构建,两者都可从 Microsoft 免费获取。

其他资源

历史

  • 2007 年 7 月 4 日
  • 初始版本。

  • 2007 年 7 月 14 日
  • 更新至 X 4.17。

© . All rights reserved.