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 基本代碼示例
可見,配置 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 接口
這里同我們常用的 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 接口
讀者們可以參考清單 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 的簡單使用
這里我們先構建了一個 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 的進階使用
很明顯,這里除了 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 協同
讀者可以關注一下代碼中加粗的三個 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 簡單示例
大家主要關注一下“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 簡單示例
注意,這里我們可以通過 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 簡單示例
參見代碼注釋我們可以了解到,這里的 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 簡單示例
可以看到,這里我們通過“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/store ,dojo/d ata,dojo/dom 相關,dojo/html ,dojo/window,dojo/fx,dojo/back,dojo/hash,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 在這些領域的優勢。本文主要是基于實際的代碼示例來說明這些接口用法,簡明直觀,推薦大家在日常開發中多參考。