深入了解 Dojo 的核心接口

时间:2022-10-13 17:37:03

转载自:http://www.ibm.com/developerworks/cn/web/1303_zhouxiang_dojocore/

Dojo 的这些接口大大简化了我们的 Web 前端开发的复杂度,使得我们能够在更短的时间内实现功能更为丰富的应用。这篇文章将重点介绍 Dojo 的核心接口所带给 Web 开发工程师们的各种便利以及它的一些使用技巧。

Dojo 核心接口简介

Dojo 的核心接口主要位于 Dojo 的三大库(“dojo”,“dijit”和“dojox”)中的“dojo”包里,它包括了日常 Web 开发中最为通用的一些组件和模块,覆盖面也非常广泛,囊括了 AJAX、DOM 操作,面向对象模式开发、事件、Deferred、数据(data stores)、拖拽(DND)和国际化组件等等。这些通用组件中用很多非常强大实用的核心接口,能给我们的日常 Web 开发带来相当大的便利。本文我们会深入详细的介绍这些核心接口(文中代码示例主要基于 Dojo 的 1.7 版本及以后)。

鉴于 Dojo 的核心接口比较复杂,内容比较多,所以,本文将 Dojo 的核心接口分为两个部分介绍:核心基础接口和核心功能接口。我们先来了解一下 Dojo 的核心基础接口。

核心基础接口

核心基础接口是 Dojo 中最为通用的一组接口,但凡基于 Dojo 开发的 Web 基本都会涉及到这些接口。

Kernel 接口 (dojo/_base/kernel)

Kernal 组件包含了 Dojo 核心里面的最基本的一些特性,这个组件往往不是由开发人员直接引入的,而是通过引入其它的某些常用核心组件时间接引入的。


清单 1. Kernel 代码示例
  
require(["dojo/_base/kernel"], function(kernel){
kernel.deprecated("dijit.layout.SplitContainer",
"Use dijit.layout.BorderContainer instead", "2.0");

kernel.experimental("acme.MyClass");

var currentLocale = kernel.locale;

query(".info").attr("innerHTML", kernel.version);
});

首先,Kernel 的 deprecated 方法,用于在控制台输出相关警告信息,如某些包或方法被删除、修改了或者用户正在使用一个方法老版本等等。通过 deprecated 输出的信息只有的 dojoConfig 里设置了“isDebug”为 true 时才会输出到控制台。

然后,experimental 方法,同 deprecated 一样,不过它主要用来提示用户某些模块或者方法是 experimental 的,请谨慎使用。

最后,是“locale”和“version”属性,分别表示国际化信息和 Dojo 的版本信息。

Kernel 还有一个 global 的属性,在浏览器中就是 window 对象的一个别名:


清单 2. Kernel 的 global 代码示例
  
require(["dojo/_base/kernel", "dojo/on"], function(kernel, on){
on(kernel.global, "click", function(e){
console.log("clicked: ", e.target);
});
});

清单 2 的操作就相当于给 window 对象绑上了一个“click”事件。

Dojo.config 接口 (dojo/_base/config)

Config 接口应该是我们最为熟悉不过的接口了,在我们引入 Dojo 的时候都会先做一些全局的配置,所使用的就是 Dojo 的 Config 接口。


清单 3. Config 基本代码示例
  
// 通过“data-dojo-config”属性配置
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Dojo dojo.config Tutorial</title>
<script type="text/javascript"
src="//ajax.googleapis.com/ajax/libs/dojo/1.8/dojo/dojo.js"
data-dojo-config="parseOnLoad: true, isDebug: true"></script>
</head>
<body>
<p>...</p>
</body>
</html>


// 通过 JavaScript 变量配置
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Dojo dojoConfig Tutorial</title>
<script type="text/javascript">
var dojoConfig = {
parseOnLoad: true,
isDebug: true,
locale: 'en-us',
extraLocale: ['ja-jp']
};
</script>

<script type="text/javascript"
src="//ajax.googleapis.com/ajax/libs/dojo/1.8/dojo/dojo.js">
</script>
</head>
<body>
<p>...</p>
</body>
</html>

可见,配置 Config 可以通过两种方式,“data-dojo-config”属性或者“dojoConfig ”变量,其效果是一样的。

接下来我们来深入了解一下这些配置参数的含义:

isDebug:设为“true”则会进入 Dojo 的 debug 模式,这时不管您用什么浏览器,您都会看到 Dojo 的扩展调试控制台,并能够在其中键入和运行任意您想运行的代码。同时,您也能通过 Dojo 自带的 Console 工具输出调试信息。如果是 debug 模式,您还能够看到很多其它的调试信息,如:通过“deprecated”或者“experimental”等输出的警告信息。

debugContainerId:指定页面上某元素的 ID,Dojo 会将扩展调试控制台放在该元素里。

locale:Dojo 会根据浏览器的语言环境决定 locale 的值,但是这里我们也可以强制指定。

addOnLoad:功能同“dojo.ready()”,“dojo.addOnLoad”。使用方式如下:

djConfig.addOnLoad = [myObject, "functionName"] 或者 djConfig.addOnLoad = [myObject, function(){}];

require:当“dojo.js”加载完后,会马上加载 require 设定的模块。

An array of module names to be loaded immediately after dojo.js has been included in a page.

dojoBlankHtmlUrl:默认值为“dojo/resources/blank.html”,在我们使用 dojo.io.iframe、dojo.back 或者 dojox 的一些跨域 Ajax 请求组件时,需要临时创建一个空的页面供 iframe 使用,它会通过 dojoBlankHtmlUrl 的值去查找这个空白页面。用户可以把它设定成您想要的任何路径。

useCustomLogger:是否使用自定义的 console。

transparentColor:定义 dojo.Color 中的透明色,默认为 [255,255,255]。

defaultDuration:设定动画默认的持续时间(dijit.defaultDuration)

以上是 dojoConfig 的配置参数,我们也可以加入我们自己的自定义参数,供我们的应用程序使用:


清单 4. Config 自定义参数代码示例
  
var dojoConfig = { parseOnLoad:true, myCustomVariable:true}

require(["dojo/_base/declare", "dojo/_base/config"], function(declare, config){
declare("my.Thinger", null, {
thingerColor: (config.myCustomVariable ? "wasTrue" : "wasFalse"),
constructor: function(){
if(config.myCustomVariable){ ... }
}
});
});

可见,这里我们只要在 dojoConfig 中加入我们的自定义变量“myCustomVariable”,便可以在后面的应用中通过 config.myCustomVariable 使用。

我们还能够通过“has”参数来开启或关闭某些功能:


清单 5. Config 的 has 代码示例
  
<script>
dojoConfig = {
has: {
"dojo-firebug": true,
"dojo-debug-messages": true,
"dojo-amd-factory-scan": false
}
};
</script>

可以看到,这里我们开启了 Dojo 的默认调试工具和“deprecated”,“experimental”的消息显示,但是关闭了 AMD 模块扫描的功能。

最后,我们来看看如何通过 Dojo 的 Config 来重定位模块:


清单 6. Config 的模块重定位代码示例
  
<script>
dojoConfig = {
has: {
"dojo-firebug": true,
"dojo-debug-messages": true
},
// 不解析页面上的 widgets
parseOnLoad: false,
packages: [
// demo 包下的模块都会定位到"/documentation/tutorials/1.7/dojo_config/demo"
{
name: "demo",
location: "/documentation/tutorials/1.7/dojo_config/demo"
}
],
// 模块加载时间不得超过 10 秒,否则超时
waitSeconds: 10,
aliases: [
// 以"ready"作为"dojo/domReady"模块的别名
["ready", "dojo/domReady"]
],
// 不缓存
cacheBust: true
};
</script>

从清单 6 中可以看到,这里我们可以通过“packages”参数,设置模块与实际位置的对应表,我们也可以通过“paths”参数来定义不同地址之间的对应表。aliases 也是一个非常有用的参数:设置模块或文件的别名。当我们的项目中某些模块需要调整路径,但又不想去影响那些正在使用该模块的应用单元时,可以通过别名,做到无缝升级。还有很多参数,如:“cacheBust”和“waitSeconds”等等,也都是比较有用的属性,读者可以关注一下。

Dojo 的 loader 相关接口

Dojo 的 loader 模块专门用于 Dojo 组件的加载,它包含了各种加载组件的方法。

首先我们来看看“dojo/ready”相关接口:


清单 7. Loader 的 dojo/ready 模块代码示例
  
// 方法 1:
require(["dojo/ready", "dojo/dom", "dojo/parser", "dijit/form/Button"],
function(ready, dom){
ready(80, window, function(){
dom.byId("myWidget").innerHTML = "A different label!";
});
});


// 方法 2:
require(["dojo/domReady!"], function(){
// DOM 加载完毕后开始执行
if(dayOfWeek == "Sunday"){
document.musicPrefs.other.value = "Afrobeat";
}
});

方法 1 可能会让有些读者觉得别扭,因为我们在使用“ready”方法时,大都只传入了一个回调函数。事实上“ready”方法可以接受三个参数:ready(priority,context,callback),只是我们通常只传入了第三个参数而已。priority 表示优先级,这里默认是 1000,如果我们传入了 80,则表示回调函数会在 DOM 加载完成但是“dojo/parser”未完成时触发。context 这里表示设定的回调方法的上下文。当然,我们也可以使用方法 2 的方式,这样也可以省去很多代码。

再来看看“dojo/_base/unload”模块相关接口,它也是 loader 模块的一员,先来看看“addOnUnload ”方法。该方法基于“window.onbeforeunload”而触发,由于它是在页面卸载之前执行的,所以这个时候页面的 document 等等对象并没有被销毁,所以,这个时候,我们还是可以执行一些 DOM 操作或者访问 JavaScript 对象属性的:


清单 8. Loader 的 dojo/_base/unload 模块代码示例
  
require(['dojo/_base/unload','dojo/_base/xhr'], function(baseUnload, xhr){
baseUnload.addOnUnload(function(){
console.log("unloading...");
alert("unloading...");
});

baseUnload.addOnUnload(function(){
// unload 时尽量使用同步 AJAX 请求
xhr("POST",{
url: location.href,
sync: true,
handleAs: "text",
content:{
param1:1
},
load:function(result){
console.log(result);
}
});
});

// 同样,也可以绑定对象的方法
window.unLoad=function(){
console.log("an unload function");
return "This is a message that will appear on unLoad.";
};
baseUnload.addOnUnload(window, "unLoad");
});

注意,我们是可以添加多个“unload”回调函数的。

再来看看“addOnWindowUnload”,该方法基于“window.onunload”而触发,所以,这个时候强烈不建议读者在回调函数中执行一些 DOM 操作或者访问 JavaScript 对象属性,因为这个时候这些 DOM 和 JavaScript 对象很可能已经不可用了。

Dojo 的 loader 模块还包含了很多向下兼容的接口,如我们再熟悉不过的“dojo.provide”、“dojo.require”、“dojo.requireIf”、“dojo.platformRequire”、“dojo.declare”、“dojo.registerModulePath”(新方式:require({paths:...}) 或者 config 中的 paths 配置参数)等等。与这些接口对应的就是 AMD 接口了,AMD 是在“dojo.js”中定义的用于支持异步的模块定义和加载,主要是“define”和“require”接口。先来看看“require”接口,其实用方式是非常简单的。


清单 9. AMD 的 require 代码示例
  
require(
configuration, // 配置参数,如 paths:["myapp", "../../js/com/myapp"]
dependencies, // 请求加载的模块(Module)的 ID
callback // 模块加载完成后的回调函数
) -> 返回值为 undefined


// 使用示例 1
require([ "my/app", "dojo" ], function(app, dojo){
// 您的代码
});

// 使用示例 2
require(
moduleId // 模块的 ID(字符串)
) -> any

// 使用示例 3
require(["http://acmecorp.com/stuff.js"], function(){
// 简单地执行 stuff.js 的代码
});

可见,require 接口使用起来很方便,通常我们也主要按照示例 1 的方式使用,示例 2 也是一种使用方式,它主要通过传入模块的 ID 来返回这个模块,但是这种模式需要保证该模块已经被定义并加载了。使用示例 3 展示了加载远程非模块脚本的方式。

同样,定义 AMD 模块也是非常简单的:


清单 10. AMD 的 define 代码示例
  
define(
moduleId, // 定义模块的 ID
dependencies, // 定义预加载的模块
factory // 模块的内容,或者一个会返回模块内容的函数
)

// 使用示例 1
define(
["dijit/layout/TabContainer", "bd/widgets/stateButton"],
definedValue
);


// 使用示例 2
define(
["dijit/layout/TabContainer", "bd/widgets/stateButton"],
function(TabContainer, stateButton){
// do something with TabContainer and stateButton...
return definedValue;
}
);

使用示例 1 展示了最为简单的模块的内容,使用示例 2 是我们通常使用的函数返回模块的内容。对于简单的模块定义,可以选择示例 1 的方式,但是一般情况下,还是建议大家多使用示例 2 的方式构建自己的自定义模块。

AMD 还包括一些小工具:


清单 11. AMD 的小工具代码示例
  
// 模块路径到实际路径的转化
require.toUrl(
id // 模块的 ID 或者以模块 ID 做前缀的资源的标识
) -> 返回具体路径(字符串)

define(["require", ...], function(require, ...){
... require.toUrl("./images/foo.jpg") ...
}


// 相对模块 ID ---> 绝对模块 ID
require.toAbsMid(
moduleId // 模块 ID
) -> 绝对模块 ID(字符串)


// 注销模块
require.undef(
moduleId // 模块 ID
)


// 输出日志
require.log(
// 日志内容
)

可见,AMD 的这些小工具十分使用,建议读者们关注一下。

Dojo 的 AMD 的 loader 还有关于事件的接口,它能监听并响应一些 loader 特有的事件,如:错误消息、配置的变化、跟踪的记录等等:


清单 12. AMD 的微事件代码示例
  
require.on = function(
eventName, // 事件名
listener // 触发函数
)

var handle = require.on("config", function(config, rawConfig){
if(config.myApp.myConfigVar){
// config 发生变化时,处理相关事务
}
});


// 错误事件
function handleError(error){
console.log(error.src, error.id);
}
require.on("error", handleError);

清单 12 中展示了 loader 的微事件接口说明以及相应的使用示例,这些内容容易被我们忽视,但其实在某些情况下往往能派上大用场,希望读者好好关注一下。

接下来我们来看几个 loader 的插件,首先是 i18n 插件,它专门用于加载和使用国际化文件:


清单 13. Loader 的 i18n 插件
  
require(["dojo/i18n!../../_static/dijit/nls/common.js",
"dojo/dom-construct", "dojo/domReady!"],
function(common, domConst){
domConst.place("<ul>"
+ "<li> buttonOk: " + common.buttonOk + "</li>"
+ "<li> buttonCancel: " + common.buttonCancel + "</li>"
+ "<li> buttonSave: " + common.buttonSave + "</li>"
+ "<li> itemClose: " + common.itemClose + "</li></ul>",
"output"
);
});


define({
root: {
buttonOk : "OK",
buttonCancel : "Cancel"
........
}
de: true,
"de-at" : true
});

国际化文件的使用方式很简单,文件路径前加上“dojo/i18n!”前缀即可。

同样,“dojo/text”插件也是如此,不同的是,它用来加载文件内容:


清单 14. Loader 的 text 插件
  
define(["dojo/_base/declare", "dijit/_Widget",
"dojo/text!dijit/templates/Dialog.html"],
function(declare, _Widget, template){
return declare(_Widget, {
templateString: template
});
});

require(["dojo/text!/dojo/helloworld.html",
"dojo/dom-construct", "dojo/domReady!"],
function(helloworld, domConst){
domConst.place(helloworld, "output");
});

清单 14 列举了“dojo/text”插件使用模式,同 i18n 插件基本类似,但是它所返回的变量不是一个对象,而是文件内容的字符串。

再来看看“dojo/has”插件,这个插件在我们的日常开发中应该是使用得非常频繁的:特性检测。它主要是用于检测某些功能是否可用,或者说该功能是否已经被加载并就绪。您甚至可以在 require 的时候进行条件选择加载:


清单 15. Loader 的 has 插件
  
require(["dojo/has", "dojo/has!touch?dojo/touch:dojo/mouse", "dojo/dom",
"dojo/domReady!"],
function(has, hid, dom){
if(has("touch")){
dom.byId("output").innerHTML = "You have a touch capable device and
so I loaded <code>dojo/touch</code>.";
}else{
dom.byId("output").innerHTML = "You do not have a touch capable device and
so I loaded <code>dojo/mouse</code>.";
}
});

这里我们看到了“dojo/has!touch?dojo/touch:dojo/mouse”,这就是我们说的条件选择加载,如果 touch 为 true(程序运行于触摸屏的设备上),则加载“dojo/touch”模块,否则加载“dojo/mouse”模块。同样,回调函数里也是通过 has("touch") 来判断。

除了“dojo/has”,“dojo/sniff”也属于其中之一,它主要是检测浏览器的相关特性。


清单 16. 特性检测的 sniff 模块
  
require(["dojo/has", "dojo/sniff"], function(has){
if(has("ie")){
// IE 浏览器特殊处理
}
if(sniff("ie"){
// IE 浏览器特殊处理
});
if(has("ie") <= 6){
// IE6 及之前版本
}
if(has("ff") < 3){
// Firefox3 之前版本
}
if(has("ie") == 7){
// IE7
}
});

这里可以通过 sniff 对象做检测,同样也能通过 has 对象做检测。

还有一个“dojo/node”插件,它专门用来在 Dojo 中加载 Node.js 的模块,使用方式同 i18n 和 text 插件类似,这里不做进一步讨论。

基础 lang 相关接口

“dojo/base/lang”包含了很多实用的基础接口,如果我们使用同步加载的老方式“async: false”,该模块会自动加载。如果是异步,需要显示引用:


清单 17. 引用 lang 模块
  
require(["dojo/_base/lang"], function(lang){
// 引入 lang 模块
});

接下来我们看看它主要的功能接口,首先是 clone 接口,用于克隆对象或数组,使用方式如下:


清单 18. 模块 lang 的 clone 接口
  
require(["dojo/_base/lang", "dojo/dom", "dojo/dom-attr"],
function(lang, dom, attr){
// 克隆对象
var obj = { a:"b", c:"d" };
var thing = lang.clone(obj);
// 克隆数组
var newarray = lang.clone(["a", "b", "c"]);
// 克隆节点
var node = dom.byId("someNode");
var newnode = lang.clone(node);
attr.set(newnode, "id", "someNewId");
});

可见,这里的克隆接口使用方式很简单,但是要注意:这个接口在 Web 的日常开发中需要引起重视,JavaScript 对数组和对象的操作通常是传递引用,同一个对象赋值给不同的变量,其实还是指向的同一个对象。如果有两段不同的逻辑需要操作这个对象,仅仅用两个变量是不可行的!我们需要做一个 clone,才能避免由于操作同一个对象而产生的错误。

再来看看 delegate 接口:


清单 19. 模块 lang 的 delegate 接口
  
require(["dojo/_base/lang", function(lang){
var anOldObject = { bar: "baz" };
var myNewObject = lang.delegate(anOldObject, { thud: "xyzzy"});
myNewObject.bar == "baz"; // 代理 anOldObject 对象
anOldObject.thud == undefined; // thud 只是代理对象的成员
myNewObject.thud == "xyzzy"; // thud 只是代理对象的成员
anOldObject.bar = "thonk";
myNewObject.bar == "thonk"; // 随着 anOldObject 属性的改变,myNewObject 属性随之改变,
这就是代理,它永远只是被代理对象的一个引用
});

相信读者参见清单 19 的代码注释就能完全明白了。接下来我们在看一个接口:replace,主要用于字符串替代。其实我们知道,JavaScript 本身有这种基础接口,但是,模块 lang 提供的接口更为强大:


清单 20. 模块 lang 的 replace 接口
  
require(["dojo/_base/array", "dojo/_base/lang", "dojo/dom", "dojo/domReady!"],
function(array, lang, dom){
function sum(a){
var t = 0;
array.forEach(a, function(x){ t += x; });
return t;
}

dom.byId("output").innerHTML = lang.replace(
"{count} payments averaging {avg} USD per payment.",
lang.hitch(
{ payments: [11, 16, 12] },
function(_, key){
switch(key){
case "count": return this.payments.length;
case "min": return Math.min.apply(Math, this.payments);
case "max": return Math.max.apply(Math, this.payments);
case "sum": return sum(this.payments);
case "avg": return sum(this.payments) / this.payments.length;
}
}
)
);

});

这里同我们常用的 replace 方法不一样,它的第二个参数是一个函数,也就是说,我们能通过函数的方式实现一些我们自定义的复杂的转换逻辑。

接下来还有 extend、mixin、exists、getObject、setObject、hitch、partial 等接口,这些接口我们再熟悉不过了,不再深入,但是这里提醒大家注意,extend 和 mixin 很类似,区别主要在于 extend 主要操作 prototype,而 mixin 只针对对象。hitch 和 partial 的区别主要在于函数执行的上下文。

另外,以下函数也非常实用,希望大家重视:

isString():判断是否为字符串。

isArray():判断是否为数组。

isFunction():判断是否为函数对象。

isObject():判断是否为对象。

isArrayLike():判断是否为数组。

isAlien():判断是否为 JavaScript 基础函数。

declare 接口

这个接口大家应该在熟悉不过了,它的功能和老版本 Dojo 中的“dojo.declare”类似,主要用于声明和定义“类”,只是使用方式稍微有所改变:


清单 21. declare 接口
  
define(["dojo/_base/declare"], function(declare){
var VanillaSoftServe = declare(null, {
constructor: function(){
console.debug ("adding soft serve");
}
});

var OreoMixin = declare(null, {
constructor: function(){
console.debug("mixing in oreos");
},
kind: "plain"
});

var CookieDoughMixin = declare(null, {
constructor: function(){
console.debug("mixing in cookie dough");
},
chunkSize: "medium"
});
};

return declare([VanillaSoftServe, OreoMixin, CookieDoughMixin], {
constructor: function(){
console.debug("A blizzard with " +
this.kind + " oreos and " +
this.chunkSize + "-sized chunks of cookie dough."
);
}
});
});

这是一个简单的多继承示例,通过“return declare([VanillaSoftServe, OreoMixin, CookieDoughMixin]......;”来返回我们所定义的类(widget),然后在其它的地方通过“require”或者“define”来指明变量引用该类(widget)。数组里面的对象“[VanillaSoftServe, OreoMixin, CookieDoughMixin]”是该自定义类的基类。需要强调一点,这里的 declare 省略了第一个变量:类的名称,即:“declare("pkg.MyClassName", [VanillaSoftServe, OreoMixin, CookieDoughMixin]......;”,

如果设定这第一个变量,它会将这个字符串存储到该类的“declaredClass”变量中,同时会将"pkg.MyClassName"作为一个全局的变量用于今后方便构建该类的对象。

还有一个接口需要强调一下:safeMixin(),这是 dojo/declare 里面定义的一个接口方法,专门用于在已有类中加入额外的方法。它的功能和 lang.mixin 相同,但是它除了做方法或者属性的合并外,还能保证并入的方法与 declare 定义的类的兼容。参考如下示例:


清单 22. declare 的 safeMixin 接口
  
require(["dojo/_base/declare", "dojo/_base/lang"], function(declare, lang){
var A = declare(null, {
m1: function(){ /*...*/ },
m2: function(){ /*...*/ },
m3: function(){ /*...*/ },
m4: function(){ /*...*/ },
m5: function(){ /*...*/ }
});

var B = declare(A, {
m1: function(){
// declare 方法也保证了其中定义法的方法与类本身兼容,如我们可以直接调用
// this.inherited(arguments)
return this.inherited(arguments); // 调用 A.m1
}
});

B.extend({
m2: function(){
// 类的 extend 方法也保证了其中定义法的方法与类本身兼容
return this.inherited(arguments); // 调用 A.m2
}
});

lang.extend(B, {
m3: function(){
// lang 的 extend 方法不能保证其中定义法的方法与类本身兼容,所以要加入方法“m3”本身
return this.inherited("m3", arguments); // 调用 A.m3
});

var x = new B();

declare.safeMixin(x, {
m4: function(){
// declare 的 safeMixin 能保证其中定义法的方法与类本身兼容
return this.inherited(arguments); // 调用 A.m4
}
});


lang.mixin(x, {
m5: function(){
// 普通的 mixin 不能保证兼容
return this.inherited("m5", arguments); // 调用 A.m5
});
});

读者们可以参考清单 22 代码中的注释,并重点关注一下“declare.safeMixin”方法,千万别和普通的 lang.mixin 方法混淆。

介绍完了 Dojo 的核心基础接口,我们应该对 Dojo 的核心接口有个大概的印象了。这些基础接口看似功能简单,但却是我们日常 Web 开发中必不可少的一部分,尤其是对于开发复杂的 RIA 富客户端应用来说,这些接口便显得更加重要了。

接下来我们要开始了解 Dojo 的核心功能接口了,这里不同于核心基础接口,我们会把重点放在功能上面。您会看到很多 Dojo 封装好的功能强大并实用的类对象以及它们的接口,这些接口会帮我们解决很多我们日常 Web 开发中碰到的难题,从而大大加速我们的开发效率。

核心功能接口

了解了 Dojo 的核心基础接口,我们可以转入 Dojo 的核心功能接口了。Dojo 包括了大量的强大的核心功能,这些功能给我们的日常开发带来了相当多的帮助和便利。但是正是由于 Dojo 如此的完善和丰富,导致很多读者在使用过程中无暇顾及它所有的方面,很多非常实用的接口至今还不被大多数人所知晓和熟悉。接下来,我们会略过大家都比较熟悉的一些功能接口(如 forEach,addClass 等等),而选出一些非常实用但又有可能被读者们忽视的核心接口,深入介绍,希望读者们能有所收获。

Deferreds 和 Promises

Deferreds 和 Promises 主要的目的在于让用户更为方便的开发异步调用的程序,该核心功能接口中包含了很多用于管理异步调用机器回调函数的接口,使用起来非常简单,对开发 Web2.0 应用的帮助也非常大。

先来看看 dojo/promise/Promise,这其实是一个抽象的基类,我们熟悉的 dojo/Deferred 类其实是它的一个具体的实现。


清单 23. dojo/promise/Promise 抽象基类实现
  
define(["dojo/promise/Promise", "dojo/_base/declare"], function(Promise, declare){
return declare([Promise], {
then: function(){
// 实现 .then() 方法
},
cancel: function(){
// 实现 .cancel() 方法
},
isResolved: function(){
// 实现 .isResolved() 方法
},
isRejected: function(){
// 实现 .isRejected() 方法
},
isFulfilled: function(){
// 实现 .isFulfilled() 方法
},
isCanceled: function(){
// 实现 .isCanceled() 方法
}
});
});

这里我们加入这段示例代码的目的是让读者们对 Promise 可用的接口有一个整体的认识,后面我们会用不同的示例来分别详细介绍这些接口。

之前我们介绍了,dojo/Deferred 类是 dojo/promise/Promise 的一个具体的实现,那么基于 dojo/Deferred 我们肯定可以实现异步调用的管理。这里我们用 setTimeout 来模拟异步调用,参见如下接口:


清单 24. dojo/Deferred 的简单使用
  
require(["dojo/Deferred", "dojo/dom", "dojo/on", "dojo/domReady!"],
function(Deferred, dom, on){
function asyncProcess(){
var deferred = new Deferred();

dom.byId("output").innerHTML = "I'm running...";

setTimeout(function(){
deferred.resolve("success");
}, 1000);

return deferred.promise;
}

on(dom.byId("startButton"), "click", function(){
var process = asyncProcess();
process.then(function(results){
dom.byId("output").innerHTML = "I'm finished, and the result was: " + results;
});

});

});

这里我们先构建了一个 dojo/Deferred 对象:“var deferred = new Deferred()”,然后在 asyncProcess 的末尾返回了 deferred.promise 对象。在后面的脚本中,我们使用了这个返回的 promise 对象的 then 方法:"process.then(function(results){...}"。好了,这里要注意了,then 方法是这个 promise 的关键,它是由之前的“deferred.resolve”这个方法触发的,也就是说,当 deferred 对象的 resolve 方法调用的时候,就会触发 deferred.promise 对象的 then 方法,这个 then 方法会调用传给它的回调函数,就是我们代码中最后面看到的“function(results){...}”。这就构成了一个异步调用的管理。试想这样一个场景,这里我们不是 setTimeout,而是一个异步向后台取数据的 AJAX 请求,而我们又不知道当我们点击“startButton”时数据是否返回,所以这里使用 Deferred 和 Promise 是再为合适不过了。

dojo/Deferred 不仅能处理正常返回的情况,也能处理进行中和出错等情况,参见代码如下:


清单 25. dojo/Deferred 的进阶使用
  
require(["dojo/Deferred", "dojo/dom", "dojo/on", "dojo/domReady!"],
function(Deferred, dom, on){
function asyncProcess(msg){
var deferred = new Deferred();

dom.byId("output").innerHTML += "<br/>I'm running...";

setTimeout(function(){
deferred.progress("halfway");
}, 1000);

setTimeout(function(){
deferred.resolve("finished");
}, 2000);

setTimeout(function(){
deferred.reject("ooops");
}, 1500);

return deferred.promise;
}

on(dom.byId("startButton"), "click", function(){
var process = asyncProcess();
process.then(function(results){
dom.byId("output").innerHTML += "<br/>I'm finished, and the result was: " +
results;
}, function(err){
dom.byId("output").innerHTML += "<br/>I errored out with: " + err;
}, function(progress){
dom.byId("output").innerHTML += "<br/>I made some progress: " + progress;
});

});
});

很明显,这里除了 resolve 方法,还有 progress 和 reject。progress 代表进行中,reject 代表出问题,同样,then 方法中,根据参数顺序分别是:resolve 的回调函数,reject 的回调函数,progress 的回调函数。我们可以根据需要做相应的回调处理。

接下来我们来看看 dojo/promise/all,它取代了原先 dojo/DeferredList ,相信熟悉 DeferredList 的读者应该知道它的主要功能了。

dojo/promise/all 同 DeferredList 一样,主要为了处理多个异步请求的情况。假如我们初始化时需要向后台的多个服务发起异步请求,并且我们只关心最迟返回的请求,返回后然后再做相应处理。面对这种情况,dojo/promise/all 是我们的不二选择。


清单 26. dojo/promise/all 的使用
  
require(["dojo/promise/all", "dojo/Deferred", "dojo/dom", "dojo/on", "dojo/json",
"dojo/domReady!"],
function(all, Deferred, dom, on, JSON){

function googleRequest(){
var deferred = new Deferred();
setTimeout(function(){
deferred.resolve("foo");
}, 500);
return deferred.promise;
}

function bingRequest(){
var deferred = new Deferred();
setTimeout(function(){
deferred.resolve("bar");
}, 750);
return deferred.promise;
}

function baiduRequest(){
var deferred = new Deferred();
setTimeout(function(){
deferred.resolve("baz");
}, 1000);
return deferred.promise;
}

on(dom.byId("startButton"), "click", function(){
dom.byId("output").innerHTML = "Running...";
all([googleRequest(), bingRequest(), baiduRequest()]).then(function(results){
dom.byId("output").innerHTML = JSON.stringify(results);
});
});

});

这里我们同样还是用 setTimeout 来模拟异步调用,注意最后的“all([googleRequest(), bingRequest(), baiduRequest()]).then(function(results){......}”,这里的 then 就是等待着三个异步调用全部返回的时候才触发的,并且回调函数里的传入的实参是这三个异步调用返回值的共和体。

还有一个类似的处理多个异步调用的类是:dojo/promise/first,它的原理和 dojo/promise/all 正好相反,它自关注第一个返回的请求:


清单 27. dojo/promise/first 的使用
  
require(["dojo/promise/first", "dojo/Deferred", "dojo/dom", "dojo/on", "dojo/json",
"dojo/domReady!"],
function(first, Deferred, dom, on, JSON){

function googleRequest(){
var deferred = new Deferred();
setTimeout(function(){
deferred.resolve("foo");
}, 500);
return deferred.promise;
}

function bingRequest(){
var deferred = new Deferred();
setTimeout(function(){
deferred.resolve("bar");
}, 750);
return deferred.promise;
}

function baiduRequest(){
var deferred = new Deferred();
setTimeout(function(){
deferred.resolve("baz");
}, 1000);
return deferred.promise;
}

on(dom.byId("startButton"), "click", function(){
dom.byId("output").innerHTML = "Running...";
first([googleRequest(), bingRequest(), baiduRequest()]).then(function(result){
dom.byId("output").innerHTML = JSON.stringify(result);
});
});

});

读者可以看到,这里代码和之前的 dojo/promise/all 示例的代码几乎相同,区别只是:这里是 first,并且回调函数的实参“result”只是这三个异步请求中最早返回的那个异步请求的返回值,有可能是 googleRequest,bingRequest 和 baiduRequest 中的任意一个。

最后我们来看看 dojo/when,它的出现主要用于同时处理同步和异步的请求。设想您并不确定某些方式是否一定是执行了异步调用,并返回了 promise 对象,那么这个时候,then 方法在这里就不可行了。因为如果该函数由于传入参数的不同而执行了同步请求,或者根本没有执行任何请求,并且只返回了一个数值,而不是一个 promise 对象,那么 then 方法在这里是根本不能用的。但是没关系,我们还有 dojo/when:


清单 28. dojo/when 的使用
  
require(["dojo/when", "dojo/Deferred", "dojo/dom", "dojo/on", "dojo/domReady!"],
function(when, Deferred, dom, on){
function asyncProcess(){
var deferred = new Deferred();

setTimeout(function(){
deferred.resolve("async");
}, 1000);

return deferred.promise;
}

function syncProcess(){
return "sync";
}

function outputValue(value){
dom.byId("output").innerHTML += "<br/>completed with value: " + value;
}

on(dom.byId("startButton"), "click", function(){
when(asyncProcess(), outputValue);
when(syncProcess(), outputValue);

});

});

注意,其实 dojo/when 在这里的作用同之前的 promise 是类似的,asyncProcess 如果正确返回,则会执行后面的 outputValue 函数。但是同时,它也支持 syncProcess,即只返回数值的情况,数值返回后,它同样会执行后面的 outputValue。这么一来,我们的代码将会变得非常简单,我们不用再为处理各种不同返回值的情况而增加大量的额外的代码,dojo/when 已经帮我们全部考虑了。

Events 和 Connections

事件处理也是我们日常开发中必不可少的一个环节,Dojo 在这方面也是不断的优化和推陈出新,希望能提供给开发者们一个强大且使用方便的事件处理组件。

我们先来看看 dojo/on,这是新版 Dojo 主推的一个事件处理接口,它不仅包含了 Dojo 之前版本的所有功能,还提供了很多新的接口,无论从使用的便利性还是从性能上都大大优于之前。


清单 29. dojo/on 的简单使用
  
require(["dojo/on", "dojo/_base/window"], function(on, win){
var signal = on(win.doc, "click", function(){
// 解除监听
signal.remove();
// TODO
});
});

可见,绑定事件非常简单,解除绑定也只需 remove 即可。

再来看看 emit() 方法,该方法用于触发事件,类似 fireEvent() 的功能。


清单 30. dojo/on 的 emit 方法
  
require(["dojo/on"], function(on){
on(target, "event", function(e){
// 事件处理代码
});

on.emit(target, "event", {
bubbles: true,
cancelable: true
});
});

可以看到,这里我们通过 emit 触发之前绑定的事件,bubbles 这里表示事件按照正常顺序触发,即从底向上。先是元素本身,然后是其父层节点,最后一直到整个页面的顶层根节点(除非在这之间有 listener 调用 event.stopPropagation())。cancelable 表示该事件是可以被 cancel 的,只要有 listener 调用 event.preventDefault() 便会 cancel 该 event 的事件链。

接下来我们看几个高级用法,首先是 pausable 接口,该接口用于建立可暂停的事件监听器:


清单 31. dojo/on 的 pausable 接口
  
require(["dojo/on"], function(on){
var buttonHandler = on.pausable(button, "click", clickHandler);

on(disablingButton, "click", function(){
buttonHandler.pause();
});

on(enablingButton, "click", function(){
buttonHandler.resume();
});
});

很明显,pausable 的使用方式同 on 基本一样,不同的是它的返回值“buttonHandler”有“pause”和“resume”这两个方法。“buttonHandler.pause()”会保证之前的 listener 不会被触发,而“buttonHandler.resume()”会恢复 listener 的功能。

同样,once 也是一个很实用的接口,该接口保证绑定的事件只会被触发一次,之后就会自动解除事件的监听:


清单 32. dojo/on 的 once 接口
  
require(["dojo/on"], function(on){
on.once(finishedButton, "click", function(){
// 只触发一次 ...
});
});

可见,其使用方式同 on。

多事件监听也是 Dojo 的事件机制里面比较有特点的一个功能,它可以将多个事件直接绑定到同一个方法:


清单 33. dojo/on 的多事件监听
  
require("dojo/on", function(on){
on(element, "dblclick, touchend", function(e){
// 判断具体触发了哪个事件,并作出相应处理
});
});

这种模式不仅可以节省大量代码,也便于我们管理多事件。

dojo/on 的事件代理功能也是值得我们关注的特性之一,它能够通过 CSS 的 Selector 去定位元素并绑定事件,这是的我们能够非常方便的批量绑定和和处理事件:


清单 34. dojo/on 的事件代理
  
require(["dojo/on", "dojo/_base/window", "dojo/query"], function(on, win){
on(win.doc, ".myClass:click", clickHandler);
});

on(document, "dblclick, button.myClass:click", clickHandler);

require(["dojo/on", "dojo/query"], function(on){
on(myTable, "tr:click", function(evt){
console.log("Clicked on node ", evt.target, " in table row ", this);
});
});

清单 34 中可以看出,我们能够通过诸如“<selector>:<eventType>”的方式定位元素并绑定事件,如".myClass:click",即所有 Class 属性中包含 myClass 的节点,同样,它也支持多事件绑定:on(document, "dblclick, button.myClass:click", clickHandler),该行代码表示绑定 document 的 dblclick 事件,以及绑定其下所有子节点中标签为“button”且 Class 属性包含“myClass”的所有节点的“click”事件。

dojo/on 甚至支持自定义的事件监听。如 dojo/mouse 的自定义鼠标事件:


清单 35. dojo/on 监听自定义事件
  
require(["dojo/on", "dojo/mouse"], function(on, mouse){
on(node, mouse.enter, hoverHandler);
});

这里就是监听自定义的 mouseenter 事件。

最后,我们来看一个 dojo/on 和 query 协同工作的示例:


清单 36. dojo/on 同 query 协同
  
require([
'dojo/on',
'dojo/dom-class',
'dojo/dom-attr',
'dojo/query',
'dojo/domReady!'
], function(on, domClass, domAttr, query) {

var highlighter = {

setCol: function(cellIdx, classStr, tbl) {
var i = 0, len = tbl.rows.length;
for (i; i < len; i++) {
var cell = tbl.rows[i].cells[cellIdx];
if (cell && !domAttr.has(cell, 'colspan')) {
domClass.toggle(cell, classStr)
}
}
},

highlightCol: function(cssQuery, classStr) {
var self = this;
query(cssQuery).on('td:mouseover, td:mouseout', function(evt) {
self.setCol(this.cellIndex, classStr, evt.currentTarget);
});

},

highlightRow: function(cssQuery, classStr) {
query(cssQuery).on('tr:mouseover, tr:mouseout', function() {
domClass.toggle(this, classStr);
});

},

highlightBoth: function(cssQuery, classStrRow, classStrCol){
var self = this;
query(cssQuery).on('td:mouseover, td:mouseout', function(evt) {
var tbl = evt.currentTarget;
var tr = evt.target.parentNode;
var td = evt.target;
self.setCol(td.cellIndex, classStrCol, tbl);
domClass.toggle(tr, classStrRow);
});

}
};

highlighter.highlightBoth('#tbl', 'tdHover', 'trHover');

});

读者可以关注一下代码中加粗的三个 query 方法,它们的返回值是直接支持用 on 来批量绑定事件的,并且事件本身也支持事件代理,即基于 CSS 的 Selector 的批量元素绑定事件。通过这个示例,我们可以看到:基于 dojo/on 模块,我们几乎可以随心所欲的管理各种复杂和批量的事件。

关于 dojo/_base/connect(connect & subscribe),dojo/_base/event,dojo/Evented(自定义事件基类)都是大家再为熟悉不过的接口,这里不再介绍。

最后,我们来看看 dojo/behavior。dojo/behavior 主要模式是定义并添加行为(dojo.behavior.add),然后触发行为(dojo.behavior.apply),使用方式相当简单:


清单 37. dojo/behavior 使用示例
  
require(["dojo/behavior"], function(behavior){
// 定义行为
var myBehavior = {
// 所有 <a class="noclick"></a> 节点 :
"a.noclick" : {
// 一旦找到符合条件节点,便绑定 onclick 事件
onclick: function(e){
e.preventDefault(); // stop the default event handler
console.log('clicked! ', e.target);
}
},
// 所有 <span> 节点
"span" : {
// 一旦找到符合条件节点,便触发 found 事件
found: function(n){
console.log('found', n);
}
}
};
// 添加行为
behavior.add(myBehavior);
// 触发行为
behavior.apply();
});

读者可参考注释,这里我们通过 add 添加行为,apply 触发行为。这是 behavior 同事件协同工作的示例,其实 behavior 也能够同 topic 协同工作:


清单 38. dojo/behavior 协同 topic 使用示例
  
require(["dojo/behavior", "dojo/topic"], function(behavior, topic){
behavior.add({
"#someUl > li": "/found/li"
});
topic.subscribe("/found/li", function(msg){
console.log('message: ', msg);
});
behavior.apply();
});

这里我们主要关注一下"/found/li"这个 topic,当 behavior 调用 apply 以后,一旦找到符合“#someUl > li”的节点,便会 publish 这个"/found/li"的 topic,此时便会触发我们这里 subscribe 的函数。

Requests

顾名思义,Requests 主要就是指我们常用的 XHR 请求模块,新版 Dojo 中主要是指 dojo/request 对接口做出了一些调整,使得我们使用起来更加方便了。

先来看一个简单的示例:


清单 39. dojo/request 简单示例
  
require(["dojo/request", "dojo/dom", "dojo/dom-construct", "dojo/json", "dojo/on",
"dojo/domReady!"],
function(request, dom, domConst, JSON, on){
on(dom.byId("startButton"), "click", function(){
domConst.place("<p>Requesting...</p>", "output");
request("request/helloworld.json", options).then(function(text){
domConst.place("<p>response: <code>" + text + "</code>", "output");
});

});
});

大家主要关注一下“request”这一段代码,可以看到,它是和“then”方法一起使用的,options 中传入相关定制参数(如:handleAs,timeout 等等),功能上同之前的 dojo.xhrGet/Post 基本类似,但是这种编程模式比之前的 dojo.xhrPost 的模式更加清晰易懂了。

同样,dojo/request/xhr 接口替代了原有的 dojo/_base/xhr 接口,使用方式如下:


清单 40. dojo/request/xhr 简单示例
  
require(["dojo/request/xhr", "dojo/dom", "dojo/dom-construct", "dojo/json",
"dojo/on", "dojo/domReady!"],
function(xhr, dom, domConst, JSON, on){
on(dom.byId("startButton"), "click", function(){
domConst.place("<p>Requesting...</p>", "output");
xhr("helloworld.json",{
query: {
key1: "value1",
key2: "value2"
},
method: "POST",

handleAs: "json"
}).then(function(data){
domConst.place("<p>response: <code>" + JSON.stringify(data) + "</code></p>",
"output");
});
});
});

这里我们注意一下它的参数定制,query 负责传递实参,method 负责定义请求模式,这里是 POST。它支持 4 种模式 GET,POST,PUT 和 DEL。

dojo/request/node 模块是我们能够在 Dojo 中使用 Node.js 的模块发送 AJAX 请求,这里不深入,有兴趣的读者可以研究一下。

再来看看 dojo/request/iframe 模块,该模块主要通过 iframe 发送请求,它取代了 dojo/io/iframe 接口。它除了能够发送基本的 AJAX 请求外,还能够发送跨域的请求,并且可以通过 iframe 实现文件的异步上传,看一个简单的示例:


清单 41. dojo/request/iframe 简单示例
  
require(["dojo/request/iframe", "dojo/dom", "dojo/dom-construct", "dojo/json",
"dojo/on", "dojo/domReady!"],
function(iframe, dom, domConst, JSON, on){
on(dom.byId("startButton"), "click", function(){
domConst.place("<p>Requesting...</p>", "output");
iframe("helloworld.json.html",{
form: "theForm",
handleAs: "json"
}).then(function(data){
domConst.place("<p>data: <code>" + JSON.stringify(data) + "</code></p>",
"output");
});

});
});

注意,这里我们可以通过 form 参数来指定我们要提交的表单。

同 dojo/request/iframe 一样,dojo/request/script 取代了 dojo/io/script。它主要通过动态 <script> 标签来发送请求和接收响应。接口本身也是支持 JSONP 的:


清单 42. dojo/request/script 简单示例
  
require(["dojo/request/script", "dojo/dom", "dojo/dom-construct", "dojo/json",
"dojo/on", "dojo/domReady!"],
function(script, dom, domConst, JSON, on){
on(dom.byId("startButton"), "click", function(){
domConst.place("<p>Requesting...</p>", "output");
script.get("helloworld.jsonp.js", {
jsonp: "callback"
}).then(function(data){
domConst.place("<p>response data: <code>" + JSON.stringify(data) +
"</code></p>", "output");
});
});
});

这里其实是一个 JSONP 的请求,我们通过“jsonp”参数指定了回调函数的名称,后台通过返回 JSONP 模式的 JavaScript 代码来传递数据。这里需要强调一点,dojo/request/script 也是支持跨域的,有这种开发需求的读者们可以在自己的 Web 应用中多试用一下。

接下来我们看看 dojo/request/notify 模块,该模块专门用于监听 dojo/request 的各种请求事件。基于该模块,我们监听系统的 AJAX 各种请求事件并作出相应处理:


清单 43. dojo/request/notify 简单示例
  
require(["dojo/request/xhr", "dojo/request/notify", "dojo/on", "dojo/dom",
"dojo/dom-construct", "dojo/json", "dojo/domReady!"],
function(xhr, notify, on, dom, domConst, JSON){
notify("start", function(){
domConst.place("<p>start</p>", "output");
});
notify("send", function(response, cancel){
domConst.place("<p>send: <code>" + JSON.stringify(response) + "</code></p>",
"output");
});
notify("load", function(response){
domConst.place("<p>load: <code>" + JSON.stringify(response) + "</code></p>",
"output");
});
notify("error", function(response){
domConst.place("<p>error: <code>" + JSON.stringify(response) + "</code></p>",
"output");
});
notify("done", function(response){
domConst.place("<p>done: <code>" + JSON.stringify(response) + "</code></p>",
"output");
});
notify("stop", function(){
domConst.place("<p>stop</p>", "output");
});

on(dom.byId("startButton"), "click", function(){
xhr.get("helloworld.json", {
handleAs: "json"
}).then(function(data){

domConst.place("<p>request data: <code>" + JSON.stringify(data) +
"</code></p>", "output");
});
});
});

其实很简单,我们只需要简单的监听“start”、“send”、“load”、“error”等等方法并作出相应处理即可,当我们调用“xhr.get”的时候,这些事件便开始逐个被触发了。有了 notify 模块,对我们跟踪处理请求的状态非常有帮助。

再来看看 dojo/request/registry 接口,这个接口可能很容易被大家忽视,但是它的功能非常实用:它可以根据请求的 URL 或者参数的不同,来自动匹配相应合适的 request 模块来发送请求,如本地请求就用 dojo/request/xhr,跨域请求就用 dojo/request/script。参考下列代码:


清单 44. dojo/request/registry 简单示例
  
require(["dojo/request/registry", "dojo/request/script", "dojo/dom",
"dojo/dom-construct", "dojo/on", "dojo/domReady!"],
function(request, script, dom, domConst, on){
// 如果 URL 以 ".jsonp.js" 结尾,则使用 dojo/request/script,否则使用基本的 dojo/request
request.register(/\.jsonp\.js$/i, script);


on(dom.byId("startButton"), "click", function(){
domConst.place("<p>request: 'helloworld.jsonp.js'</p>", "output");
request.get("helloworld.jsonp.js", {
jsonp: "callback"
}).then(function(data){
domConst.place("<p>script data: <code>" + JSON.stringify(data) +
"</code></p>", "output");
});
domConst.place("<p>request: 'helloworld.json'</p>", "output");
request.get("helloworld.json", {
handleAs: "json"
}).then(function(data){
domConst.place("<p>xhr data: <code>" + JSON.stringify(data) + "</code></p>",
"output");
});
});
});

参见代码注释我们可以了解到,这里的 request.get("helloworld.jsonp.js", ...) 会使用“dojo/request/script”,而 request.get("helloworld.json", ...) 则会使用“dojo/request”。

最后,我们来看看 dojo/request/handlers。我们知道 Dojo 所有的 request 基本都支持 handleAs 这个参数,我们可以传入“json”,“javascript”,“xml”等值,举“json”为例,如果指定 handleAs 为“json”,Dojo 会在我们接收到返回值之前将纯字符串转化为 JSON 对象。但是,这些是事先设定好的 handleAs 方式,如果我们要自定义 handleAs 方式呢?答案就是 dojo/request/handlers。


清单 45. dojo/request/handlers 简单示例
  
require(["dojo/request/handlers", "dojo/request", "dojo/dom", "dojo/dom-construct",
"dojo/json",
"dojo/on", "dojo/domReady!"],
function(handlers, request, dom, domConst, JSON, on){
handlers.register("custom", function(response){
var data = JSON.parse(response.text);
data.hello += "!";
return data;
});


on(dom.byId("startButton"), "click", function(){
domConst.place("<p>Requesting...</p>", "output");
request("./helloworld.json", {
handleAs: "custom"
}).then(function(data){
domConst.place("<p>data: <code>" + JSON.stringify(data) + "</code>",
"output");
});
});
});

可以看到,这里我们通过“handlers.register("custom",...)”自定义了一个 handleAs 的方式,然后再 request 的参数里面指定了以这种方式预处理返回数据(handleAs: "custom")。有了这个功能,我们甚至能够很方便的自定义前端和后端的数据交换格式,大大增强我们开发 Web 应用的灵活性。

dojo/query

这个 dojo/query 模块相信读者们也是非常熟悉了,它主要是基于 CSS 的 Selector 来定位并返回相应节点。其实它使用起来非常简单,本小节我们会重点它的一些不太为人知的特殊功能。

先来看一个基本使用方式的示例:


清单 46. dojo/query 简单示例
  
require(["dojo/query", "dojo/dom"], function(query, dom){
var nl = query(".someClass", "someId");
// 或者
var node = dom.byId("someId");
nl = query(".someClass", node);
});

其主要参数其实很简单,第一个是 Selector 的内容,第二个是根节点的 ID 或者节点对象。这里我们就是查找节点 ID 为“someId”的节点的所有子节点中,包含 someClass 的 Class 属性的所有节点。dojo/query 返回值(这里是 nl)其实是一个 dojo/NodeList 对象,不是我们通常认为的数组对象。当然,它支持数组对象支持的下标运算符“[]”,但是它还包括很多额外的方法,如:concat,forEach,map,on,lastIndexOf 等等。所以要注意,我们不能简单的把它当成数组对象来对待。

同样,还有 dojo/NodeList-data,dojo/NodeList-dom,dojo/NodeList-fx,dojo/NodeList-html,dojo/NodeList-traverse 等等对象,它们扩展了 dojo/NodeList,实现了一些新的功能,如 dojo/NodeList-dom 在 dojo/NodeList 基础上扩展了一些 DOM 操作的接口,让我们可以很方便的批量执行一些 DOM 操作。dojo/NodeList-fx 扩展了一些动画接口,可以批量执行动画。这些接口相信很多读者之前就已经接触过了,这里不再深入,在希望未接触过的读者能注意一下,这些模块对于我们使用 dojo/query 非常有帮助。

再来看一些稍微复杂一点的示例:


清单 47. dojo/query 复杂示例
  
dojo.query('#t span.foo:not(span:first-child)'),
dojo.query('#t span.foo:not(:first-child)')

dojo.query('#t h3:nth-child(odd)'),
dojo.query('#t h3:nth-child(2n+1)')

dojo.query('#t2 > input[type=checkbox]:checked')

前两个例子会返回 ID 为“t”的节点下面,所有的不是其上层节点的第一个子节点的,并且 Class 属性为“foo”或者包含“foo”的所有 span 节点。

后两个例子会返回 ID 为“t”的节点下面,所有为其上层节点的奇数子节点的 h3

节点。

最后一个例子会返回 ID 为“t2”的节点下面,所有被选中的 checkbox 节点。

dojo/query 支持所有的 CSS3 的 Selector,感兴趣的读者可以参考一下 W3C 的关于 CSS3 的标准的定义,其中定义的所有 Selector 均可以用在 dojo/query 中。

既然我们是基于 CSS 的 Selector 来定位并返回节点的,那我们到底是基于哪个版本的 CSS 的 Selector 算法呢?事实上,dojo/query 支持四种 Selector 模式:CSS2,CSS2.1,CSS3,ACME。相比前三个大家都很熟悉了,第四个 ACME 其实是 CSS3 的进阶,除了支持所有 CSS3 的 Selector 外,它还支持一些 Selector 引擎不支持的的检索规则。默认情况下,如果设定 async 为 false,dojo/query 会使用 ACME 模式,如果 async 为 true,则使用 CSS3。

我们可以通过 dojoConfig 来定义我们所使用的 Selector 模式,也可以在引用 dojo/query 模块的时候指定:


清单 48. dojo/query 的 Selector 模式定义
  
<script data-dojo-config="selectorEngine: 'css2.1', async: true"
src="dojo/dojo.js">
</script>

<script type="text/javascript">
var dojoConfig = {
selectorEngine: "css2.1",
async: true
};
</script>
<script src="dojo/dojo.js">

define(["dojo/query!css3"], function(query){
query(".someClass:last-child").style("color", "red");
});

清单 48 中列举了三种方式定义 Selector 模式,读者们可以根据需要自行选择。

其实 Dojo 还包含其它的 Selector 模式,可以从如下网址下载:

sizzle: https://github.com/kriszyp/sizzle

slick: https://github.com/kriszyp/slick

安装好后,通过之前介绍的方式定义即可:


清单 49. dojo/query 的 Selector 模式定义进阶
  
<script data-dojo-config="selectorEngine: 'sizzle/sizzle'" src="dojo/dojo.js">
</script>

define(["dojo/query!slick/Source/slick"], function(query){
query(".someClass:custom-pseudo").style("color", "red");
});

由此可见,关于 Selector 的模式的定义是非常灵活的,可扩展性非常强。

Parser

Parser 是 Dojo 的解释器,专门用于解析 Dojo 的 widgets。其实平常我们基本不会涉及到使用 dojo/parser,但是,在某些特殊情况下,dojo/parser 可能会带给我们意想不到的便利。并且,它的一些配置参数也是非常值得我们注意的。

其实我们都知道,如何加载和运行 dojo/parser:


清单 50. dojo/parser 的简单示例
  
require(["dojo/parser"], function(parser){
parser.parse();
});

require(["dojo/parser", "dojo/ready"], function(parser, ready){
ready(function(){
parser.parse();
});
});

<script type="text/javascript" src="dojo/dojo.js"
data-dojo-config="parseOnLoad: true"></script>

清单 50 中的三种方式都是可行的,可能最后一种方式是我们用的最多的。

如果单纯调用 parser.parse(),dojo/parser 会解析整个页面,其实我们也能给它限定范围:


清单 51. dojo/parser 的进阶示例
  
require(["dojo/parser", "dojo/dom"], function(parser, dom){
parser.parse(dom.byId("myDiv"));
});

require(["dojo/parser", "dojo/dom"], function(parser, dom){
parser.parse({
rootNode: dom.byId("myDiv");
});
});

清单 51 中的代码就将 dojo/parser 限定在了 ID 为“myDiv”的节点内部。dojo/parser 甚至都能改变解析的属性:


清单 52. dojo/parser 的 Scope 示例
  
require(["dojo/parser", "dojo/dom"], function(parser, dom){
parser.parse({
scope: "myScope"
});

});

<div data-myScope-type="dijit/form/Button" data-myScope-id="button1"
data-myScope-params="onClick: myOnClick">Button 1</div>

很明显,当我们设定了“scope”为“myScope”之后,其解析的属性由“data-dojo-type”变为“data-myScope-type”。

但是仅仅停留在这样对 dojo/parser 的简单的使用模式上,我们永远成不了高手,dojo/parser 还有更多的功能:


清单 53. dojo/parser 编程
  
require(["dojo/parser", "dojo/_base/array"], function(parser, array){
parser.parse().then(function(instances){
array.forEach(instances, function(instance){
// 处理扫描到的所有 widget 实例
});
});
});

可见,通过 dojo/parser,我们是可以基于它的返回值继续进行编程开发的,而不仅仅是利用它简单的解析 Dojo 的 widget。这里我们可以拿到所有解析出来的 widget 实例对象,并做出相应处理。

同样,我们还能直接调用“instantiate”方法实例化类:


清单 54. dojo/parser 的 instantiate 方法
  
<div id="myDiv" name="ABC" value="1"></div>

require(["dojo/parser", "dojo/dom"], function(parser, dom){
parser.instantiate([dom.byId("myDiv")], { data-dojo-type: "my/custom/type"});
});

这种做法和您在页面上写好 {data-dojo-type: "my/custom/type"},然后调用 parser.parse() 的效果是一样的。

既然说到了 dojo/parser,我们也要了解一些关于 dojo/parser 的默认解析行为,让我们看一个下面的例子:


清单 55. dojo/parser 解析行为
  
//JavaScript 代码:定义类并解析
require(["dojo/_base/declare", "dojo/parser"], function(declare, parser){
MyCustomType = declare(null, {
name: "default value",
value: 0,
when: new Date(),
objectVal: null,
anotherObject: null,
arrayVal: [],
typedArray: null,
_privateVal: 0
});

parser.parse();
});

//HTML 代码:使用 MyCustomType 类
<div data-dojo-type="MyCustomType" name="nm" value="5" when="2008-1-1"
objectVal="{a: 1, b:'c'}" anotherObject="namedObj" arrayVal="a, b, c, 1, 2"
typedArray="['a', 'b', 'c', 1, 2]" _privateVal="5" anotherValue="more"></div>

这里我们先定义了一个 MyCustomType 类,并声明了它的属性,然后在后面的 HTML 代码中使用了该类。现在我们来看看 dojo/parser 对该类的变量的解析和实例化情况:

name: "nm", // 简单字符串

value: 5, // 转成整型

when: dojo.date.stamp.fromISOString("2008-1-1"); // 转成 date 类型

objectVal: {a: 1, b:'c'}, // 转成对象类型

anotherObject: dojo.getObject("namedObj"), // 根据字符串的特点转成对象类型

arrayVal: ["a", "b", "c", "1", "2"], // 转成数组类型

typedArray: ["a", "b", "c", 1, 2] // 转成数组类型

可见,成员变量的实例化和成员变量最初的初始化值有着密切联系,dojo/parser 会智能化的做出相应的处理,以达到您最想要的结果。注意,这里 _privateVal 的值没有传入到对象中,因为以“_”开头的变量会被 Dojo 理解成私有变量,所以其值不会被传入。另外,anotherValue 也不会实例化,因为该成员变量不存在。

当然,如果我们不喜欢 dojo/parser 的默认行为,我们可以在类里面实现“markupFactory”方法,这个方法专门用来实现自定义的实例化。

Browser History

顾名思义,这一小节主要是关于如何处理浏览器历史。在 Web2.0 应用中,越来越多的使用单页面模式了(没有页面跳转),即用户的所有操作以及该操作所带来的界面的变化都放生的同一个页面上,这样一来,浏览器默认就不会有任何的操作历史记录,也就是说,浏览器上的“前进”和“后退”按钮会永远处于不可用的状态。这个时候,如果我们还想记录用户的一些复杂操作的历史,并能通过浏览器的“前进”和“后退”按钮来重现这些历史,我们就必须借助浏览器的历史管理功能了,Dojo 中就有这样的接口能够方便的让我们管理浏览器的历史:dojo/back。接下来,我们就来看看如何使用该接口:


清单 56. dojo/back 示例
  
<body>
<script type="text/javascript">
require(["dojo/back"], function(back){
back.init();

var state = {
back: function(){ alert("Back was clicked!"); },
forward: function(){ alert("Forward was clicked!"); }
};
back.setInitialState(state);

// 进行一些列操作后,如果想将当前状态记入历史状态,则调用如下代码即可
// back.addToHistory(state2);
});
</script>
// body 的其它代码
</body>

可见,首先通过 back.init() 初始化,然后定义一个状态对象,并调用 setInitialState 方法初始化当前历史状态。最后,如果需要再次记录进行一些列操作后的状态到历史状态,调用 addToHistory 即可。这里 state 对象中定义的 back 和 forward 方法,就是为了响应当前历史状态下用户点击“后退”和“前进”的动作。

既然说到了 dojo/back,我们也顺便提一下 dojo/hash,顾名思义,该接口主要用来管理浏览器 URL 的 hash 的历史状态:


清单 57. dojo/hash 示例
  
require(["dojo/hash", "dojo/io-query"], function(hash, ioQuery){
connect.subscribe("/dojo/hashchange", context, callback);
function callback(hash){
// hashchange 事件 !
var obj = ioQuery.queryToObject(hash);
if(obj.firstParam){
// 处理代码
}
}
});


require(["dojo/hash", "dojo/io-query"], function(hash, ioQuery){
var obj = ioQuery.queryToObject(hash()); // 取 hash 值
obj.someNewParam = true;
hash(ioQuery.objectToQuery(obj)); // 设置 hash 值
});

这里列举了比较典型的 dojo/hash 用法:

1. 我们能够通过监听“/dojo/hashchange”的 topic 来监听 URL 的 hash 值的变化,并作出相应处理。

2. 我们也能够通过 hash() 取得当前 URL 的 hash 值,同样也能通过 hash(ioQuery.objectToQuery(obj)) 去设定当前 URL 的 hash 值。

Dojo 中还有一个 dojo/router 模块与 hash 有关,它专门用来有条件的触发 hashchange 事件。我们先来看一个示例:


清单 58. dojo/router 示例
  
require(["dojo/router", "dojo/dom", "dojo/on", "dojo/request", "dojo/json",
"dojo/domReady!"],
function(router, dom, on, request, JSON){
router.register("/foo/bar", function(evt){
evt.preventDefault();
request.get("request/helloworld.json", {
handleAs: "json"
}).then(function(response){
dom.byId("output").innerHTML = JSON.stringify(response);
});
});

router.startup();

on(dom.byId("changeHash"), "click", function(){
router.go("/foo/bar");
});
});

这里我们重点关注一下 router.register 方法,第一个参数其实是用于匹配的 hash 值,它也可以 hash 的一个 pattern,即 RegExp。一旦它匹配了当前的 hash 值,便会触发它的回调函数(第二个参数)。router.startup 用于 router 的初始化,最后的 router.go("/foo/bar") 用于更新当前的 hash 值,所以 router.go 之后便会触发 router 的回调函数。

Mouse, Touch 和 Keys

本小节我们主要来讲讲 Dojo 的一些特殊的事件处理,Dojo 基于标准的 Web 事件机制,对某些事件做了一些封装和优化,解决了交互开发中的很多令人苦恼的问题。先来看看 dojo/mouse,该模块是专门用来优化和管理鼠标事件的,先来看一个示例:


清单 59. dojo/mouse 示例
  
require(["dojo/mouse", "dojo/dom", "dojo/dom-class", "dojo/on", "dojo/domReady!"],
function(mouse, dom, domClass, on){
on(dom.byId("hoverNode"), mouse.enter,function(){
domClass.add("hoverNode", "hoverClass");
});

on(dom.byId("hoverNode"), mouse.leave,function(){
domClass.remove("hoverNode", "hoverClass");
});
});

这里我们主要关注一下 mouse.enter 和 mouse.leave。可以看到,我们绑定的事件不再是“onmouseover/onmouseenter”和“onmouseout/onmouseleave”,而是 mouse.enter 和 mouse.leave,这是 Dojo 对 Web 标准事件的一个扩展。很多用过 onmouseover 和 onmouseout 事件的读者可能有所体会,这两个事件其实是非常不完美的:当绑定 onmouseover 事件的节点,它还有很多内部节点时,鼠标在该节点内部悬停或者移动时往往会触发很多次没有意义的 onmouseout 或者 onmouseover,这也是我们不希望看到的。而并不是像我们想象的那样:只有当鼠标移出该节点是才会触发 onmouseout 事件。这一点 IE 浏览器的 onmouseenter/onmouseleave 做的就不错。Dojo 的 mouse.enter 和 mouse.leave 也是基于 IE 的 onmouseenter/onmouseleave 事件的原理所做的 Web 基础事件的扩展,使得所有的浏览器都能使用类似 IE 的 onmouseenter/onmouseleave 事件。

同样,dojo/mouse 还有很多其它功能接口:


清单 60. dojo/mouse 功能接口示例
  
require(["dojo/mouse", "dojo/on", "dojo/dom"], function(mouse, on, dom){
on(dom.byId("someid"), "click", function(evt){
if (mouse.isLeft(event)){
// 处理鼠标左键点击事件
}else if (mouse.isRight(event)){
// 处理鼠标右键点击事件
}
});
});

基于代码注释可以看到,mouse.isLeft/Right 用于判断鼠标的左右键。基于这些接口,我们就不用去考虑底层不同浏览器的差异,而只西药关注我们自己的代码逻辑了。

再来看看 dojo/touch,这个接口更是实用了,它可以让一套代码同时支持桌面 Web 应用和触摸屏应用。先来看一个简单的示例:


清单 61. dojo/touch 功能接口示例
  
require(["dojo/touch", "dojo/on"], function(touch){
on(node, touch.press, function(e){
// 处理触摸屏的 touchstart 事件,或者桌面应用的 mousedown 事件
});
});

require(["dojo/touch"], function(touch){
touch.press(node, function(e){
// 处理触摸屏的 touchstart 事件,或者桌面应用的 mousedown 事件
});
});

可见,其使用方式非常简单,同 dojo/mouse 一样。这里列出了两种用法,通过 dojo/on 绑定事件或者直接通过 touch.press 绑定均可。

我们可以参照如下的事件对照表:

dojo/touch 事件 桌面 Web 浏览器 触摸屏设备(ipad, iphone)
touch.press mousedown touchstart
touch.release mouseup touchend
touch.over mouseover 合成事件
touch.out mouseout 合成事件
touch.enter dojo/mouse::enter 合成事件
touch.leave dojo/mouse::leave 合成事件
touch.move mousemove 合成事件
touch.cancel mouseleave touchcancel

最后我们来看看 dojo/keys,这个就非常简单了,它存储了所有键盘按键对应的常量。基于这个接口,我们的代码会非常的通俗易懂。参见以下示例:


清单 62. dojo/keys 功能接口示例
  
require(["dojo/keys", "dojo/dom", "dojo/on", "dojo/domReady!"],
function(keys, dom, on){
on(dom.byId("keytest"), "keypress", function(evt){
var charOrCode = evt.charCode || evt.keyCode,
output;
switch(charOrCode){
case keys.LEFT_ARROW:
case keys.UP_ARROW:
case keys.DOWN_ARROW:
case keys.RIGHT_ARROW:
output = "You pressed an arrow key";
break;
case keys.BACKSPACE:
output = "You pressed the backspace";
break;
case keys.TAB:
output = "You pressed the tab key";
break;
case keys.ESCAPE:
output = "You pressed the escape key";
break;
default:
output = "You pressed some other key";
}
dom.byId("output").innerHTML = output;
});
});

这里的 keys.LEFT_ARROW,keys.UP_ARROW 等等分别对应着键盘上的“左”键和“上”键等等。这些接口看似功能简单,但是对于我们的代码维护和管理是非常有帮助的。

还有 dojo/aspect,dojo/Stateful,dojo/storedojo/d ata,dojo/dom 相关,dojo/html ,dojo/window,dojo/fx,dojo/back,dojo/h ash,dojo/router ,dojo/cookie,dojo/string,dojo/json,dojo/colors,dojo/date,dojo/rpc,dojo/robot 等等功能接口,这些接口大家都比较熟悉,而且很多接口在我之前发表的文章中已经专题讨论并详细介绍过了,这里就不在深入了。其实关于 Dojo 的核心接口还有很多,将来也会不断丰富和完善,这些接口能大大便利我们的日常开发,希望读者们能够了解并早日熟悉起来。

结束语

这篇文章介绍了 Dojo 的一些核心接口及其使用方法,从核心基础接口,如:dojo/_base/kernel,dojo/_base/config,loader 相关等等,到核心功能接口,如:dojo/promise/Promise,dojo/Deferred,dojo/request,dojo/query 等等,依次介绍了影响我们日常 Web 开发的各种接口。不仅介绍了这些接口的用途和优缺点,也给出了很多使用示例来帮助读者们理解和掌握这些接口。针对一些比较重要的接口,还给出了相关的原理的剖析和与现有原始接口的比较,充分揭示了 Dojo 在这些领域的优势。本文主要是基于实际的代码示例来说明这些接口用法,简明直观,推荐大家在日常开发中多参考。

参考资料


参考资料

学习

  • Dojo 文档主页 :Dojo 中控件的比较完全的 API 文档主页,包括 Dojo,Dijit,Dojox 等等。

  • Dojo 官方文档主页 :Dojo 的官方文档,里面包含了 Dojo 的各种最新特性和代码示例。

  • 查看 HTML5 专题 ,了解更多和 HTML5 相关的知识和动向。

  • developerWorks Web development 专区 :通过专门关于 Web 技术的文章和教程,扩展您在网站开发方面的技能。

  • developerWorks Ajax 资源中心 :这是有关 Ajax 编程模型信息的一站式中心,包括很多文档、教程、论坛、blog、wiki 和新闻。任何 Ajax 的新信息都能在这里找到。

  • developerWorks Web 2.0 资源中心 ,这是有关 Web 2.0 相关信息的一站式中心,包括大量 Web 2.0 技术文章、教程、下载和相关技术资源。您还可以通过Web 2.0 新手入门 栏目,迅速了解 Web 2.0 的相关概念。