JavaScript SOAP 客户端






4.77/5 (41投票s)
2006年1月24日
4分钟阅读

1099299

17459
使用 AJAX 调用 Web 服务。
引言
关于 AJAX 的讨论随处可见;AJAX 是“Asynchronous JavaScript and XML”的缩写,这是一项基于 XMLHttpRequest
的技术,目前所有主流浏览器都支持它。基本思想很简单——实际上并不是什么突破——但它允许在服务器请求后更新页面,而无需重新加载所有数据。您可以在 GMail 或 Google Suggest 上找到一些示例。有关 AJAX 的更多信息,您可以查看 Wikipedia。
在本文中,我们提出了一种基于 AJAX 的解决方案,与互联网上常见的解决方案相比,它具有一个很大的优点:调用的是 Web 服务。
这允许
- 在服务器端,我们只需公开一个包含所需方法的 Web 服务(而不是生成包含基于自定义语法或通用 XML 的数据的动态页面)。
- 在客户端,我们使用 WSDL(Web Service Description Language)自动生成一个 JavaScript 代理类,以便使用 Web 服务的返回类型——这与 Visual Studio 添加 Web 引用到解决方案时所做的事情类似。
下图显示了异步调用的 SOAP Client 工作流程
客户端使用 JavaScript 函数调用“SOAPClient.invoke
”方法,并指定以下内容:
- Web 服务 URL(请注意,出于安全原因,许多浏览器不允许跨域调用)。
- Web 方法名称。
- Web 方法参数值。
- 调用模式(
async
=true
,sync
=false
)。 - 响应接收后调用的回调方法(同步调用时可选)。
“SOAPClient.invoke
”方法执行以下操作(数字对应于上一图):
- 它获取 WSDL 并缓存描述以供将来的请求使用。
- 它准备并向服务器发送一个 SOAP(v. 1.1)请求(调用方法和参数值)。
- 它使用 WSDL 处理服务器的回复,以便构建要返回的相应 JavaScript 对象。
- 如果调用模式是
async
,则调用回调方法,否则它返回相应的对象。
使用代码
在公开了我们通过 JavaScript 使用 Web 服务的想法之后,我们只需要分析代码。
让我们从用于定义要传递给 Web 方法的参数的类开始:“SOAPClientParameters
”
function SOAPClientParameters()
{
var _pl = new Array();
this.add = function(name, value)
{
_pl[name] = value;
return this;
}
this.toXml = function()
{
var xml = "";
for(var p in _pl)
{
if(typeof(_pl[p]) != "function")
xml += "<" + p + ">" +
_pl[p].toString().replace(/&/g,
"&").replace(/</g,
"<").replace(/>/g,
">
代码仅包含一个内部字典(关联数组),其中包含参数名称(键)和相关值;“add
”方法允许添加新参数,而“toXml
”方法提供 SOAP 请求的 XML 序列化(请参阅“SOAPClient._sendSoapRequest
”)。
让我们定义“SOAPClient
”类,该类只能包含静态方法以允许异步调用,以及该类中唯一的“公共”方法:“SOAPClient.invoke
”。
注意:由于 JavaScript 没有预留访问修饰符——例如“public”、“private”、“protected”等——我们将使用“_”前缀来表示私有方法。
function SOAPClient() {}
SOAPClient.invoke = function(url, method,
parameters, async, callback)
{
if(async)
SOAPClient._loadWsdl(url, method,
parameters, async, callback);
else
return SOAPClient._loadWsdl(url, method,
parameters, async, callback);
}
“SOAPClient.invoke
”方法的接口在上面进行了描述;我们的实现检查调用是异步的(调用结果将传递给回调方法)还是同步的(调用结果将直接返回)。对 Web 服务的调用通过调用“SOAPClient._loadWsdl
”方法开始。
SOAPClient._loadWsdl = function(url, method, parameters, async, callback)
{
// load from cache?
var wsdl = SOAPClient_cacheWsdl[url];
if(wsdl + "" != "" && wsdl + "" != "undefined")
return SOAPClient._sendSoapRequest(url, method,
parameters, async, callback, wsdl);
// get wsdl
var xmlHttp = SOAPClient._getXmlHttp();
xmlHttp.open("GET", url + "?wsdl", async);
if(async)
{
xmlHttp.onreadystatechange = function()
{
if(xmlHttp.readyState == 4)
SOAPClient._onLoadWsdl(url, method,
parameters, async, callback, xmlHttp);
}
}
xmlHttp.send(null);
if (!async)
return SOAPClient._onLoadWsdl(url, method, parameters,
async, callback, xmlHttp);
}
该方法在缓存中搜索相同的 WSDL,以避免重复调用。
SOAPClient_cacheWsdl = new Array();
如果 WSDL 未在缓存中找到(这是当前上下文中的第一次调用),则使用 XMLHttpRequest
根据所需的模式(同步或不同步)从服务器请求它。一旦从服务器获得响应,“SOAPClient._onLoadWsdl
”方法将被调用。
SOAPClient._onLoadWsdl = function(url, method,
parameters, async, callback, req)
{
var wsdl = req.responseXML;
SOAPClient_cacheWsdl[url] = wsdl;
return SOAPClient._sendSoapRequest(url, method,
parameters, async, callback, wsdl);
}
WSDL 的副本存储在缓存中,然后执行“SOAPClient._sendSoapRequest
”方法。
SOAPClient._sendSoapRequest = function(url, method,
parameters, async, callback, wsdl)
{
var ns = (wsdl.documentElement.attributes["targetNamespace"] +
"" == "undefined") ?
wsdl.documentElement.attributes.getNamedItem(
"targetNamespace").nodeValue :
wsdl.documentElement.attributes["targetNamespace"].value;
var sr =
"<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
"<soap:Envelope " +
"xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" " +
"xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" " +
"xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\">" +
"<soap:Body>" +
"<" + method + " xmlns=\"" + ns + "\">" +
parameters.toXml() +
"</" + method + "></soap:Body></soap:Envelope>";
var xmlHttp = SOAPClient._getXmlHttp();
xmlHttp.open("POST", url, async);
var soapaction =
((ns.lastIndexOf("/") != ns.length - 1) ? ns + "/" : ns) + method;
xmlHttp.setRequestHeader("SOAPAction", soapaction);
xmlHttp.setRequestHeader("Content-Type", "text/xml; charset=utf-8");
if(async)
{
xmlHttp.onreadystatechange = function()
{
if(xmlHttp.readyState == 4)
SOAPClient._onSendSoapRequest(method,
async, callback, wsdl, xmlHttp);
}
}
xmlHttp.send(sr);
if (!async)
return SOAPClient._onSendSoapRequest(method,
async, callback, wsdl, xmlHttp);
}
服务命名空间从 WSDL 中提取(为 Internet Explorer 和 Mozilla / FireFox 使用不同的 XPath 查询),然后创建一个 SOAP v. 1.1 请求并提交。在收到服务器响应后将调用“SOAPClient._onSendSoapRequest
”方法。
SOAPClient._onSendSoapRequest = function(method,
async, callback, wsdl, req)
{
var o = null;
var nd = SOAPClient._getElementsByTagName(
req.responseXML, method + "Result");
if(nd.length == 0)
{
if(req.responseXML.getElementsByTagName(
"faultcode").length > 0)
throw new Error(500,
req.responseXML.getElementsByTagName(
"faultstring")[0].childNodes[0].nodeValue);
}
else
o = SOAPClient._soapresult2object(nd[0], wsdl);
if(callback)
callback(o, req.responseXML);
if(!async)
return o;
}
服务器响应被处理以查找故障:如果找到故障,则会引发错误。相反,如果获得正确的结果,一个递归函数将使用服务描述生成返回类型。
SOAPClient._soapresult2object = function(node, wsdl)
{
return SOAPClient._node2object(node, wsdl);
}
SOAPClient._node2object = function(node, wsdl)
{
// null node
if(node == null)
return null;
// text node
if(node.nodeType == 3 || node.nodeType == 4)
return SOAPClient._extractValue(node, wsdl);
// leaf node
if (node.childNodes.length == 1 &&
(node.childNodes[0].nodeType == 3 ||
node.childNodes[0].nodeType == 4))
return SOAPClient._node2object(node.childNodes[0], wsdl);
var isarray = SOAPClient._getTypeFromWsdl(node.nodeName,
wsdl).toLowerCase().indexOf("arrayof") != -1;
// object node
if(!isarray)
{
var obj = null;
if(node.hasChildNodes())
obj = new Object();
for(var i = 0; i < node.childNodes.length; i++)
{
var p = SOAPClient._node2object(node.childNodes[i], wsdl);
obj[node.childNodes[i].nodeName] = p;
}
return obj;
}
// list node
else
{
// create node ref
var l = new Array();
for(var i = 0; i < node.childNodes.length; i++)
l[l.length] =
SOAPClient._node2object(node.childNodes[i], wsdl);
return l;
}
return null;
}
SOAPClient._extractValue = function(node, wsdl)
{
var value = node.nodeValue;
switch(SOAPClient._getTypeFromWsdl(
node.parentNode.nodeName, wsdl).toLowerCase())
{
default:
case "s:string":
return (value != null) ? value + "" : "";
case "s:boolean":
return value+"" == "true";
case "s:int":
case "s:long":
return (value != null) ? parseInt(value + "", 10) : 0;
case "s:double":
return (value != null) ? parseFloat(value + "") : 0;
case "s:datetime":
if(value == null)
return null;
else
{
value = value + "";
value = value.substring(0, value.lastIndexOf("."));
value = value.replace(/T/gi," ");
value = value.replace(/-/gi,"/");
var d = new Date();
d.setTime(Date.parse(value));
return d;
}
}
}
SOAPClient._getTypeFromWsdl = function(elementname, wsdl)
{
var ell = wsdl.getElementsByTagName("s:element"); // IE
if(ell.length == 0)
ell = wsdl.getElementsByTagName("element"); // MOZ
for(var i = 0; i < ell.length; i++)
{
if(ell[i].attributes["name"] + "" == "undefined") // IE
{
if(ell[i].attributes.getNamedItem("name") != null &&
ell[i].attributes.getNamedItem("name").nodeValue ==
elementname && ell[i].attributes.getNamedItem("type") != null)
return ell[i].attributes.getNamedItem("type").nodeValue;
}
else // MOZ
{
if(ell[i].attributes["name"] != null &&
ell[i].attributes["name"].value ==
elementname && ell[i].attributes["type"] != null)
return ell[i].attributes["type"].value;
}
}
return "";
}
“SOAPClient._getElementsByTagName
”方法根据可用的 XML 解析器优化 XPath 查询。
SOAPClient._getElementsByTagName = function(document, tagName)
{
try
{
return document.selectNodes(".//*[local-name()=\""+
tagName +"\"]");
}
catch (ex) {}
return document.getElementsByTagName(tagName);
}
一个工厂函数根据浏览器类型返回 XMLHttpRequest
。
SOAPClient._getXmlHttp = function()
{
try
{
if(window.XMLHttpRequest)
{
var req = new XMLHttpRequest();
if(req.readyState == null)
{
req.readyState = 1;
req.addEventListener("load",
function()
{
req.readyState = 4;
if(typeof req.onreadystatechange == "function")
req.onreadystatechange();
},
false);
}
return req;
}
if(window.ActiveXObject)
return new ActiveXObject(SOAPClient._getXmlHttpProgID());
}
catch (ex) {}
throw new Error("Your browser does not support XmlHttp objects");
}
SOAPClient._getXmlHttpProgID = function()
{
if(SOAPClient._getXmlHttpProgID.progid)
return SOAPClient._getXmlHttpProgID.progid;
var progids = ["Msxml2.XMLHTTP.5.0",
"Msxml2.XMLHTTP.4.0",
"MSXML2.XMLHTTP.3.0",
"MSXML2.XMLHTTP",
"Microsoft.XMLHTTP"];
var o;
for(var i = 0; i < progids.length; i++)
{
try
{
o = new ActiveXObject(progids[i]);
return SOAPClient._getXmlHttpProgID.progid = progids[i];
}
catch (ex) {};
}
throw new Error("Could not find an installed XML parser");
}
关注点
通过使用一个小型(小于 10KB)JavaScript 库,并在服务器端简单地公开一个带有远程方法的 Web 服务,您可以使用 AJAX 创建动态 Web 应用程序,而无需重新加载整个页面。
有关使用示例,请参阅 在线演示。