XmlHttpRequest 对象的思考
在传统的以页面为单位的浏览器和服务器交互模式中,每一次服务器请求都会导致整个页面的重新加载,即使需要更新的仅仅是页面的一小部分(比如显示一个登录错误信息)。 Ajax 技术的出现给页面带来了一些变化,其中最直观的莫过于站点的页面上出现越来越多的“ loading …”,“正在加载中……”等提示信息,有些忽如一夜春风来,loading 加载处处开的意思。“ loading …”或者“正在加载中……”表示浏览器正在与服务器之间进行交互,交互完成之后,将对页面进行局部刷新,这种交互模式虽然简单却极大的提高了 Web 应用的用户体验。实现这种模式的核心就是 XmlHttpRequest(后文简称 XHR)对象。
XHR 对象促使越来越多“单一页面”的 Web 应用的诞生。使用 XHR 对象可以发送异步 HTTP 请求。因为是异步,在浏览器和服务器交互的过程中,仍然可以操作页面。当页面中有多个进行异步调用的 XHR 对象时,事情有了质的变化,每一个 XHR 对象都可以独立于服务器进行通信,浏览器中的页面仿佛是一个多线程的应用程序。这种多线程异步调用的特性给 Web 应用的开发带来了很大的影响,越来越多像 Google Mail 这种“单一页面”的应用涌现出来,而且大受欢迎。之所以能做到“单一页面”是因为有很多的 XHR 对象默默地在背后服务,我们可以通过启用 firebug 来查看每次在 Google Mail 页面上的操作“生产”了多少个 XHR 对象。
Ajax 资源中心
请访问 Ajax 资源中心,这是有关 Ajax 编程模型信息的一站式中心,包括很多文档、教程、论坛、blog、wiki 和新闻。任何 Ajax 的新信息都能在这里找到。
使用 XHR 对象的另一个好处是可以减少服务器返回的数据量,进而提升系统的性能。在原有的 B/S 交互模式中,服务器返回的是粗粒度的 HTML 页面;使用 XHR 对象之后,服务器返回的是细粒度的数据,如 HTML,JSON,XML 等,请注意这里返回的是数据而不是页面,也就是说只返回需要更新的内容,而不返回已经在页面上显示的其他内容,所以每次从服务器返回的数据量比原来要少。采用 AJAX 技术的 Web 应用在初次加载时花费的时间比较长,但是加载完成之后,其性能比原来的 Web 应用要好很多。
这里介绍了一些 XmlHttpRequest 对象给 Web 开发带来的变化,这些变化是 Ajax 技术能够流行的重要原因,认识这些变化可以帮助开发人员设计、开发高效的 Web 应用。本文并不打算介绍 XmlHttpRequest 的属性、方法,很多文章在这方面已经做得很好。
XHR 框架
XmlHttpRequest 对象是 Dojo 中的 XHR 框架的基础,目前主流浏览器都已经支持此对象,但是不同浏览器上实现方式却不一样,IE5、IE6 采用 ActiveX 对象的方式,Firefox 和 Safari 都实现为一个内部对象,所以创建 XHR 对象之前需要先测试浏览器的类型,清单 1 展示了最简单的创建 XHR 对象的代码。
清单 1
function createXHR(){
if (window.XMLHttpRequest) { // Non IE
return new XMLHttpRequest();
}
else if (window.ActiveXObject) { // IE
return new ActiveXObject("Microsoft.XMLHTTP");
}
}
或许是认识到 XHR 对象的重要性,微软在 IE7 中已经把它实现为一个窗口对象的属性。但是判断浏览器类型的代码依然不能消除,因为 IE5, IE6 仍然有大量的使用者。
XHR 对象创建方式不一致是 Dojo 的 XHR 框架诞生的一个原因,更重要的原因是原始 XHR 对象还不够强大,有些方面不能满足开发的需要:首先 XHR 对象支持的返回类型有限,原始 XHR 对象只有 responseText 和 responseXML 两个属性代表返回的数据,重要的数据交换格式 JSON 就不被支持;其次不能设置 HTTP Request 的超时时间,设置超时时间可以让客户端脚本控制请求存在的时间,而不是被动的等待服务器端的返回。
基于这些问题,Dojo 组织提供了一组函数来支持各种 HTTP 请求,包括 xhrGet,rawXhrPost,xhrPut,rawXhrPut,xhrPut,xhrDelete,这几个函数与 HTTP 协议中的四种请求是一一对应的,HTTP 四种请求是:Get(读取),Post(更新),Put(创建),Delete(删除)。 Dojo 组织的发起者 Alex Russell 把这些跟 XHR 对象相关的函数放在一起称为 XHR 框架。下面我们来看看 Dojo 是如何创建 XHR 对象的。清单 2 是 Dojo 1.1 中创建 XHR 对象的代码片段。
清单 2
d._XMLHTTP_PROGIDS = ['Msxml2.XMLHTTP', 'Microsoft.XMLHTTP', 'Msxml2.XMLHTTP.4.0'];
d._xhrObj= function(){
var http = null;
var last_e = null;
if(!dojo.isIE || !djConfig.ieForceActiveXXhr){
try{ http = new XMLHttpRequest(); }catch(e){}
}
if(!http){
for(var i=0; i<3; ++i){
var progid = dojo._XMLHTTP_PROGIDS[i];
try{
http = new ActiveXObject(progid);
}catch(e){
last_e = e;
}
if(http){
dojo._XMLHTTP_PROGIDS = [progid];
break;
}
}
}
if(!http){
throw new Error("XMLHTTP not available: "+last_e);
}
return http; // XMLHTTPRequest instance
}
_xhrObj 是 Dojo 创建的 XHR 对象。与清单 1 相比,是不是显得有点“冗长”?其实不然,虽然多了很多 try-catch 语句,但这些 try-catch 块保证了即使创建 XHR 对象出错时,浏览器依然不会崩溃,增强了代码的健壮性;此外,代码对 IE 浏览器的“照顾”周到之至,三个 XHR 对象可能存在的命名空间('Msxml2.XMLHTTP', 'Microsoft.XMLHTTP', 'Msxml2.XMLHTTP.4.0')都做了判断,只有这样才能保证 XHR 对象在各个不同的浏览器能顺利“诞生”。从这段代码也可以看出要编写健壮、高效的代码,开发人员必须具有系统性的思维,并能合理使用错误处理机制。下面将对 XHR 框架中的每个方法进行介绍。
使用 xhrGet 请求资源
xhrGet 是 XHR 框架中最重要的函数,使用频率也最高。使用它即可以请求服务器上的静态文本资源如 txt、xml 等,也可以获取动态页面 php、jsp、asp 等,只要从服务器返回的是字符数据流即可。首先看一个简单的例子。
清单 3
function helloWorld(){
dojo.xhrGet({
url: "helloworld.txt" ,
handleAs: "txt",
load: function(response, ioArgs){alert(response);},
error: function(error, ioArgs){alert(error.message);}
});
}
函数 helloWorld 调用 dojo.xhrGet 获取服务器上与引用此 Javascript 脚本的页面同一目录下的 helloworld.txt 文件。服务器成功返回之后,使用 alert 显示文件的内容。如果出错了则使用 alert 显示错误信息。
dojo.xhrGet 的参数是一个 JSON 对象,JSON 对象由很多的属性 / 值对组成,其中的值可以是任意类型的数据: 整形、字符串、函数……甚至是 JSON 对象,这一点使得 JSON 对象的数据描述能力可以与 XML 匹敌,而且 JSON 对象可以使用“ . ”操作符来直接访问它的属性,没有任何解析的开销,非常方便。在 Javascript 领域,JSON 大有超越 XML 成为事实上的数据交换标准的趋势。使用 JSON 对象作为函数参数的情形在 Javascript 中非常普遍,可以看成 Javascript 开发中的一个模式,开发人员应该熟悉它。再回到作为 xhrGet 参数的 JSON 对象,在清单 3 的例子中的,这一对象有四个属性:
url:请求的服务器资源 url,url 标识的只能是文本文件,而不能是二进制文件。
handleAs:返回的数据类型,可以是 text(默认)、json、json-comment-optional, json-comment-filtered、javascript、xml 。 Dojo 将根据 handleAs 设置的数据类型对从服务器返回的数据进行预处理,再传给 load 属性指向的回调函数。
load:它的值是一个函数,这个函数在请求的资源成功返回之后被调用,实际上就是一回调函数。
error:它的值也是一个回调函数,但是只在 http 请求出错之后(比如,404 错误:请求的资源找不到)才被调用。
load, error 所指向的值即可以像清单 3 中所示的那样是无名函数,也可以是一个已经定义过的函数名,清单 3 的例子可以修改如下:
清单 4
function display(response, ioArgs)
{
alert(response);
}
function helloWorld2(){
dojo.xhrGet({
url: "helloworld.txt" ,
handleAs: "txt",
load:display,
error:display});
}
使用这一方法可以提高代码的复用率,尤其在有多个 xhrGet 对象需要使用相同的 load 回调函数时。
不管 load 的回调函数是无名函数还是预定义的有名函数,它都包含两个参数:response 和 ioArgs(注意:这两个参数的名称可以任意取,这里只是使用了两个常用的名称。实际上,在 Javascript 中,函数是由函数名唯一声明的,函数参数可以不出现在函数的声明中,在函数体内可以使用 arguments 引用函数的实际参数)。
response:表示从服务器端返回的数据,Dojo 已经根据 handleAs 设置的数据类型进行了预处理。
ioArgs: 这是一个对象,包含调用 xhrGet 时使用的一些参数。之所以把这些信息放在一个对象中并传递给回调函数是为了给回调函数一个执行“上下文”,让回调函数知道自己属于哪个 HTTP 请求,请求有哪些参数,返回的数据是什么类型等。这些信息在调试程序时特别有用。
ioArgs.url:请求的 URL,与调用 xhrGet 时设置的值一样。
ioArgs.query:请求中包含的参数, URL 中“ ? ”后面的内容。
ioArgs.handAs:如何对返回的数据进行预处理,与调用 xhrGet 时设置的值一样。
ioArgs.xhr: xhrGet 函数使用的 XHR 对象。
前面介绍了 xhrGet 函数以及与它关联的回调函数,xhrGet 中的 handleAs 的设置决定了如何对服务器返回的数据进行预处理,表 1 详细介绍了不同的 handleAs 代表的不同的预处理方式。
表 1 handleAs VS 预处理方式
handleAs 预处理方式
text 默认值,不对返回的数据做任何处理
xml 返回 XHR 对象的 responseXML
javascript 使用 dojo.eval 处理返回的数据,返回处理结果
json 使用 dojo.fromJSon 来处理返回的数据,返回生成的 Json 对象
json-comment-optional 如果有数据包含在注释符中,则只使用 dojo.fromJSon 处理这部分数据,如果没有数据包含在注释符中,则使用 dojo.fromJSon 处理全部数据。
json-comment-filtered 数据应该包含在 /* … */ 中,返回使用 dojo.fromJSon 生成的 Json 对象,如果数据不是包含在注释符中则不处理。
假设 handleAs 被设置为“ json ”,按照上表,则 load 回调函数的参数 response 为 JSON 对象。如果 handleAs 不是“ json ”,还能不能生成 JSON 对象呢?答案是肯定的,可以把 handleAs 设为“ text ”,那么返回的是普通的字符串,只要字符串是 JSON 对象的文本形式,则可以简单地使用 eval() 函数把它转换为真正的 JSON 对象,而不再需要任何其他的 API 完成转换工作。
清单 5
function jsonDemo() {
response = (
"[{ name: 'Joe', age: '30', gender: 'M'},
{ name: 'Chandler', age: '32', gender: 'M'},
{ name: 'Rose', age: '31', gender: 'M'}]"
);
json = eval(response);
document.write(json[0].name + "," + json[1].age +
"," + json[2].gender);
}
清单 5 是一个把文本字符串转换为 JSON 对象的例子。 response 经过 eval 处理后转换成一个 JSON 对象数组,最后输出每个 JSON 对象的一个属性。调用 jsonDemo 将在浏览器输出:
Joe,32,M
这一部分有很大篇幅跟 JSON 相关,因为 JSON 这种数据交换格式应用越来越广,希望广大开发者能更加重视。
开发人员往往在 xhrGet 的 load 回调函数中处理服务器返回的内容,然后更新页面。最简单的更新方法是把经过处理的内容设置为页面上某一节点的 innnerHTML 属性。返回的内容中最好不要包含需要立即执行的 javascript 代码片段,因为这段 javascript 是不起作用的。
使用 xhrGet 提交表单
表单的提交在 Web 应用中必不可少,以前 javascript 应用最广的地方是做表单的验证,今天我们知道 javascript 能做的比这远远要多。使用 xhrGet 提交表单与请求资源类似,只需要在 xhrGet 的参数对象中增加一个属性,关联需要提交的 form 。使用 xhrGet 异步提交 form 意义重大,在传统的 B/S 交互模式中,提交 form 则意味着页面的跳转,但很多情况下页面不用跳转,比如用户登录时,用户名或密码错误,这时不跳转页面而是直接给出错误提示信息用户体验明显要好得多,清单 6 是使用 xhrGet 提交表单的例子。
清单 6
<html>
<body>
<script src="../dojo/dojo.js"
djConfig="parseOnLoad: true"></script>
<script>
function handler(response)
{ document.write(response); }
function submitForm(){
dojo.xhrGet({
form: "loginForm" ,
handleAs: "text" ,
handle: handler,
content: { pwd: "modified" },
sync: false
});
return false;
}
</script>
<form id="loginForm" οnsubmit="return submitForm();" action="data.php">
<input type="textfield" name="id" />
<input type="password" name="pwd" />
<input type="submit" name="sub" value="submit" />
</form>
</body>
</html>
在这个例子中我们看到 xhrGet 的一些新的参数。这些参数不是仅针对提交表单的,请求资源时也可以使用。之所以在这里介绍,是为了达到循序渐进学习的目的。例子中的 data.php 是服务器端的程序,比较简单,只包含一行代码 echo $_POST[“pwd”],用来输出表单中的密码字段。
form:需要异步提交的表单的 id 。只有把它设置成想要异步提交的表单的 id,并在这个表单的 onsubmit 事件中调用自定义的 submitForm() 函数,才能真正做到异步提交。注意在 submitForm 函数中最后返回了 false,这是为了阻止系统默认的表单提交事件,让表单提交异步进行,如果不返回 false,会引起页面跳转。
handle:handle 也是一个回调函数,在 xhrGet 返回时被调用,正常和错误返回的情况都能处理,可以说是 load 和 error 的混合体,但优先级比 load 低,只有在没有设置 load 时才起作用。
content:在这里可以修改来自表单的信息,在清单 6 所示的例子中,就使用这一属性修改了用户登录时输入的密码。
sync:设置同步还是异步提交。默认是异步提交,所以在清单 6 以前的例子中并没有设置这一属性。
需要注意的是:虽然表单提交的默认方法是 POST,但当使用 xhrGet 提交时,表单提交方式就自动改为 GET,所有表单的数据都会变成查询字符串出现在 URL 中。所以在服务器端只能从查询字符串中取得这些提交的信息。在 jsp 中是:request.getQueryString(“PWD”),而在 php 中可以使用 $_GET[“PWD”] 在服务器端获取表单字段。
XHR 框架中的其他方法
除了 xhrGet,Dojo 的 XHR 框架还包含 xhrPost,rawXhrPost,xhrPut,rawXhrPut,xhrDelete 。这几个函数与 xhrGet 类似,使用方法和参数都可以参考 xhrGet 。区别在于他们的 HTTP 请求类型,xhrPost 发送的是 Post 请求,xhrPut 发送的是 Put 请求,xhrDelete 发生的是 Delete 请求。
xhrPost 一般用来发送表单数据,当然 xhrGet 也可以做到,区别是 xhrPost 把表单数据封装在 HTTP 请求的 Body 部分。在服务器端只能使用取 POST 信息的方法获取这些表单数据,假设我们要取清单 6 中的表单的 PWD 密码框中的数据,在 JSP 中可以是 request.getParameter(“PWD”),在 PHP 中可以是 $_POST[“PWD”] 。
如果使用 xhrDelete 去删除服务器上的资源,比如某一文件,因为它表示删除服务器上的某一资源,而普通用户是没有权限删除服务器上资源的权限的。所以使用 xhrDelete 方法时,一般回返回 405 错误(图 1 是使用 javascript alert 显示的错误信息),表示“对于请求所标识的资源,不允许使用请求行为中所指定的方法”。
图 1. 405 错误提示
Dojo 提供这些方法的目的当然不是为了方便开发人员增加 / 删除 / 修改服务器上的物理资源,而是为了支持 REST 架构风格。 REST 架构风格强调使用标准 HTTP 方法,即前文提到的 Get,Post,Put,Delete 来请求、操作 web 资源,注意不是物理资源。举个例子,在 REST 架构中,新建订单,应该使用 Put 方法,而删除订单应该使用 Delete 方法,而不像在以前的 Web 应用架构中,开发人员通过额外的参数来确定操作的类型。 Dojo 提供这些方法对 REST 架构风格是很好的支持。
回页首
iframe,别样的思路
除了 XHR 框架,Dojo Core 还提供了一个 io 包,Dojo 的官方说明把他们描述成“高级传输层(advanced ajax transport layer)”,由两个对象组成,dojo.io.iframe 和 dojo.io.script 。 使用 dojo.io.iframe 同样可以跟服务器交互,但是它采用了与 XHR 对象不同的实现思路。清单 7 是一个使用 iframe 方式提交表单的例子。
清单 7
<html><body>
<script src="../dojo/dojo/dojo.js"
djConfig="parseOnLoad: true"></script>
<script>
dojo.require("dojo.io.iframe" );
function iframeTest(){
dojo.io.iframe.send({
form: dojo.byId('testForm') ,
url: "data.php" ,
method: "post" ,
handleAs: "html" ,
load: function(response, ioArgs) {
alert(response);
}
});
}
</script>
<form id="testForm" method="post" >
Name:<input type="text" name="name" size="40" /><br />
Sex:<input type="radio" name="sex" value="m" >
Male<input type="radio" name="sex" value="f"> Female<br />
<input type="button" name="sub" value="submit" οnclick="iframeTest()" />
</form>
</body></html>
从这个例子中可以看出,dojo.io.iframe 的使用方式、参数与 xhrGet 非常相似。其中,from,url,handleAs,load 等在 xhrGet 中也存在,唯一不同的是 method,method 表示 dojo.io.iframe 将以何种 HTTP Method 来发送请求。另外需要注意的一点是 handleAs 参数,dojo.io.iframe 一般使用 html,因为在 iframe 中存的其实是另一个 HTML 页面。如果 handleAs 设置为其他值,像 json,text 等,则在服务器端须使用 <textarea></textarea> 把要返回的数据包装起来,比如 hellow, world 要被包装成 <textarea>hello,world</textarea>,所以最后存在 iframe 中的是一个文本域(textarea),这个文本域包含了从服务器端返回的数据。这么做的原因很简单,就是为了保持从服务器返回的数据“一成不变”,因为任何字符数据都可以“安全的”放在 HTML 页面的文本域中。想像一下,我们是不是可以在文本域中输入各种字符! dojo.io.iframe 会对 textarea 包装的数据进行处理:首先把 textarea 标签去掉,然后把数据转换为 handleAs 指定的类型传递给 handle 中设置的回调函数。
dojo.io.iframe 是如何工作的呢?除了 XHR 对象之外还有什么方法可以实现表单的异步提交?其实这一切都很简单,dojo.io.iframe 首先会创建一个隐藏的 iframe 并插入到父页面的最后,然后设置此 iframe 的 src 属性为dojo-module-path/resources/blank.html(dojo-module-path 指 dojo 包所在的目录),iframe 页面的 onload 事件的处理函数被设置为父窗体的回调函数。接下来就是在 iframe 页面中发送请求,并接收服务器的响应。当 iframe 接收到服务器的反馈并加载完之后,父窗体的回调函数即被调用。
dojo.io.iframe 还有其他几个很有用的函数
create: function(/*String*/fname, /*String*/onloadstr, /*String?*/uri)dojo.io.iframe.setSrc()
create 函数用来在页面中创建 iframe,参数 fname 表示 iframe 的名字,setSrc 和 doc 函数据此引用创建的 iframe,onloadstr 表示 iframe 加载完成后执行的回调函数,uri:iframe 请求的资源。后两个参数是可选的,当 uri 为空时,将加载dojo-module-path/resources/blank.html。
setSrc: function(/*DOMNode*/iframe, /*String*/src, /*Boolean*/replace)
设置指定的 iframe 的 src 属性,这将导致 iframe 页面重新加载。 iframe:需要刷新的 iframe 的名字,src:用来刷新 iframe 的页面 , replace:是否使用 location.replace 方法来更新 iframe 页面的 url,如果使用 location.replace 方法,则不会在浏览器上留下历史记录。
doc: function(/*DOMNode*/iframeNode)
获取指定的 iframe 页面的 DOM 根节点,有了它可以对 iframe 页面进行任意的操作。
dojo.io.iframe 采用了不同的思路实现了“异步”发送请求,但是 dojo.io.iframe 使用并不多,因为当页面中多处需要异步通信时,在页面中创建很多的 iframe 并不是好的注意。在能使用 xhr 框架的地方尽量使用 xhr 框架,唯一值得使用 iframe 的地方是发送文件。
回页首
动态 Script,跨域访问
XHR 框架中的函数功能强大,使用方便。但是 XHR 框架的函数有一问题就是不能跨域访问,浏览器不允许 XHR 对象访问其他域的站点。比如有一个页面属于 a.com,在这个页面中使用 XHR 对象去访问 b.com 的某一页面,这是被禁止的。如果使用 XHR 对象来做跨域访问一般需要服务器端的程序做“中转”,先由服务器端的程序获取其他域的数据,然后浏览器再使用 XHR 对象从服务器上获取这些数据,这种方式即增加了服务器端的开销,浏览器端的效率也不高。
有没有方法直接在浏览器中实现跨域访问呢?当然有,它就是 script 标签,使用 script 标签可以引用本域或其他域的文件,只要这些文件最后返回的是 javascript 。返回的 javascript 会立即在浏览器中执行,执行结果存储在本地浏览器。这一点很重要,它使得各站点可以通过 javascript 来发布自己的服务,像 google 的很多的服务都是通过这种方式提供的。 script 标签不仅可以静态添加到页面中,也可以被动态插入到页面中,而且通过 DOM 操作方式动态插入的 script 标签具有与静态 script 标签一样的效果,动态 script 标签引用的 javascript 文件也会被执行(注意:通过 innerHTML 方式插入的 javascript 是不会被执行的,这一点在前文已经介绍过)。
清单 8
function createScript() {
var element = document.createElement("script" );
element.type = "text/javascript" ;
element.src = url;
document.getElementsByTagName("head" )[0].appendChild(element);
}
清单 8 中的例子展示了动态创建 script 标签的例子,script 标签即可以放在页面的 head 部分,也可以放在 body 部分。
动态插入 script 标签的一个问题是如何判断返回的 Javascript 执行完了,只有在执行完之后才能引用 Javascript 中的对象、变量、调用它中间的函数等。最简单的方法是“标志变量法”,即在脚本中插入标志变量,在脚本最后给这个变量赋值,而在浏览器的脚本中判断这一变量是否已经被赋值,如果已经被赋值则表示返回的脚本已经执行完。但是这种方法缺点也很明显,首先如果一个页面有很多的动态 script 标签,而每个 script 标签引用的 javascript 都使用一个标志变量,那就有很多变量需要判断,而且这些变量的命名可能冲突,因为这些 Javascript 是由不同的组织、公司提供的,难保不产生冲突。另外在浏览器本地脚本中需要轮询这些变量的值,虽然可以实现,但实在不是高明的做法。目前被广泛使用的是另一种方法:JSONP(JSON with Padding)。 JSON 表示返回的 Javascript 其实就是一 JSON 对象,这是使用 JSONP 这种方式的前提条件。 Padding 表示在 JSON 对象前要附加上一些东西,究竟是什么呢?请往下看!
JSONP 的思路很简单,与其让浏览器脚本来判断返回的 Javascript 是否执行完毕,不如让 Javascript 在执行完毕之后自动调用我们想要执行的函数。是不是想起了学习面向对象设计中的“依赖倒置”原则时的那句名言:“Don't call us, we will call you ”。使用 JSONP 这种方法,只需要在原来的 Javascript 引用链接上加上一个参数,把需要执行的回调函数传递进去。请看下面的两个 script 标签。
<script src= ” http://url/js.php?parameter= … . ” >
<script src= ” http://url/js.php? parameter= … .&callbackname=mycallback” >
如果 http://url/js.php?parameter= … . 返回的是 JSON 对象 {result:“hello,world”} , 那么第二个 script 标签返回的则是 mycallback({result: ” hello, world ” }),这一函数将在写入到浏览器后立即被执行,这不就实现了在 Javascript 执行完之后自动调用我们需要执行的回调函数了吗?
介绍了这么多动态脚本的背景知识,终于来到了 Dojo 对动态脚本的支持上。 Dojo 即支持标志变量法,也支持 JSONP 方式。清单 9 是使用标志变量法的动态 script 的例子,在这个例子中使用了 dojo.io.script.get 函数。
清单 9
<html>
<head>
<script src="../dojo/dojo/dojo.js"
djConfig="parseOnLoad: true"></script>
dojo.require("dojo.io.script" );
</head>
<body>
<script>
dojo.io.script.get({
url:"data.php" ,checkStrin
g:"test_01" ,
handle:function(response, ioArgs) {
greetFromServer("hello, world");
}
});
</script>
</body>
</html>
dojo.io.script.get 函数的使用方式和参数是不是与 xhrGet 很相似?只有 checkString 是 xhrGet 所特有的,checkString 正是“标志变量法”的关键,checkString 表示从服务器返回的 javascript 需要定义的变量。清单 10 展示了使用 PHP 编写的服务器端的脚本,它输出了一段 javascript,在这段 javasript 的最后给变量 test_01 赋值。而清单 9 中 dojo.io.script.get 函数的 handle 指向的回调函数又调用了这段 javascript 中定义的函数 greetFromServer() 。只有在 test_01 被赋值后,调用 greetFromServer 才是安全的。
清单 10
<?php
echo "vartest_01;";
echo "function greetFromServer(name) {alert(name)}";
echo "test_01='defined'";
?>
dojo.io.script.get 函数也支持 JSONP 方式,当 dojo.io.script.get 函数的参数对象使用了 callbackParamName 属性时,表示它工作在 JSONP 方式下。 callbackParamName 表示在 url 中添加回调函数名的参数名称,有点拗口,但是看了下面 dojo.io.script.get 函数在页面中动态创建的 script 标签一切就都清楚了,最终出现在 URL 中的是 callbackName,而不是 callbackParamName 。
<script src="data2.php?callbackName=dojo.io.script.jsonp_dojoIoScript1._jsonpCallback" >
清单 11
<html><head>
<script src="../dojo/dojo/dojo.js"
djConfig="parseOnLoad: true"></script>
dojo.require("dojo.io.script" );
</head>
<body>
<script>
dojo.io.script.get({
url:"data2.php",
callbackParamName: "callbackName" ,
handle: function(response, ioArgs) {
document.write(response.greet);
}
});
}
</script>
</body></html>
Dojo 会自动创建一个名为 dojo.io.script.jsonp_dojoIoScript1._jsonpCallback 的 javascript 函数,这个函数其实什么都不做,只是作为一个回调函数传给服务器端程序。 php 的服务器端程序如清单 12 所示,callbackName 像浏览器和服务器之间的一个“信令”,服务器端必须返回对 callbackName 所代表的函数的调用,因为 Dojo 会检查它是否被调用过。
所以服务器端返回的是 dojo.io.script.jsonp_dojoIoScript1._jsonpCallback({greet:’hello, world’}) 。参数 {greet:’hello, world’} 正是要返回到浏览器的 JSON 对象。
清单 12
<?php
$cb = $_GET["callbackName"];
echo $cb."({greet:'hello, world'});";
?>
清单 11 所示程序的输出为:hello, world,由此可以看出,response 参数就是从服务器端返回的 JSON 对象,服务器端的 JSON 对象终于成功的传递到浏览器了。前面介绍了这么多的机制都是为了使这个 JSON 对象安全返回到浏览器中。当然你可以在服务器端返回任何数据,比如直接返回一个字符串,但此时 response 就变成字符串了,当然也就不能再叫 JSONP 了,因为 JSONP 特指返回的是 JSON 对象。
dojo.io.script 对象中除了 get 函数之外,还有 attach,和 remove 两个函数
attach: function(/*String*/id, /*String*/url)
创建动态 script 标签,标签的 id 由参数 id 指定,src 由 url 指定。
remove: function(/*String*/id)
删除 id 代表 script 标签。
总结
本文介绍了 Dojo 中三种浏览器与服务器交互的方式,这三种方式各有优缺点,但是在使用方式却出奇的一致; xhr 框架的函数,dojo.io.iframe、dojo.io.script 对象的函数使用的 JSON 对象参数也极其相似,而且浅显易懂。 Dojo 设计者的这一良好设计极大的减轻了开发人员的学习负担,作为框架开发人员应该了解这一理念。表 2 对这三种方式从三个方面进行了比较。
表 2. 三种方式的比较
支持的 HTTP 请求类型 期望的输出 跨域访问
XHR Get, post, delete, put text, json, xml, javascript … N
iframe Get, post html N
script Get javascript Y
综上所述,使用上述三种方法时需要遵循一条简单的原则:传送文件则 iframe,跨域访问则使用动态脚本,其余则选 XHR 框架。
在传统的以页面为单位的浏览器和服务器交互模式中,每一次服务器请求都会导致整个页面的重新加载,即使需要更新的仅仅是页面的一小部分(比如显示一个登录错误信息)。 Ajax 技术的出现给页面带来了一些变化,其中最直观的莫过于站点的页面上出现越来越多的“ loading …”,“正在加载中……”等提示信息,有些忽如一夜春风来,loading 加载处处开的意思。“ loading …”或者“正在加载中……”表示浏览器正在与服务器之间进行交互,交互完成之后,将对页面进行局部刷新,这种交互模式虽然简单却极大的提高了 Web 应用的用户体验。实现这种模式的核心就是 XmlHttpRequest(后文简称 XHR)对象。
XHR 对象促使越来越多“单一页面”的 Web 应用的诞生。使用 XHR 对象可以发送异步 HTTP 请求。因为是异步,在浏览器和服务器交互的过程中,仍然可以操作页面。当页面中有多个进行异步调用的 XHR 对象时,事情有了质的变化,每一个 XHR 对象都可以独立于服务器进行通信,浏览器中的页面仿佛是一个多线程的应用程序。这种多线程异步调用的特性给 Web 应用的开发带来了很大的影响,越来越多像 Google Mail 这种“单一页面”的应用涌现出来,而且大受欢迎。之所以能做到“单一页面”是因为有很多的 XHR 对象默默地在背后服务,我们可以通过启用 firebug 来查看每次在 Google Mail 页面上的操作“生产”了多少个 XHR 对象。
Ajax 资源中心
请访问 Ajax 资源中心,这是有关 Ajax 编程模型信息的一站式中心,包括很多文档、教程、论坛、blog、wiki 和新闻。任何 Ajax 的新信息都能在这里找到。
使用 XHR 对象的另一个好处是可以减少服务器返回的数据量,进而提升系统的性能。在原有的 B/S 交互模式中,服务器返回的是粗粒度的 HTML 页面;使用 XHR 对象之后,服务器返回的是细粒度的数据,如 HTML,JSON,XML 等,请注意这里返回的是数据而不是页面,也就是说只返回需要更新的内容,而不返回已经在页面上显示的其他内容,所以每次从服务器返回的数据量比原来要少。采用 AJAX 技术的 Web 应用在初次加载时花费的时间比较长,但是加载完成之后,其性能比原来的 Web 应用要好很多。
这里介绍了一些 XmlHttpRequest 对象给 Web 开发带来的变化,这些变化是 Ajax 技术能够流行的重要原因,认识这些变化可以帮助开发人员设计、开发高效的 Web 应用。本文并不打算介绍 XmlHttpRequest 的属性、方法,很多文章在这方面已经做得很好。
XHR 框架
XmlHttpRequest 对象是 Dojo 中的 XHR 框架的基础,目前主流浏览器都已经支持此对象,但是不同浏览器上实现方式却不一样,IE5、IE6 采用 ActiveX 对象的方式,Firefox 和 Safari 都实现为一个内部对象,所以创建 XHR 对象之前需要先测试浏览器的类型,清单 1 展示了最简单的创建 XHR 对象的代码。
清单 1
function createXHR(){
if (window.XMLHttpRequest) { // Non IE
return new XMLHttpRequest();
}
else if (window.ActiveXObject) { // IE
return new ActiveXObject("Microsoft.XMLHTTP");
}
}
或许是认识到 XHR 对象的重要性,微软在 IE7 中已经把它实现为一个窗口对象的属性。但是判断浏览器类型的代码依然不能消除,因为 IE5, IE6 仍然有大量的使用者。
XHR 对象创建方式不一致是 Dojo 的 XHR 框架诞生的一个原因,更重要的原因是原始 XHR 对象还不够强大,有些方面不能满足开发的需要:首先 XHR 对象支持的返回类型有限,原始 XHR 对象只有 responseText 和 responseXML 两个属性代表返回的数据,重要的数据交换格式 JSON 就不被支持;其次不能设置 HTTP Request 的超时时间,设置超时时间可以让客户端脚本控制请求存在的时间,而不是被动的等待服务器端的返回。
基于这些问题,Dojo 组织提供了一组函数来支持各种 HTTP 请求,包括 xhrGet,rawXhrPost,xhrPut,rawXhrPut,xhrPut,xhrDelete,这几个函数与 HTTP 协议中的四种请求是一一对应的,HTTP 四种请求是:Get(读取),Post(更新),Put(创建),Delete(删除)。 Dojo 组织的发起者 Alex Russell 把这些跟 XHR 对象相关的函数放在一起称为 XHR 框架。下面我们来看看 Dojo 是如何创建 XHR 对象的。清单 2 是 Dojo 1.1 中创建 XHR 对象的代码片段。
清单 2
d._XMLHTTP_PROGIDS = ['Msxml2.XMLHTTP', 'Microsoft.XMLHTTP', 'Msxml2.XMLHTTP.4.0'];
d._xhrObj= function(){
var http = null;
var last_e = null;
if(!dojo.isIE || !djConfig.ieForceActiveXXhr){
try{ http = new XMLHttpRequest(); }catch(e){}
}
if(!http){
for(var i=0; i<3; ++i){
var progid = dojo._XMLHTTP_PROGIDS[i];
try{
http = new ActiveXObject(progid);
}catch(e){
last_e = e;
}
if(http){
dojo._XMLHTTP_PROGIDS = [progid];
break;
}
}
}
if(!http){
throw new Error("XMLHTTP not available: "+last_e);
}
return http; // XMLHTTPRequest instance
}
_xhrObj 是 Dojo 创建的 XHR 对象。与清单 1 相比,是不是显得有点“冗长”?其实不然,虽然多了很多 try-catch 语句,但这些 try-catch 块保证了即使创建 XHR 对象出错时,浏览器依然不会崩溃,增强了代码的健壮性;此外,代码对 IE 浏览器的“照顾”周到之至,三个 XHR 对象可能存在的命名空间('Msxml2.XMLHTTP', 'Microsoft.XMLHTTP', 'Msxml2.XMLHTTP.4.0')都做了判断,只有这样才能保证 XHR 对象在各个不同的浏览器能顺利“诞生”。从这段代码也可以看出要编写健壮、高效的代码,开发人员必须具有系统性的思维,并能合理使用错误处理机制。下面将对 XHR 框架中的每个方法进行介绍。
使用 xhrGet 请求资源
xhrGet 是 XHR 框架中最重要的函数,使用频率也最高。使用它即可以请求服务器上的静态文本资源如 txt、xml 等,也可以获取动态页面 php、jsp、asp 等,只要从服务器返回的是字符数据流即可。首先看一个简单的例子。
清单 3
function helloWorld(){
dojo.xhrGet({
url: "helloworld.txt" ,
handleAs: "txt",
load: function(response, ioArgs){alert(response);},
error: function(error, ioArgs){alert(error.message);}
});
}
函数 helloWorld 调用 dojo.xhrGet 获取服务器上与引用此 Javascript 脚本的页面同一目录下的 helloworld.txt 文件。服务器成功返回之后,使用 alert 显示文件的内容。如果出错了则使用 alert 显示错误信息。
dojo.xhrGet 的参数是一个 JSON 对象,JSON 对象由很多的属性 / 值对组成,其中的值可以是任意类型的数据: 整形、字符串、函数……甚至是 JSON 对象,这一点使得 JSON 对象的数据描述能力可以与 XML 匹敌,而且 JSON 对象可以使用“ . ”操作符来直接访问它的属性,没有任何解析的开销,非常方便。在 Javascript 领域,JSON 大有超越 XML 成为事实上的数据交换标准的趋势。使用 JSON 对象作为函数参数的情形在 Javascript 中非常普遍,可以看成 Javascript 开发中的一个模式,开发人员应该熟悉它。再回到作为 xhrGet 参数的 JSON 对象,在清单 3 的例子中的,这一对象有四个属性:
url:请求的服务器资源 url,url 标识的只能是文本文件,而不能是二进制文件。
handleAs:返回的数据类型,可以是 text(默认)、json、json-comment-optional, json-comment-filtered、javascript、xml 。 Dojo 将根据 handleAs 设置的数据类型对从服务器返回的数据进行预处理,再传给 load 属性指向的回调函数。
load:它的值是一个函数,这个函数在请求的资源成功返回之后被调用,实际上就是一回调函数。
error:它的值也是一个回调函数,但是只在 http 请求出错之后(比如,404 错误:请求的资源找不到)才被调用。
load, error 所指向的值即可以像清单 3 中所示的那样是无名函数,也可以是一个已经定义过的函数名,清单 3 的例子可以修改如下:
清单 4
function display(response, ioArgs)
{
alert(response);
}
function helloWorld2(){
dojo.xhrGet({
url: "helloworld.txt" ,
handleAs: "txt",
load:display,
error:display});
}
使用这一方法可以提高代码的复用率,尤其在有多个 xhrGet 对象需要使用相同的 load 回调函数时。
不管 load 的回调函数是无名函数还是预定义的有名函数,它都包含两个参数:response 和 ioArgs(注意:这两个参数的名称可以任意取,这里只是使用了两个常用的名称。实际上,在 Javascript 中,函数是由函数名唯一声明的,函数参数可以不出现在函数的声明中,在函数体内可以使用 arguments 引用函数的实际参数)。
response:表示从服务器端返回的数据,Dojo 已经根据 handleAs 设置的数据类型进行了预处理。
ioArgs: 这是一个对象,包含调用 xhrGet 时使用的一些参数。之所以把这些信息放在一个对象中并传递给回调函数是为了给回调函数一个执行“上下文”,让回调函数知道自己属于哪个 HTTP 请求,请求有哪些参数,返回的数据是什么类型等。这些信息在调试程序时特别有用。
ioArgs.url:请求的 URL,与调用 xhrGet 时设置的值一样。
ioArgs.query:请求中包含的参数, URL 中“ ? ”后面的内容。
ioArgs.handAs:如何对返回的数据进行预处理,与调用 xhrGet 时设置的值一样。
ioArgs.xhr: xhrGet 函数使用的 XHR 对象。
前面介绍了 xhrGet 函数以及与它关联的回调函数,xhrGet 中的 handleAs 的设置决定了如何对服务器返回的数据进行预处理,表 1 详细介绍了不同的 handleAs 代表的不同的预处理方式。
表 1 handleAs VS 预处理方式
handleAs 预处理方式
text 默认值,不对返回的数据做任何处理
xml 返回 XHR 对象的 responseXML
javascript 使用 dojo.eval 处理返回的数据,返回处理结果
json 使用 dojo.fromJSon 来处理返回的数据,返回生成的 Json 对象
json-comment-optional 如果有数据包含在注释符中,则只使用 dojo.fromJSon 处理这部分数据,如果没有数据包含在注释符中,则使用 dojo.fromJSon 处理全部数据。
json-comment-filtered 数据应该包含在 /* … */ 中,返回使用 dojo.fromJSon 生成的 Json 对象,如果数据不是包含在注释符中则不处理。
假设 handleAs 被设置为“ json ”,按照上表,则 load 回调函数的参数 response 为 JSON 对象。如果 handleAs 不是“ json ”,还能不能生成 JSON 对象呢?答案是肯定的,可以把 handleAs 设为“ text ”,那么返回的是普通的字符串,只要字符串是 JSON 对象的文本形式,则可以简单地使用 eval() 函数把它转换为真正的 JSON 对象,而不再需要任何其他的 API 完成转换工作。
清单 5
function jsonDemo() {
response = (
"[{ name: 'Joe', age: '30', gender: 'M'},
{ name: 'Chandler', age: '32', gender: 'M'},
{ name: 'Rose', age: '31', gender: 'M'}]"
);
json = eval(response);
document.write(json[0].name + "," + json[1].age +
"," + json[2].gender);
}
清单 5 是一个把文本字符串转换为 JSON 对象的例子。 response 经过 eval 处理后转换成一个 JSON 对象数组,最后输出每个 JSON 对象的一个属性。调用 jsonDemo 将在浏览器输出:
Joe,32,M
这一部分有很大篇幅跟 JSON 相关,因为 JSON 这种数据交换格式应用越来越广,希望广大开发者能更加重视。
开发人员往往在 xhrGet 的 load 回调函数中处理服务器返回的内容,然后更新页面。最简单的更新方法是把经过处理的内容设置为页面上某一节点的 innnerHTML 属性。返回的内容中最好不要包含需要立即执行的 javascript 代码片段,因为这段 javascript 是不起作用的。
使用 xhrGet 提交表单
表单的提交在 Web 应用中必不可少,以前 javascript 应用最广的地方是做表单的验证,今天我们知道 javascript 能做的比这远远要多。使用 xhrGet 提交表单与请求资源类似,只需要在 xhrGet 的参数对象中增加一个属性,关联需要提交的 form 。使用 xhrGet 异步提交 form 意义重大,在传统的 B/S 交互模式中,提交 form 则意味着页面的跳转,但很多情况下页面不用跳转,比如用户登录时,用户名或密码错误,这时不跳转页面而是直接给出错误提示信息用户体验明显要好得多,清单 6 是使用 xhrGet 提交表单的例子。
清单 6
<html>
<body>
<script src="../dojo/dojo.js"
djConfig="parseOnLoad: true"></script>
<script>
function handler(response)
{ document.write(response); }
function submitForm(){
dojo.xhrGet({
form: "loginForm" ,
handleAs: "text" ,
handle: handler,
content: { pwd: "modified" },
sync: false
});
return false;
}
</script>
<form id="loginForm" οnsubmit="return submitForm();" action="data.php">
<input type="textfield" name="id" />
<input type="password" name="pwd" />
<input type="submit" name="sub" value="submit" />
</form>
</body>
</html>
在这个例子中我们看到 xhrGet 的一些新的参数。这些参数不是仅针对提交表单的,请求资源时也可以使用。之所以在这里介绍,是为了达到循序渐进学习的目的。例子中的 data.php 是服务器端的程序,比较简单,只包含一行代码 echo $_POST[“pwd”],用来输出表单中的密码字段。
form:需要异步提交的表单的 id 。只有把它设置成想要异步提交的表单的 id,并在这个表单的 onsubmit 事件中调用自定义的 submitForm() 函数,才能真正做到异步提交。注意在 submitForm 函数中最后返回了 false,这是为了阻止系统默认的表单提交事件,让表单提交异步进行,如果不返回 false,会引起页面跳转。
handle:handle 也是一个回调函数,在 xhrGet 返回时被调用,正常和错误返回的情况都能处理,可以说是 load 和 error 的混合体,但优先级比 load 低,只有在没有设置 load 时才起作用。
content:在这里可以修改来自表单的信息,在清单 6 所示的例子中,就使用这一属性修改了用户登录时输入的密码。
sync:设置同步还是异步提交。默认是异步提交,所以在清单 6 以前的例子中并没有设置这一属性。
需要注意的是:虽然表单提交的默认方法是 POST,但当使用 xhrGet 提交时,表单提交方式就自动改为 GET,所有表单的数据都会变成查询字符串出现在 URL 中。所以在服务器端只能从查询字符串中取得这些提交的信息。在 jsp 中是:request.getQueryString(“PWD”),而在 php 中可以使用 $_GET[“PWD”] 在服务器端获取表单字段。
XHR 框架中的其他方法
除了 xhrGet,Dojo 的 XHR 框架还包含 xhrPost,rawXhrPost,xhrPut,rawXhrPut,xhrDelete 。这几个函数与 xhrGet 类似,使用方法和参数都可以参考 xhrGet 。区别在于他们的 HTTP 请求类型,xhrPost 发送的是 Post 请求,xhrPut 发送的是 Put 请求,xhrDelete 发生的是 Delete 请求。
xhrPost 一般用来发送表单数据,当然 xhrGet 也可以做到,区别是 xhrPost 把表单数据封装在 HTTP 请求的 Body 部分。在服务器端只能使用取 POST 信息的方法获取这些表单数据,假设我们要取清单 6 中的表单的 PWD 密码框中的数据,在 JSP 中可以是 request.getParameter(“PWD”),在 PHP 中可以是 $_POST[“PWD”] 。
如果使用 xhrDelete 去删除服务器上的资源,比如某一文件,因为它表示删除服务器上的某一资源,而普通用户是没有权限删除服务器上资源的权限的。所以使用 xhrDelete 方法时,一般回返回 405 错误(图 1 是使用 javascript alert 显示的错误信息),表示“对于请求所标识的资源,不允许使用请求行为中所指定的方法”。
图 1. 405 错误提示
Dojo 提供这些方法的目的当然不是为了方便开发人员增加 / 删除 / 修改服务器上的物理资源,而是为了支持 REST 架构风格。 REST 架构风格强调使用标准 HTTP 方法,即前文提到的 Get,Post,Put,Delete 来请求、操作 web 资源,注意不是物理资源。举个例子,在 REST 架构中,新建订单,应该使用 Put 方法,而删除订单应该使用 Delete 方法,而不像在以前的 Web 应用架构中,开发人员通过额外的参数来确定操作的类型。 Dojo 提供这些方法对 REST 架构风格是很好的支持。
回页首
iframe,别样的思路
除了 XHR 框架,Dojo Core 还提供了一个 io 包,Dojo 的官方说明把他们描述成“高级传输层(advanced ajax transport layer)”,由两个对象组成,dojo.io.iframe 和 dojo.io.script 。 使用 dojo.io.iframe 同样可以跟服务器交互,但是它采用了与 XHR 对象不同的实现思路。清单 7 是一个使用 iframe 方式提交表单的例子。
清单 7
<html><body>
<script src="../dojo/dojo/dojo.js"
djConfig="parseOnLoad: true"></script>
<script>
dojo.require("dojo.io.iframe" );
function iframeTest(){
dojo.io.iframe.send({
form: dojo.byId('testForm') ,
url: "data.php" ,
method: "post" ,
handleAs: "html" ,
load: function(response, ioArgs) {
alert(response);
}
});
}
</script>
<form id="testForm" method="post" >
Name:<input type="text" name="name" size="40" /><br />
Sex:<input type="radio" name="sex" value="m" >
Male<input type="radio" name="sex" value="f"> Female<br />
<input type="button" name="sub" value="submit" οnclick="iframeTest()" />
</form>
</body></html>
从这个例子中可以看出,dojo.io.iframe 的使用方式、参数与 xhrGet 非常相似。其中,from,url,handleAs,load 等在 xhrGet 中也存在,唯一不同的是 method,method 表示 dojo.io.iframe 将以何种 HTTP Method 来发送请求。另外需要注意的一点是 handleAs 参数,dojo.io.iframe 一般使用 html,因为在 iframe 中存的其实是另一个 HTML 页面。如果 handleAs 设置为其他值,像 json,text 等,则在服务器端须使用 <textarea></textarea> 把要返回的数据包装起来,比如 hellow, world 要被包装成 <textarea>hello,world</textarea>,所以最后存在 iframe 中的是一个文本域(textarea),这个文本域包含了从服务器端返回的数据。这么做的原因很简单,就是为了保持从服务器返回的数据“一成不变”,因为任何字符数据都可以“安全的”放在 HTML 页面的文本域中。想像一下,我们是不是可以在文本域中输入各种字符! dojo.io.iframe 会对 textarea 包装的数据进行处理:首先把 textarea 标签去掉,然后把数据转换为 handleAs 指定的类型传递给 handle 中设置的回调函数。
dojo.io.iframe 是如何工作的呢?除了 XHR 对象之外还有什么方法可以实现表单的异步提交?其实这一切都很简单,dojo.io.iframe 首先会创建一个隐藏的 iframe 并插入到父页面的最后,然后设置此 iframe 的 src 属性为dojo-module-path/resources/blank.html(dojo-module-path 指 dojo 包所在的目录),iframe 页面的 onload 事件的处理函数被设置为父窗体的回调函数。接下来就是在 iframe 页面中发送请求,并接收服务器的响应。当 iframe 接收到服务器的反馈并加载完之后,父窗体的回调函数即被调用。
dojo.io.iframe 还有其他几个很有用的函数
create: function(/*String*/fname, /*String*/onloadstr, /*String?*/uri)dojo.io.iframe.setSrc()
create 函数用来在页面中创建 iframe,参数 fname 表示 iframe 的名字,setSrc 和 doc 函数据此引用创建的 iframe,onloadstr 表示 iframe 加载完成后执行的回调函数,uri:iframe 请求的资源。后两个参数是可选的,当 uri 为空时,将加载dojo-module-path/resources/blank.html。
setSrc: function(/*DOMNode*/iframe, /*String*/src, /*Boolean*/replace)
设置指定的 iframe 的 src 属性,这将导致 iframe 页面重新加载。 iframe:需要刷新的 iframe 的名字,src:用来刷新 iframe 的页面 , replace:是否使用 location.replace 方法来更新 iframe 页面的 url,如果使用 location.replace 方法,则不会在浏览器上留下历史记录。
doc: function(/*DOMNode*/iframeNode)
获取指定的 iframe 页面的 DOM 根节点,有了它可以对 iframe 页面进行任意的操作。
dojo.io.iframe 采用了不同的思路实现了“异步”发送请求,但是 dojo.io.iframe 使用并不多,因为当页面中多处需要异步通信时,在页面中创建很多的 iframe 并不是好的注意。在能使用 xhr 框架的地方尽量使用 xhr 框架,唯一值得使用 iframe 的地方是发送文件。
回页首
动态 Script,跨域访问
XHR 框架中的函数功能强大,使用方便。但是 XHR 框架的函数有一问题就是不能跨域访问,浏览器不允许 XHR 对象访问其他域的站点。比如有一个页面属于 a.com,在这个页面中使用 XHR 对象去访问 b.com 的某一页面,这是被禁止的。如果使用 XHR 对象来做跨域访问一般需要服务器端的程序做“中转”,先由服务器端的程序获取其他域的数据,然后浏览器再使用 XHR 对象从服务器上获取这些数据,这种方式即增加了服务器端的开销,浏览器端的效率也不高。
有没有方法直接在浏览器中实现跨域访问呢?当然有,它就是 script 标签,使用 script 标签可以引用本域或其他域的文件,只要这些文件最后返回的是 javascript 。返回的 javascript 会立即在浏览器中执行,执行结果存储在本地浏览器。这一点很重要,它使得各站点可以通过 javascript 来发布自己的服务,像 google 的很多的服务都是通过这种方式提供的。 script 标签不仅可以静态添加到页面中,也可以被动态插入到页面中,而且通过 DOM 操作方式动态插入的 script 标签具有与静态 script 标签一样的效果,动态 script 标签引用的 javascript 文件也会被执行(注意:通过 innerHTML 方式插入的 javascript 是不会被执行的,这一点在前文已经介绍过)。
清单 8
function createScript() {
var element = document.createElement("script" );
element.type = "text/javascript" ;
element.src = url;
document.getElementsByTagName("head" )[0].appendChild(element);
}
清单 8 中的例子展示了动态创建 script 标签的例子,script 标签即可以放在页面的 head 部分,也可以放在 body 部分。
动态插入 script 标签的一个问题是如何判断返回的 Javascript 执行完了,只有在执行完之后才能引用 Javascript 中的对象、变量、调用它中间的函数等。最简单的方法是“标志变量法”,即在脚本中插入标志变量,在脚本最后给这个变量赋值,而在浏览器的脚本中判断这一变量是否已经被赋值,如果已经被赋值则表示返回的脚本已经执行完。但是这种方法缺点也很明显,首先如果一个页面有很多的动态 script 标签,而每个 script 标签引用的 javascript 都使用一个标志变量,那就有很多变量需要判断,而且这些变量的命名可能冲突,因为这些 Javascript 是由不同的组织、公司提供的,难保不产生冲突。另外在浏览器本地脚本中需要轮询这些变量的值,虽然可以实现,但实在不是高明的做法。目前被广泛使用的是另一种方法:JSONP(JSON with Padding)。 JSON 表示返回的 Javascript 其实就是一 JSON 对象,这是使用 JSONP 这种方式的前提条件。 Padding 表示在 JSON 对象前要附加上一些东西,究竟是什么呢?请往下看!
JSONP 的思路很简单,与其让浏览器脚本来判断返回的 Javascript 是否执行完毕,不如让 Javascript 在执行完毕之后自动调用我们想要执行的函数。是不是想起了学习面向对象设计中的“依赖倒置”原则时的那句名言:“Don't call us, we will call you ”。使用 JSONP 这种方法,只需要在原来的 Javascript 引用链接上加上一个参数,把需要执行的回调函数传递进去。请看下面的两个 script 标签。
<script src= ” http://url/js.php?parameter= … . ” >
<script src= ” http://url/js.php? parameter= … .&callbackname=mycallback” >
如果 http://url/js.php?parameter= … . 返回的是 JSON 对象 {result:“hello,world”} , 那么第二个 script 标签返回的则是 mycallback({result: ” hello, world ” }),这一函数将在写入到浏览器后立即被执行,这不就实现了在 Javascript 执行完之后自动调用我们需要执行的回调函数了吗?
介绍了这么多动态脚本的背景知识,终于来到了 Dojo 对动态脚本的支持上。 Dojo 即支持标志变量法,也支持 JSONP 方式。清单 9 是使用标志变量法的动态 script 的例子,在这个例子中使用了 dojo.io.script.get 函数。
清单 9
<html>
<head>
<script src="../dojo/dojo/dojo.js"
djConfig="parseOnLoad: true"></script>
dojo.require("dojo.io.script" );
</head>
<body>
<script>
dojo.io.script.get({
url:"data.php" ,checkStrin
g:"test_01" ,
handle:function(response, ioArgs) {
greetFromServer("hello, world");
}
});
</script>
</body>
</html>
dojo.io.script.get 函数的使用方式和参数是不是与 xhrGet 很相似?只有 checkString 是 xhrGet 所特有的,checkString 正是“标志变量法”的关键,checkString 表示从服务器返回的 javascript 需要定义的变量。清单 10 展示了使用 PHP 编写的服务器端的脚本,它输出了一段 javascript,在这段 javasript 的最后给变量 test_01 赋值。而清单 9 中 dojo.io.script.get 函数的 handle 指向的回调函数又调用了这段 javascript 中定义的函数 greetFromServer() 。只有在 test_01 被赋值后,调用 greetFromServer 才是安全的。
清单 10
<?php
echo "vartest_01;";
echo "function greetFromServer(name) {alert(name)}";
echo "test_01='defined'";
?>
dojo.io.script.get 函数也支持 JSONP 方式,当 dojo.io.script.get 函数的参数对象使用了 callbackParamName 属性时,表示它工作在 JSONP 方式下。 callbackParamName 表示在 url 中添加回调函数名的参数名称,有点拗口,但是看了下面 dojo.io.script.get 函数在页面中动态创建的 script 标签一切就都清楚了,最终出现在 URL 中的是 callbackName,而不是 callbackParamName 。
<script src="data2.php?callbackName=dojo.io.script.jsonp_dojoIoScript1._jsonpCallback" >
清单 11
<html><head>
<script src="../dojo/dojo/dojo.js"
djConfig="parseOnLoad: true"></script>
dojo.require("dojo.io.script" );
</head>
<body>
<script>
dojo.io.script.get({
url:"data2.php",
callbackParamName: "callbackName" ,
handle: function(response, ioArgs) {
document.write(response.greet);
}
});
}
</script>
</body></html>
Dojo 会自动创建一个名为 dojo.io.script.jsonp_dojoIoScript1._jsonpCallback 的 javascript 函数,这个函数其实什么都不做,只是作为一个回调函数传给服务器端程序。 php 的服务器端程序如清单 12 所示,callbackName 像浏览器和服务器之间的一个“信令”,服务器端必须返回对 callbackName 所代表的函数的调用,因为 Dojo 会检查它是否被调用过。
所以服务器端返回的是 dojo.io.script.jsonp_dojoIoScript1._jsonpCallback({greet:’hello, world’}) 。参数 {greet:’hello, world’} 正是要返回到浏览器的 JSON 对象。
清单 12
<?php
$cb = $_GET["callbackName"];
echo $cb."({greet:'hello, world'});";
?>
清单 11 所示程序的输出为:hello, world,由此可以看出,response 参数就是从服务器端返回的 JSON 对象,服务器端的 JSON 对象终于成功的传递到浏览器了。前面介绍了这么多的机制都是为了使这个 JSON 对象安全返回到浏览器中。当然你可以在服务器端返回任何数据,比如直接返回一个字符串,但此时 response 就变成字符串了,当然也就不能再叫 JSONP 了,因为 JSONP 特指返回的是 JSON 对象。
dojo.io.script 对象中除了 get 函数之外,还有 attach,和 remove 两个函数
attach: function(/*String*/id, /*String*/url)
创建动态 script 标签,标签的 id 由参数 id 指定,src 由 url 指定。
remove: function(/*String*/id)
删除 id 代表 script 标签。
总结
本文介绍了 Dojo 中三种浏览器与服务器交互的方式,这三种方式各有优缺点,但是在使用方式却出奇的一致; xhr 框架的函数,dojo.io.iframe、dojo.io.script 对象的函数使用的 JSON 对象参数也极其相似,而且浅显易懂。 Dojo 设计者的这一良好设计极大的减轻了开发人员的学习负担,作为框架开发人员应该了解这一理念。表 2 对这三种方式从三个方面进行了比较。
表 2. 三种方式的比较
支持的 HTTP 请求类型 期望的输出 跨域访问
XHR Get, post, delete, put text, json, xml, javascript … N
iframe Get, post html N
script Get javascript Y
综上所述,使用上述三种方法时需要遵循一条简单的原则:传送文件则 iframe,跨域访问则使用动态脚本,其余则选 XHR 框架。