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

用于 JavaScript 和 AJAX 的 WebService 代理生成器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.71/5 (24投票s)

2005年7月6日

8分钟阅读

viewsIcon

334243

downloadIcon

1624

从 JavaScript 调用服务器是 AJAX 应用程序的基本组成部分。如果代理对象和方法在浏览器中可用,那么使用 SOAP 和 WSDL 的 Web 服务会非常简单。

引言

从 C、.NET CLR 和 Java 等语言和编程环境中,我们很早就知道基于 IDL 和 RPC 的代理生成机制。这些生成的类和文件使程序员能够通过调用同名本地方法来调用服务器端方法。网络传输的实现从您的应用程序代码中分离出来。

如果您想通过 SOAP 从 JavaScript 实现与 Web 服务的通信,使用只需要少量代码的方法非常重要。复杂且冗长的脚本容易出错。

这个代理生成器可以独立使用,但也是我博客站点上 AJAX 框架的一部分,该框架仍在开发中。

一些 AJAX 实现使用自己的方式在客户端和服务器之间传输信息。此实现使用标准的 SOAP 协议,并可在 Internet Explorer 和 Firefox 浏览器上运行。

工作原理 - 简而言之

Web 服务可以使用用于 Web 服务的正式描述标准 WSDL(Web 服务描述语言)来描述。调用 Web 服务所需的一切信息都包含在这个 XML 格式的信息中,我们只需要将这些信息转换为 JavaScript 源代码语法,就可以通过 XSLT 转换直接执行。一个通用的包含文件用于引入 SOAP 协议的核心实现。

使用代理

为了使这些代理函数正常工作,必须包含一个通用的 JavaScript 包含文件(ajax.js)和一个生成 Web 服务特定代码的文件。

<script type="text/javascript" src="ajax.js"></script>
<script type="text/javascript" 
   src="GetJavaScriptProxy.aspx?service=CalcService.asmx">
</script>

实际通信细节的实现包含在 ajax.js 文件中。一个名为 proxies 的变量被创建为一个空的 JavaScript 对象,这是我们唯一需要的全局变量。然后将各个代理附加到该对象,以最大程度地减少可能发生的命名冲突。

第二个脚本包含现在检索 Web 服务的 WSDL 描述,并生成该服务特定的 JavaScript,其中包含可调用的本地代理方法,以执行服务器上的相应方法。

异步调用

调用服务器端方法可能看起来像这样

proxies.CalcService.CalcPrimeFactors.func = 
          displayFactors;  // hook up a method that 
                           // gets the response
proxies.CalcService.CalcPrimeFactors(12); // now call the server

// The return value is passed to this function as a parameter
function displayFactors (retVal) {
  document.getElementById("outputField").value = retVal;
} // displayFactors

在这里,您可以看到一个异步调用。函数 CalcPrimeFactors() 会立即返回,客户端脚本将继续执行。几毫秒(或更长时间)后,服务器将返回已调用 Web 服务方法的結果,并将该值作为参数传递给已连接的方法。

同步调用

还有一个可用的同步版本。在这种情况下,函数属性必须保持未设置或为 null,并且服务器端方法的結果将直接从客户端方法调用返回。这种调用服务器的方式可能会阻塞几毫秒,因为在调用期间不会处理任何用户事件,例如输入或单击。

proxies.CalcService.func = null; // no hook up function !
var f = proxies.CalcService.CalcPrimeFactors(12);
// call the server and return the result.

实现细节

这是为客户端生成的代码的示例摘录,以展示机制的工作原理。

包含文件 ajax.js 生成名为 ajax 的全局对象。

var proxies = new Object();

每个 WebService,都会有一个名为 WebService 的对象附加到 ajax 对象上,用于保存服务特定的信息,例如 WebService 的 URL 和 namespace

// JavaScript proxy for webservices
// A WebService for the calculation of prime factors.
proxies.CalcService = {
  url: "https://:1049/CalcFactors/CalcService.asmx",
  ns: "http://www.mathertel.de/CalcFactorsService/"
} // proxies.CalcService

对于每个 Web 服务方法,都会在客户端创建一个函数,该函数镜像服务器上的方法。构建完整的 SOAP 消息所需的信息作为属性附加到函数对象上。

// Add 2 numbers.
proxies.CalcService.AddInteger = 
        function () { return(proxies.callSoap(arguments)); }
proxies.CalcService.AddInteger.fname = "AddInteger";
proxies.CalcService.AddInteger.service = proxies.CalcService;
proxies.CalcService.AddInteger.action = 
        "http://www.mathertel.de/CalcFactors/AddInteger";
proxies.CalcService.AddInteger.params = 
                           ["number1:int","number2:int"];
proxies.CalcService.AddInteger.rtype = ["AddIntegerResult:int"];

缓存

代理实现还提供了客户端缓存功能。这种方法由于可以防止重复调用,因此可以减少网络流量。

使用 HTTP 头实现的 HTTP 缓存功能在这些情况下不起作用,因为请求不是 HTTP-GET 请求,并且 HTTP 主体中始终有负载。因此,缓存必须通过客户端上的某些脚本来实现。

可以通过调用方法 proxies.EnableCache 并传递应该进一步使用缓存的函数来启用 JavaScript Web 服务代理实现中的缓存功能。CalcFactorsAJAX.htm 示例中有一个按钮显示了如何启用此功能。

proxies.EnableCache(proxies.CalcService.CalcPrimeFactors)

通过调用此方法,将添加一个 JavaScript 对象,该对象存储所有结果,并用于防止调用服务器(如果参数的条目已存在于该对象中)。这不是一个完美的解决方案,但它仅在以下情况下有效:

  • 参数必须是 string 或数字,可用于索引 JavaScript 对象的属性。
  • 缓存不会自行清除。可以通过再次调用 EnableCache 来清除它。
  • 仅支持具有单个参数的方法。

参考

这是代理函数正常工作所使用的对象和属性的映射图。

属性 用法
proxies.service.url WebServices 的 URL
proxies.service.ns WebServices 的命名空间
proxies.service.function() 调用服务器端方法
proxies.service.function.fname 方法的名称
proxies.service.function.action 方法的 SOAP action,用于 HTTP 头
proxies.service.function.params 包含参数名称和类型的数组
proxies.service.function.func 用于接收结果的函数
proxies.service.function.onException 用于处理异常的函数
proxies.service.function.corefunc 调试辅助函数
proxies.service.function.service 指向服务对象的链接
proxies.EnableCache(func) 用于启用缓存功能的函数

支持的数据类型

随着 2.0 版本的发布,现在对更多不同的数据类型提供了支持。

简单数据类型

到目前为止,仅支持那些转换了参数且不需要返回值的方法。这适用于 string 和数字。

在此版本中,服务器上定义的数据类型和 WSDL 被传递到客户端,以便可以在运行时使用 JavaScript 进行数据类型转换。在生成的代理代码中,参数名称列表现在通过可选的数据类型规范进行了扩展。如果没有这些,值将被视为 string

在 HTML 对象模型中,JavaScript 数据类型支持不佳。HTML 输入字段中显示的值始终是 string,即使它只包含数字。因此,在调用代理函数时,所有参数也接受为 JavaScript string,并(如果可能)转换为正确的类型。

XML 数据

实现了 XML 文档的传递,以便能够传递复杂数据。在支持的浏览器客户端中,可以使用 Microsoft 或 Firefox 的 XMLDocument 对象,在服务器上可以使用 .NET XmlDocument 类。

C# 中的方法声明应如下所示:

[WebMethod()]
public XmlDocument Calc(XmlDocument xDoc) {
 ...
 return (xDoc);
} // Calc

代理函数还接受 XML 文档作为 string 类型。在这种情况下,传递的 string 的内容将直接传递到服务器,因此它必须包含一个有效的 XML 文档,不带声明和任何“XML 处理指令”,例如 <? ... ?>

使用此数据类型,可以将会将复杂数据直接传递到服务器,而无需定义具有多个参数的方法。如果数据方案使用新字段进行扩展,则无需为 Web 服务提供新的签名。

这种方法的缺点是,Web 服务基础结构无法验证 XML 文档的内容,因为没有该对话部分的模式。

数据类型映射

XML 数据类型 代理属性中的别名 JavaScript 数据类型
字符串 string / null 字符串
int, unsignedInt,
short, unsignedShort,
unsignedLong, slong
int Number (parseInt)
double, float float Number (parseFloat)
dateTime date 日期
布尔值 bool 布尔值
System.Xml.XmlDocument x 在 Mozilla / Firefox 中
XMLDocument
在 Internet Explorer 中
ActiveXObject("Microsoft.XMLDOM")
ActiveXObject("MSXML2.DOMDocument")

调用的实现

SOAP/XML 消息的传输可以使用当前许多最先进浏览器中提供的适当 XMLHTTP 对象来实现。此实现(截至目前)已在 Internet Explorer 和 Firefox 中进行过测试。

///<summary>
///Get a browser specific implementation of the 
///XMLHTTP object.
///</summary>
function getXMLHTTP() {
  var obj = null;

  try {
    obj = new ActiveXObject("Msxml2.XMLHTTP");
  } catch (e) { }

  if (obj == null) {
    try {
      obj = new ActiveXObject("Microsoft.XMLHTTP");
    } catch (e) { }
  } // if
  
  if ((obj == null) && 
            (typeof XMLHttpRequest != "undefined"))
    obj = new XMLHttpRequest();
  return(obj);
} // getXMLHTTP

该对象根据浏览器中可用的技术以不同的技术实现。它最初由 Microsoft 在 Internet Explorer 中作为 ActiveX 控件开发,Mozilla 开发人员通过提供相同的方法和属性对其进行了重新实现。可以通过使用以下方法序列进行调用:

x.open("POST", p.service.url, true); // async call 
x.setRequestHeader("SOAPAction", p.action);
x.setRequestHeader("Content-Type", 
                   "text/xml; charset=utf-8");
// hook up a method for the result processing
x.onreadystatechange = p.corefunc; 
x.send(soap); // send a soap request

更多细节和一些内部描述可以在 ajax.js 包含文件中找到。

JavaScript 代理生成器

在 ASP.NET 中实现时,检索 WSDL 描述非常简单。您导航到 Web 服务的 URL,并使用该页面上提供的链接。您还可以附加一个 WSDL 参数

代理生成器使用 HttpWebRequest 检索此 XML 文档。通过使用 XSLT 转换,现在可以轻松实现 WSDL 到 JavaScript 的编译器。

复杂的部分在于编写正确的转换。在 wsdl.xslt 文件中,您可以找到定义这些代理对象的 JavaScript 代码模板。此转换不生成另一个 XML 文档,而是生成有效的 JavaScript 代码的纯文本。

GetJavaScriptProxy.aspx 的源代码

<%@ Page Language="C#" Debug="true" %>

<%@ Import Namespace="System.IO" %>
<%@ Import Namespace="System.Net" %>
<%@ Import Namespace="System.Xml" %>
<%@ Import Namespace="System.Xml.Xsl" %>

<!-- 
/// 19.07.2005 white space removed
/// 20.07.2005 more datatypes and XML Documents
/// 04.09.2005 XslCompiledTransform
 -->
 
<script runat="server">
  private string FetchWsdl(string url) {
    Uri uri = new Uri(Request.Url, url + "?WSDL");
    HttpWebRequest req = 
                  (HttpWebRequest)WebRequest.Create(uri);
    req.Credentials = CredentialCache.DefaultCredentials;
    // running on the same server !
    // req.Proxy = WebRequest.DefaultWebProxy; 
    req.Timeout = 6 * 1000; // 6 seconds

    WebResponse res = req.GetResponse();
#if DOTNET11
    XmlDocument data = new XmlDocument();
    data.Load(res.GetResponseStream());

    XslTransform xsl = new XslTransform();
    xsl.Load(Server.MapPath("~/ajaxcore/wsdl.xslt"));

    System.IO.StringWriter sOut = 
             new System.IO.StringWriter();
    xsl.Transform(data, null, sOut, null);
#else
    XmlReader data = 
           XmlReader.Create(res.GetResponseStream());

    XslCompiledTransform xsl = new XslCompiledTransform();
    xsl.Load(Server.MapPath("~/ajaxcore/wsdl.xslt"));

    System.IO.StringWriter sOut = 
                             new System.IO.StringWriter();
    xsl.Transform(data, null, sOut);
#endif
    return (sOut.ToString());
  } // FetchWsdl
</script>

<%
  string asText = Request.QueryString["html"];

  Response.Clear();
  if (asText != null) {
    Response.ContentType = "text/html";
    Response.Write("<pre>");
  } else {
    Response.ContentType = "text/text";
  } // if

  string fileName = Request.QueryString["service"];
  if (fileName == null)
    fileName = "CalcService";

  // get good filenames only (local folder)
  if ((fileName.IndexOf('$') >= 0) || (Regex.IsMatch(fileName, 
                           @"\b(COM\d|LPT\d|CON|PRN|AUX|NUL)\b", 
                           RegexOptions.IgnoreCase)))
    throw new ApplicationException("Error in filename.");

  if (! Server.MapPath(fileName).StartsWith(
         Request.PhysicalApplicationPath, 
                    StringComparison.InvariantCultureIgnoreCase))
    throw new ApplicationException("Can show local files only.");

  string ret = FetchWsdl(fileName);
  ret = Regex.Replace(ret, @"\n *", "\n");
  ret = Regex.Replace(ret, @"\r\n *""", "\"");
  ret = Regex.Replace(ret, @"\r\n, *""", ",\"");
  ret = Regex.Replace(ret, @"\r\n\]", "]");
  ret = Regex.Replace(ret, @"\r\n; *", ";");
  Response.Write(ret);
%>

wsdl.xslt 的源代码

<?xml version="1.0" ?>
<xsl:stylesheet version='1.0' 
  xmlns:xsl='http://www.w3.org/1999/XSL/Transform'
  xmlns:msxsl="urn:schemas-microsoft-com:xslt"
  xmlns:fmt="urn:p2plusfmt-xsltformats" 

  xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
  xmlns:s="http://www.w3.org/2001/XMLSchema"
  xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" 
  xmlns:soap12="http://schemas.xmlsoap.org/wsdl/soap12/">
  <!-- 
  /// 19.07.2005 optional documentation
  /// 20.07.2005 more datatypes and XML Documents 
  /// 20.07.2005 more datatypes and XML Documents fixed
  -->
  <xsl:strip-space elements="*" />
  <xsl:output method="text" version="4.0" />
  <xsl:param name="alias">
    <xsl:value-of select="wsdl:definitions/wsdl:service/@name" />
  </xsl:param>

  <xsl:template match="/">
    // javascript proxy for webservices
    // by Matthias Hertel
    /*<xsl:value-of select="wsdl:definitions/wsdl:documentation"/>*/
     <xsl:for-each select=
        "/wsdl:definitions/wsdl:service/wsdl:port[soap:address]">
       <xsl:call-template name="soapport" />
     </xsl:for-each>
  </xsl:template>

  <xsl:template name="soapport">
     proxies.<xsl:value-of select="$alias" /> = {
     url: "<xsl:value-of select="soap:address/@location" />",
     ns: "<xsl:value-of 
      select=
        "/wsdl:definitions/wsdl:types/s:schema/@targetNamespace"/>"
     } // proxies.<xsl:value-of select="$alias" />
     <xsl:text>
     </xsl:text>

     <xsl:for-each select="/wsdl:definitions/wsdl:binding[@name = 
                      substring-after(current()/@binding, ':')]">
       <xsl:call-template name="soapbinding11" />
     </xsl:for-each>
  </xsl:template>

  <xsl:template name="soapbinding11">
     <xsl:variable name="portTypeName" 
       select="substring-after(current()/@type, ':')" />
    <xsl:for-each select="wsdl:operation">
       <xsl:variable name="inputMessageName" 
        select="substring-after(/wsdl:definitions/wsdl:portType[@name = 
                $portTypeName]/wsdl:operation[@name = 
                current()/@name]/wsdl:input/@message, ':')" />
       <xsl:variable name="outputMessageName" 
        select="substring-after(/wsdl:definitions/wsdl:portType[@name = 
                $portTypeName]/wsdl:operation[@name = current()/@name]
                /wsdl:output/@message, ':')" />

       <xsl:for-each select="/wsdl:definitions/wsdl:portType[@name = 
                               $portTypeName]/wsdl:operation[@name = 
                               current()/@name]/wsdl:documentation">
        /** <xsl:value-of select="." /> */
       </xsl:for-each>
       proxies.<xsl:value-of 
        select="$alias" />.<xsl:value-of select="@name" /> 
        = function () { return(proxies.callSoap(arguments)); }
       proxies.<xsl:value-of 
        select="$alias" />.<xsl:value-of select="@name" />.fname
        = "<xsl:value-of select="@name" />";
       proxies.<xsl:value-of 
        select="$alias" />.<xsl:value-of select="@name" />.service
        = proxies.<xsl:value-of select="$alias" />;
       proxies.<xsl:value-of 
        select="$alias" />.<xsl:value-of select="@name" />.action
        = "<xsl:value-of select="soap:operation/@soapAction" />";
       proxies.<xsl:value-of 
        select="$alias" />.<xsl:value-of select="@name" />.params
        = [<xsl:for-each select="/wsdl:definitions/wsdl:message[@name 
        = $inputMessageName]">
        <xsl:call-template name="soapMessage" />
      </xsl:for-each>];
      proxies.<xsl:value-of select="$alias" />.
         <xsl:value-of select="@name" />.rtype 
         = [<xsl:for-each 
         select="/wsdl:definitions/wsdl:message[@name = 
                                    $outputMessageName]">
         <xsl:call-template name="soapMessage" />
         </xsl:for-each>];
    </xsl:for-each>
  </xsl:template>

  <xsl:template name="soapMessage">
    <xsl:variable name="inputElementName" 
       select="substring-after(wsdl:part/@element, ':')" />
    <xsl:for-each select="/wsdl:definitions/wsdl:types/s:schema/s:element
                                   [@name=$inputElementName]//s:element">
      <xsl:choose>
        <xsl:when test="@type='s:string'">
          "<xsl:value-of select="@name" />"
        </xsl:when>
        <xsl:when test="@type='s:int' 
                  or @type='s:unsignedInt' or @type='s:short' 
                  or @type='s:unsignedShort' or @type='s:unsignedLong' 
                  or @type='s:long'">
          "<xsl:value-of select="@name" />:int"
        </xsl:when>
        <xsl:when test="@type='s:double' or @type='s:float'">
          "<xsl:value-of select="@name" />:float"
        </xsl:when>
        <xsl:when test="@type='s:dateTime'">
          "<xsl:value-of select="@name" />:date"
        </xsl:when>
        <xsl:when test="./s:complexType/s:sequence/s:any">
          "<xsl:value-of select="@name" />:x"
        </xsl:when>
        <xsl:otherwise>
          "<xsl:value-of select="@name" />"
        </xsl:otherwise>
      </xsl:choose>
      <xsl:if test="position()!=last()">,</xsl:if>
    </xsl:for-each>
  </xsl:template>
</xsl:stylesheet>

历史

  • 2005 年 7 月 6 日:首次发布
    • 无需转换的简单数据类型
  • 2005 年 9 月 7 日:第二次发布
    • 带转换的简单数据
    • XML 数据类型支持
    • 添加了缓存功能
  • 2005 年 12 月 16 日:修复了断开的链接

实现文件和更多示例可在我的 AJAX 项目的 示例网站 中找到。

许可证

本文未附加明确的许可证,但可能在文章文本或下载文件本身中包含使用条款。如有疑问,请通过下面的讨论区联系作者。

作者可能使用的许可证列表可以在此处找到。

© . All rights reserved.