http://dojotoolkit.org/documentation/tutorials/1.10/modern_dojo/index.html
你可能已经不用doio一段时间了,或者你一直想保持你基于dojo1.6的老代码在1.10下能够正确运行,但却不知道应该怎么做。你一直在听说“AMD”和"baseless"等概念,但却不知道如何开始学习这些,这篇教程会帮你了解这些。
开始
从dojo的1.7版本开始,dojo Toolkit有了重大的变化,并朝着更加先进的架构模式发展。dojo1.10延续了这样的发展路线。虽然dojo1.10是向后兼容的,但为了充分发挥dojo1.10的优势,一些概念也发生了变化。这些概念将成为dojo2.0的基础,所以你必须确保你正在正确的学习道路上。所以了能够充分的利用现在dojo的一些新特性,例如dojo/on,你必须理解一些先进的概念。
在本教程中,我们会想你介绍dojo新加入的一些概念。我会提及到废弃的和先进的dojo。我会尽我最大的努力介绍哪些东西发生了变化,为什么变化。一些变化是基本的变化,但也有一些变化时令人难以理解的,不过他们都是为了让你的代码更有效率,运行的更快,更好的利用Javascript,让你的代码有更好的可维护性。我觉得花一些时间去了解现代的dojo是十分值得的。
本教程并不是一个dojo变化迁移指南,但很多时候会使你想到你已经熟悉的dojo。一些技术细节可以在该教程获取,Dojo 1.X to 2.0 Migration Guide。
你好,新的世界
现代dojo的一个核心概念就是全局命名是不可取的。得到这样的结论是有很多原因的,但在一个复杂的Web应用中,全局命名的变量很容易和各种代码混杂在一起,特别是使用了多个js框架的时候。站在安全的立场上,我不得不提及,有些不法分子甚至恶意的修改全局变量名称。现代dojo的解决办法是如果你想直接访问全局命名空间,讲会被禁止,因为你正在做一件错误的事情。虽然鉴于向后兼容的原因,绝大多数Toolkit仍然保存着全局命名,但在以后的开发中国不要再使用这种方式了。
如果你发现你的代码中抱哈了dojo.*、dijit.*或者dojox.*,那么你已经做错了一些事情。
这意味着尽管开发人员还可以使用过时的dojo,例如包含dojo.js,调用一些核心的功能,加载一些模块,定义自定义模块等,但我们是在不推荐你这些继续做下去了。事实是,这些性能在2.0版本之后都不在支持。
Again, repeat after me "the global namespace is bad, the global namespace is bad, I will not use the global namespace, I will not use the global namespace".
另一个核心概念是,同步操作较慢,异步操作较快。以前的dojo已经使用dojo.Deferred
实现了比较强大的异步加载机制,但在现代dojo中,我们想最好是所有的操作都是异步的。
To strengthen the modularity of Dojo and leverage the concepts above, in 1.7 Dojo adopted the CommonJS module definition called Asynchronous Module Definition (AMD). This meant a fundamental re-write of the Dojo module loader which is usually exposed through the require()
and define()
functions. You can find full documentation of the loader in the reference guide. This has fundamentally changed the way code is structured.
利用上面的概念,我们可以加强dojo的模块化。在dojo1.7版本时,dojo引进了CommonJS定义的模型模块化概念,叫做异步模块定义(AMD)。这意味着之前加载和定义模块的函数requires()和define()函数都要需要重写。你可以查看到关于loader in the reference guide的完整文档资料。这重根本上改变了代码的结构。
Let's take for example something we would have done in "legacy":
下面我们是使用过时的dojo方式写的一个例子。
1 dojo.ready(function(){ 2 dojo.byId("helloworld").innerHTML = "Hello World!"; 3 });
下面我们再看下现在dojo的定义方式:
1 require(["dojo/dom", "dojo/domReady!"], function(dom){ 2 dom.byId("helloworld").innerHTML = "Hello New World!"; 3 });
欢迎来到完美的新世界,现代dojo的基础是require()函数。该函数创建了一个javascript闭包,加载代码需要的模块,并作为参数传递给主函数。函数的第一个参数是模块标识集合,第二个参数是主函数。在require()闭包中,我们可以通过声明的参数来引用dojo模块。我们调用模块时,存在一些常用的用法习惯。
The loader, just like the legacy one, does all the heavy lifting of finding the module, loading it and managing the loaded modules.
加载器可以查找模块,架子啊模块以及管理已经加载的模块等重要工作。
你可以看到,在上面的代码中,dojo/domReady!模块虽然在模块加载的数组中存在,但在参数的列表中却没有。其实这是一个加载器插件,用来控制加载器的行为。该插件的作用是加载等待一直等到在页面上加载的DOM结构都已经加载完毕。在异步情况下,你不可能还没有等DOM加载完毕,就去操纵它。所以如果你想对DOM节点做些操作,请确保使用了该插件。因为我们不直接调用该插件的任何功能,而且该插件是模型标识集合中的最后一个,所以在主函数中,可以不明确设置该参数。
You can also get a reference to a module with require()
after that module has already loaded, by just supplying the MID as a single string argument. This won't load the module and will throw an error if it isn't already loaded. You won't see this coding style in the Dojo Toolkit, because we like to manage all of our dependencies centrally in the code. But if you choose to use this alternative syntax it would look something like this:
你也可以使用require()函数加载一个模块,但不用把模块作为参数传递给主函数。如果没有加载该模块就直接调用,将会发生错误。你不会在Toolkit看到这种代码风格,因为我们一般都会集中管理我们编写代码的依赖。但如果你非要采用这种方式话,编写的代码如下面的代码所示:
1 require(["dojo/dom"], function(){ 2 // some code 3 var dom = require("dojo/dom"); 4 // some more code 5 });
Dojo Base and Core
当你使用现代Dojo的时候,可能听说过"baseless"的概念。这个概念保证了一个模块不会依赖任何一个其不需要的其他模块。在之前的版本中(dojo1.6及以前),会加载所有的功能,知道2.0版本之后。但是如果你想确保你的代码可以很方便的移植到新版本上,你必须停止使用dojo.*这样的用法。This does mean you might not know where certain parts of the namespace are now。
One of the dojoConfig
options is async
. It defaults to false
and this means all the Dojo base models are automatically loaded. If you set it to true
and take advantage of the asynchronous nature of the loader, these modules will not automatically be loaded. All of this combined together makes for a more responsive and faster loading application.
dojoConfig包含了一个名为async的属性。该属性的默认值为False,意味着所有的dojo基础模块都会被默认加载。如果把该属性这只为True,利用异步加载器,那么这些模型就不会自动的被加载。这种方式会让你的应用响应和加载更为迅速。
另外,dojo支持EcmaScript 5规则。在尽可能的情况下,DOjo会支持ES5的一些功能,但在一些老旧的浏览器环境下,dojo可能模拟es5的功能。也就是说看起来像是dojo完成的功能,实际上可能不是dojo做的。
参考指南已经列出了所有的功能,里面也包含了basic functions。
Once you get outside of the Dojo Base and Core, almost everything else would work like the following. Where you would have done a dojo.require()
:
一旦你在dojo基础和核心之外编写代码,你可能会编写下面的代码。你会使用dojo.require()函数。
1 dojo.require("dojo.string"); 2 dojo.byId("someNode").innerHTML = dojo.string.trim(" I Like Trim Strings ");
但现在,你可以直接使用require()函数。
1 require(["dojo/dom", "dojo/string", "dojo/domReady!"], function(dom, string){ 2 dom.byId("someNode").innerHTML = string.trim(" I Like Trim Strings "); 3 });
事件和通知
dojo.connection()和dojo.disconnection()已经被移动大dojo/_base/connect模块中,现在dojo必须使用dojo/on来处理事件,使用dojo/aspect来通知函数。在Events教程中有更详细的信息,但我们在这里主要对比他们之间的差别。在以前的dojo中,在事件和函数通知上,没有太大的区别,我们使用使用dojo.connect()处理这两种情况。事件是当一个对象触发一个动作时关联的哈数,例如click事件。dojo/on可以无缝的处理原生态的DOM事件以及dojo小部件暴露出的事件。把一个行位链接到已有对象上也是AOP(面向方面编程)重要概念的一部分。很多dojo部分通过dojo/sapect模块提供的功能相应了AOP的思想。
在以前的dojo中,我们处理Button的Onclick事件的代码如下:
1 <script> 2 dojo.require("dijit.form.Button"); 3 4 myOnClick = function(evt){ 5 console.log("I was clicked"); 6 }; 7 8 dojo.connect(dojo.byId("button3"), "onclick", myOnClick); 9 </script> 10 <body> 11 <div> 12 <button id="button1" type="button" onclick="myOnClick">Button1</button> 13 <button id="button2" data-dojo-type="dijit.form.Button" type="button" 14 data-dojo-props="onClick: myOnClick">Button2</button> 15 <button id="button3" type="button">Button3</button> 16 <button id="button4" data-dojo-type="dijit.form.Button" type="button"> 17 <span>Button4</span> 18 <script type="dojo/connect" data-dojo-event="onClick"> 19 console.log("I was clicked"); 20 </script> 21 </div> 22 </body>
在现代的dojo中,你只需要使用dojo/on模块就可以实现上述功能。无论是编程的方式还是声明的方式,无论是原生态DOM事件还是小部件定义的事件。
1 <script> 2 require([ 3 "dojo/dom", 4 "dojo/on", 5 "dojo/parser", 6 "dijit/registry", 7 "dijit/form/Button", 8 "dojo/domReady!" 9 ], function(dom, on, parser, registry){ 10 var myClick = function(evt){ 11 console.log("I was clicked"); 12 }; 13 14 parser.parse(); 15 16 on(dom.byId("button1"), "click", myClick); 17 on(registry.byId("button2"), "click", myClick); 18 }); 19 </script> 20 <body> 21 <div> 22 <button id="button1" type="button">Button1</button> 23 <button id="button2" data-dojo-type="dijit/form/Button" type="button">Button2</button> 24 <button id="button3" data-dojo-type="dijit/form/Button" type="button"> 25 <div>Button4</div> 26 <script type="dojo/on" data-dojo-event="click"> 27 console.log("I was clicked"); 28 </script> 29 </button> 30 </div> 31 </body>
Notice how dijit.byId
isn't used. In "modern" Dojo, the dijit/registry
is used for widgets and registry.byId()
retrieves a reference to the widget. Also, notice how dojo/on
handles both the DOM node and widget events in the same way.
你会先发现我们没有用到dijit.byId函数。在现代dojo中,已经使用dijit/refistry模块中的registry.byId()函数来获取小部件的引用。并且使用dojo/on模块使用同一种方式处理原生态DOM和小部件的事件。
如果要解除一个事件绑定的话,使用以前的dojo,你可能会编写下面的代码:
1 var callback = function(){ 2 // ... 3 }; 4 var handle = dojo.connect(myInstance, "execute", callback); 5 // ... 6 dojo.disconnect(handle);
In "modern" Dojo, the dojo/aspect
module allows you to get advice from a method and add behaviour "before", "after" or "around" another method. Typically, if you were converting a dojo.connect()
you would replace it with an aspect.after()
which would look something like this:
在现代dojo中,使用dojo/aspect模块,允许你得到一个函数运行之前,之后的行为引用。你可以使用aspect.after替换dojo.connect(),代码如下所示:
1 require(["dojo/aspect"], function(aspect){ 2 var callback = function(){ 3 // ... 4 }; 5 var handle = aspect.after(myInstance, "execute", callback); 6 // ... 7 handle.remove(); 8 });
主题
Another area that has undergone a bit of a revision is the "publish/subscribe" functionality in Dojo. This has been modularized under the dojo/topic
module and improved.
For example, the "legacy" way of doing this would be something like:
另外一个有重大修订的模块时发布和订阅模块。该模块已经被整合到dojo/topic模块下。例如在以前的代码中,我们会写下面的代码:
// To publish a topic dojo.publish("some/topic", [1, 2, 3]); // To subscribe to a topic var handle = dojo.subscribe("some/topic", context, callback); // And to unsubscribe from a topic dojo.unsubscribe(handle);
但为现代dojo中,你需要利用dojo/topic,按照下面的方式做。
1 require(["dojo/topic"], function(topic){ 2 // To publish a topic 3 topic.publish("some/topic", 1, 2, 3); 4 5 // To subscribe to a topic 6 var handle = topic.subscribe("some/topic", function(arg1, arg2, arg3){ 7 // ... 8 }); 9 10 // To unsubscribe from a topic 11 handle.remove(); 12 });
Promises
dojo中,一个最重要的核心概念之一就是延迟类,在1.5版本中,该功能已经从迁移到“promise”中,它值得我们在这里讨论一下。在1.8版本时,promise类被重写。虽然重表面的语义上看,和以前一样,但该模块已经不在支持老旧的API,所以如何想使用该模块,必须使用现代Dojo的API。在老旧API中,实现相关功能的代码如下:
1 function createMyDeferred(){ 2 var myDeferred = new dojo.Deferred(); 3 setTimeout(function(){ 4 myDeferred.callback({ success: true }); 5 }, 1000); 6 return myDeferred; 7 } 8 9 var deferred = createMyDeferred(); 10 deferred.addCallback(function(data){ 11 console.log("Success: " + data); 12 }); 13 deferred.addErrback(function(err){ 14 console.log("Error: " + err); 15 });
但在现代dojo中,实现的代码是这样的
1 require(["dojo/Deferred"], function(Deferred){ 2 function createMyDeferred(){ 3 var myDeferred = new Deferred(); 4 setTimeout(function(){ 5 myDeferred.resolve({ success: true }); 6 }, 1000); 7 return myDeferred; 8 } 9 10 var deferred = createMyDeferred(); 11 deferred.then(function(data){ 12 console.log("Success: " + data); 13 }, function(err){ 14 console.log("Error: " + err); 15 }); 16 });
Requests
Ajax是每个javascript库的核心模块之一。从dojo1.8版本之后,该模块的API也进行了更新,可以跨平台、易扩展并易重用。以前,你经常兼顾处理来自XHR、脚本以及IFrameIO以及自己的程序返回的数据。dojo/request模块就可以让这些事情变得更简单。
Just like dojo/promise
the old implementations are still there, but you can easily re-factor your code to take advantage of the new. For example, in "legacy" Dojo you might have written something like this:
类似于dojo/promise
,以前的代码仍然可用,但你可以很难容易的重构你的代码。例如,使用以前的dojo,你写的代码如下所示。
1 dojo.xhrGet({ 2 url: "something.json", 3 handleAs: "json", 4 load: function(response){ 5 console.log("response:", response); 6 }, 7 error: function(err){ 8 console.log("error:", err); 9 } 10 });
使用现代Dojo,代码如下:
1 require(["dojo/request"], function(request){ 2 request.get("something.json", { 3 handleAs: "json" 4 }).then(function(response){ 5 console.log("response:", response); 6 }, function(err){ 7 console.log("error:", err); 8 }); 9 });
DOM操作
You might be seeing a trend here if you have gotten this far in the tutorial, in that not only has Dojo abandoned its dependency on the global namespace and adopted some new patterns, it has also broken out some of "core" functionality into modules and what is more core to a JavaScript toolkit than DOM manipulation.
在整个教程中,您可以看到了dojo的整个发展趋势,不仅仅是把全局命名废弃,更重要的是基于模块化架构定义很多核心模块。例如对DOM的操作模块。
Well, that too has been broken up into much smaller chunks and modularized. Here is summary of the modules and what they contain:
DOM操作模块被分解为很多下的模块,主要的模块如下表所示:
Module
描述 | 包含的主要函数 | |
---|---|---|
dojo/dom | Core DOM functions | byId() isDescendant() setSelectable() |
dojo/dom-attr | DOM attribute functions | has() get() set() remove() getNodeProp() |
dojo/dom-class | DOM class functions | contains() add() remove() replace() toggle() |
dojo/dom-construct | DOM construction functions | toDom() place() create() empty() destroy() |
dojo/dom-form | Form handling functions | fieldToObject() toObject() toQuery() toJson() |
dojo/io-query | String processing functions | objectToQuery() queryToObject() |
dojo/dom-geometry | DOM geometry related functions | position() getMarginBox() setMarginBox() getContentBox() setContentSize() getPadExtents() getBorderExtents() getPadBorderExtents() getMarginExtents() isBodyLtr() docScroll() fixIeBiDiScrollLeft() |
dojo/dom-prop | DOM property functions | get() set() |
dojo/dom-style | DOM style functions | getComputedStyle() get() set() |
有一点一直贯穿整个现代DOJO的设计思路,那就是工具包的业务逻辑和访问是分离的。
如下面的代码所示:
1 var node = dojo.byId("someNode"); 2 3 // Retrieves the value of the "value" DOM attribute 4 var value = dojo.attr(node, "value"); 5 6 // Sets the value of the "value" DOM attribute 7 dojo.attr(node, "value", "something");
现代DOJO实现上面的功能代码:
1 require(["dojo/dom", "dojo/dom-attr"], function(dom, domAttr){ 2 var node = dom.byId("someNode"); 3 4 // Retrieves the value of the "value" DOM attribute 5 var value = domAttr.get(node, "value"); 6 7 // Sets the value of the "value" DOM attribute 8 domAttr.set(node, "value", "something"); 9 });
在这个现代dojo的例子中,非常清晰的展示了代码所做的工作。因为没有响应的参数传入,你编写的代码很难运行出不是你本意的效果。这种访问方式隔离机制一直贯穿这个现代dojo的设计思想。
数据仓库与存储
在dojo1.6时,新的API dojo/store被加入,而dojo/data API被废弃。但dojo/data的数据存储以及dojox/data数据存储相关的功能一直在dojo2.0之前都可以使用。但我们建议还是使用的新的API。本教程不能展开详细的介绍该概念,但你可以在Dojo Object Store教程中查看更详细的信息。
Dijit and Widgets
dijit为了适应现在dojo世界,也进行了一些重构。但很多重构都已经在toolkit包的基础部分完成,主要的重构目标是把功能分解成一个个小的积木,然后再粘合在一起,完成复杂的功能。如果你正在创建一个自定义的小部件,你可以阅读Creating a custom widget教程。
If you are just developing with dijits or other widgets, then there were a few core concepts that were introduced with the dojo/Stateful
and dojo/Evented
classes.
dojo/Stateful
provides discrete accessors for widget attributes as well as the ability to "watch" changes to these attributes. For example, you can do the following:
如果你只是使用dijits或者其他的小部件进行开发,这儿有一些核心的概念需要介绍一下,dojo/Stateful
and dojo/Evented
。
dojo/Stateful模块提供了访问小布家属性的接口,并且可以监测小部件属性的变化,例如:
1 require(["dijit/form/Button", "dojo/domReady!"], function(Button){ 2 var button = new Button({ 3 label: "A label" 4 }, "someNode"); 5 6 // Sets up a watch on button.label 7 var handle = button.watch("label", function(attr, oldValue, newValue){ 8 console.log("button." + attr + " changed from '" + oldValue + "' to '" + newValue + "'"); 9 }); 10 11 // Gets the current label 12 var label = button.get("label"); 13 console.log("button's current label: " + label); 14 15 // This changes the value and should call the watch 16 button.set("label", "A different label"); 17 18 // This will stop watching button.label 19 handle.unwatch(); 20 21 button.set("label", "Even more different"); 22 });
dojo/Evented
provides emit()
and on()
functionality for classes and this is incorporated into Dijits and widgets. In particular, it is "modern" to use widget.on()
to set your event handling. For example, you can do the following:
dojo/Evented为对象提供了emit()和on()函数,并让着两个函数成为dijits和小部件的一部分。在现代dojo代码中,会使用widget.on()函数去处理事件。例如下面的代码:
1 require(["dijit/form/Button", "dojo/domReady!"], function(Button){ 2 var button = new Button({ 3 label: "Click Me!" 4 }, "someNode"); 5 6 // Sets the event handling for the button 7 button.on("click", function(e){ 8 console.log("I was clicked!", e); 9 }); 10 });
Parser
最后是dojo/parser。dojo包含了编码式和声明式两种应用方式。使用dojo/paeser就可以把声明式的代码转换成dojo组件或对象。现代dojo的一些思想也已经对该组件产生了影响,并进行了重构。
doji仍然支持parseOnLoad:true这个配置,但更多的时候会有更明确的定义方式,例如:
1 require(["dojo/parser", "dojo/domReady!"], function(parser){ 2 parser.parse(); 3 });
dojo/parser模块的一个重大变化就是支持HTML5,并允许你标记HTML5节点。特别是把dojoType属性修改为data-dojo-type,并且代替非标准html的属性数据。所有的这些参数都会被传递给对象的构造函数。例如:
1 <button data-dojo-type="dijit/form/Button" tabIndex=2 2 data-dojo-props="iconClass: 'checkmark'">OK</button>
dojo支持在data-dojo-type属性上设置模块的ID。例如dojoType="dijit.form.Button"
变成了data-dojo-type="dijit/form/Button"。
在上述的dojo/Evented和dojo/Stateful的变化中有Watch和on函数功能,目前daojo/parser也已经响应了这些变化。例如:
1 <button data-dojo-type="dijit/form/Button" type="button"> 2 <span>Click</span> 3 <script type="dojo/on" data-dojo-event="click" data-dojo-args="e"> 4 console.log("I was clicked!", e); 5 this.set("label", "Clicked!"); 6 </script> 7 <script type="dojo/watch" data-dojo-prop="label" data-dojo-args="prop, oldValue, newValue"> 8 console.log("button: " + prop + " changed from '" + oldValue + "' to '" + newValue + "'"); 9 </script> 10 </button>
另外,parser也支持dojo/asoect的相关概念,所以你可以在上面的代码中使用Before、after等操作。更详细的信息可参考dojo/parser。
The dojo/parser
also supports auto-requiring in modules. This means you don't necessairly have to require in the module before invoking the require. If you set isDebug
to true
though, it will warn you if you are requiring modules this way
Builder
The final area to briefly touch on in this tutorial is the Dojo builder. In Dojo 1.7 it was completely rewritten. Partly it was to handle the significant changes with AMD, but it was also designed to modernize it and make it very feature rich. It is a topic too vast for this tutorial. You should read the Creating Builds tutorial for information, but be prepared to forget everything you knew about the old builder in order to embrace the "modern" builder.
最后的部分,我们讨论下dojo构建器,在dojo1.7时,该部分被重写。一部分变化时为了适应AMD架构,但其新的架构模式让其对外可以提供更加丰富的功能。这是一个很庞大的话题。你可以在Creating Builds教程中看到更多的详细信息。但是你要准备好忘掉一切旧的应用模式,拥抱新的现代的dojo构造器。
结论
希望你对进入现代dojo世界特别感兴趣,虽然需要您花费一些精力忘记旧的用法,去开始新的dojo之路,但一旦你开始行动,你就很难在回去了,你会发现已经开始使用模块化思想开发您的应用程序。
请记住现代dojo的使用方式:
- Granular Dependencies and Modular — only require what you need when you need it. Say goodbye to the "kitchen sink". It makes for faster/smarter/safer apps.
- Asynchronous — things do not necessarily happen in order, plan for code to operate asynchronously.
- Global Scope is Bad — one more time, repeat after me, "I will not use the global scope."
- Discrete Accessors — a function only does one thing, especially when it comes to accessors. There is a
get()
and aset()
for what you want to do. - Dojo complements ES5 — if EcmaScript 5 does something (and it is "shimmable") then Dojo doesn't want to do it.
- Events and Advice, not Connections — Dojo is migrating away from "generic" connections to focus on events and aspect oriented programming.
- The Builder is a Different Beast — it is much stronger, more powerful and feature rich, but it will only go to highlight bad design assumptions in "legacy" applications, not fix them.
- 细粒度依赖和模块化-按需加载模块,这将会让你的程序运行起来更加快速、轻便、安全。
- 异步的-代码不会立即执行,都是异步的。
- 全局命名变量是不可取的-再一次的跟我重复。I will not use the global scope。
- 离散访问器-一个函数制作意见事,尤其涉及到访问器的时候。会有一个get()和set()函数知道你想做什么。
- Dojo补充了ES5-如果EcmaScript 5能够实现某个功能(而且是很简单的实现),那么Dojo就不会再实现这个功能了。
- 使用事件和通知,而不是连接-Dojo已经把来接功能改变成使用事件和定向通知进行编码。
- The Builder is a Different Beast — it is much stronger, more powerful and feature rich, but it will only go to highlight bad design assumptions in "legacy" applications, not fix them.