1. 程式人生 > >深入瞭解 Dojo 的核心介面

深入瞭解 Dojo 的核心介面

原文:https://www.ibm.com/developerworks/cn/web/1303_zhouxiang_dojocore/#ibm-pcon

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 程式碼示例

1

2

3

4

5

6

7

8

9

10

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 程式碼示例

1

2

3

4

5

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 基本程式碼示例

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

// 通過“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 自定義引數程式碼示例

1

2

3

4

5

6

7

8

9

10

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 程式碼示例

1

2

3

4

5

6

7

8

9

<script>

   dojoConfig = {

       has: {

           "dojo-firebug": true,

           "dojo-debug-messages": true,

           "dojo-amd-factory-scan": false

       }

   };

</script>

可以看到,這裡我們開啟了 Dojo 的預設除錯工具和“deprecated”,“experimental”的訊息顯示,但是關閉了 AMD 模組掃描的功能。

最後,我們來看看如何通過 Dojo 的 Config 來重定位模組:

清單 6. Config 的模組重定位程式碼示例

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

<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

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

// 方法 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 模組程式碼示例

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

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 程式碼示例

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

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 程式碼示例

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

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 的小工具程式碼示例

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

// 模組路徑到實際路徑的轉化

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 的微事件程式碼示例

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

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 外掛

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

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 外掛

1

2

3

4

5

6

7

8

9

10

11

12

13

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 外掛

1

2

3

4

5

6

7

8

9

10

11

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 模組

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

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 模組

1

2

3

require(["dojo/_base/lang"], function(lang){

 // 引入 lang 模組

});

接下來我們看看它主要的功能介面,首先是 clone 介面,用於克隆物件或陣列,使用方式如下:

清單 18. 模組 lang 的 clone 介面

1

2

3

4

5

6

7

8

9

10

11

12

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 介面

1

2

3

4

5

6

7

8

9

10

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 介面

1

2

3

4

5

6

7

8

9

10

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 介面

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

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 介面

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

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 抽象基類實現

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

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 的簡單使用

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

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 的進階使用

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

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 的使用

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

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 的使用

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15