你可能没在使用Dojo,或者在1.8 版本中依旧使用 1.6的代码而不知道如何继续。 你一直在听说 "AMD" 及 "baseleass",但不知道如何去做或从哪开始。 本教程就是关于"AMD"以及Dojo的一些特性。
开始
从Dojo 1.7开始, Dojo Tookit 开始朝现代化架构转变。 在Dojo 1.8 继续了这种转变。 虽然它广泛的向后兼容(兼容1.6之前的版本), 但为了充分的利用 Dojo 1.8,许多基础的理念发生了改变。 这些理念将形成Dojo 2.0 的基础。 所以为了直接充分的应用新的特性(例如 dojo/on), 你需要采用这些 ”现代“的理念。
在整个教程中, 我会解释这些被Dojo引入的理念。 它们可能被划分为 "legacy" 和 "modern" Dojo. 我会尽我所能的解析为什么需要改变以及如何改变。 虽然有的是改变基本原来,看起来会让人困惑, 但它们会使你的代码更加有效, 运行更快, 更容易维护。 很值得你投入时间去理解 ”modern" Dojo.
这个教程不是一个迁移向导, 在这里讲解的都是基础的概念。 详细的迁移请参考
Dojo 1.x to 2.0 Migration Guide.
Hello New World
"modern" Dojo 核心概念之一是事物(变量或者方法)保存在全局命名空间中,这是非常不好的。 不好原因有很多种, 如在一个复杂的web应用程序中, 全局的命名空间会被各式各式的代码污染, 特别是在使用多个javascript框架的情况下。 在安全角度上,也需要考虑人们非法修改全局命名空间。 这个意思是在"modern" Dojo中, 如果你正打算从全局环境中访问某些事物, 请不要这样做。虽然此刻还有大量的toolkit为了兼容性的原因,大部分还是全局的。但这不应该出现在新的开发中。
* 如果你发现你在使用 dojo.* 或者 digit.* 或 dojox.*,这些都是错误的
这意味着虽然 开发者还在使用 "legacy" dojo. 仅仅包含 dojo.js, 获得基本的核心功能, 然后在请求其它的模块, 仅输入dojo.something 到你的核心内容(虽然在2.0之前会被广泛使用, 但这真的是一件非常非常糟糕的事情。)
自己多重得一样" 全局空间是不好的,全局空间是不好的。 我不在使用全局空间。 我不在使用全局空间”。
别外一个核心概念是同步操作是非常慢的,而异步操作更快。 “legacy" Dojo 虽然已经有了异步操作的概念 dojo.Deferred. 但是在”modern" Dojo中, 你应当认为任何操作都是异步的。
为了强化 Dojo的现代化和利用以上的概念(去全局化及异步操作), 在 1.7 中 Dojo 采用了CommonJS 模块定义被称为 Asynchronous Module Definition(AMD). 它重写的Dojo 模块加载器的基本原理, 通过暴露 require() 和 define() 函数来使用加载器。 你可以查看之前翻译的
dojo loader.
让我们看看之前在"legacy"中的例子:
dojo.ready(function(){ dojo.byId("helloworld").innerHTML = "Hello World!"; });
在 modern 中的版本
require(["dojo/dom","dojo/domReady!"],function(dom){ dom.byId("helloworld").innerHTML = "Hello New World!"; });
"modern" dojo的基础就是require()函数。 它给javascript代码提供了闭包并且将需要相关工作的模块作为参数传递给require函数。 典型的, 第一个参数是一个模块IDs的(MIDs)数组, 第二个为一个函数。 在require()的闭包内。 我们引用的模块是基于在传递参数的变量名。 虽然我们可以调用一个模块的任何东西, 但也有一些规范需要遵守。
加载器, 就是传统的一样, 先是寻找模块,在加载和管理模块。
你可能已经注意到了,在依赖数组中有一个模块没有返回变量, dojo/domReady!. 它实际上加载一个“插件”,用来控制加载器的行为。 确保加载器在整个页面的 DOM结构都好后在执行回调函数。 在异步环境中, 不能够假设DOM结构已经好了,你可以操作DOM. 所以如果你想要在你的代码里操作DOM, 你必须加载domReady这个插件。 由于在回调函数内不需要使用到这个插件的返回值, 通常的惯例是将它放在数组的末尾。
你也可以在模块已经加载完后,通过 require()来获得这个模块的引用。 只需要给require传递一个MID 字符串参数。 它不会加载一个模块,如果一个模块没有被加载,会抛出一个错误。 你不会在Dojo Toolkit看到这种编程风格。 因为我们更希望在依赖数组里来管理模块。 如下是通过require来获得引用的方式:
require(["dojo/dom"],function(){ // some code var dom = require("dojo/dom"); // some more code });
Dojo 基础 和核心
你可能在处理"modern " Dojo时听说过 "baseless" 这个术语。 意思是确保一个模块不需要依赖任它不需要的 Dojo 基础功能。 在" legacy" 环境中, dojo.js 会有非常多的基础功能(在2.0之前都会存在)。 但为了更好的迁移, 你应该停止使用dojo.*.
* dojoConfig的配置选项async. 默认为false, 它的意思是所有的 Dojo 基础模型会自动被加载。 如果你设置为true. 利用加载器的异步模式。 这些模块不会被加载。
此外, Dojo 包含 EcmaScript5的特性, 并有可能, 会摒弃在非现代化的浏览器下会模似ES5的特性的形为。这样,Dojo就不会适应所有的情形了。
虽然参考指南已经更新,会告诉你基础功能在哪。 你可以在
basic functions 找到相应的参考
基础及核心的所用,如下所示
dojo.require("dojo.string"); dojo.byId("someNode").innerHTML = dojo.string.trim(" I Like Trim Strings你现在可以使用如下方式
require(["dojo/dom","dojo/string",function(dom,string){ dom.byId("someNode").innerHTML = string.trim(" I Like Trim Strings "); });
事件与通知
虽然dojo.connect() 和 dojo.disconnect() 已经被移到dojo/_base/connect 模块中。 但 "modern" dojo 应该在使用dojo/on来处理事件和djojo/aspect来通知事件时应该遵循原有的模式。 更深层次的事件教程请参考
Events,但在这我们要讲一些不同的东西。
在 "legacy" Dojo中, 事件和修改方法行为没有很明显的区别, 通常dojo.connect() 可以用来处理两者。 事件是处理发生在一个对象上的相关事情,如点击事件。dojo/on 可以无缝的处理本地DOM事件和由Dojo对象或者部件窗口触发的事件。 在修改对象行为上, advice(通知)是面向
方面编程中的一个概念(术语),用来给接入点(或方法)添加额外的功能。大部分Dojo都尊循AOP规范,dojo/aspect模块集中展示了这种机集(体现AOP软件设计模式)。 --可以参考和了解下AOP,以及在Javascript中的实现。
在"leagacy" Dojo中,我们可能有很多种方法来处理按钮的onclick事件:
<script> dojo.require("dijit.form.Button"); myOnClick = function(evt){ console.log("I was clicked"); }; dojo.connect(dojo.byId("button3"),"onclick",myOnClick); </script> <body> <div> <button id="button1" type="button" onclick="myOnClick">Button1</button> <button id="button2" data-dojo-type="dijit.form.Button" type="button" data-dojo-props="onClick: myOnClick">Button2</button> <button id="button3" type="button">Button3</button> <button id="button4" data-dojo-type="dijit.form.Button" type="button"> <span>Button4</span> <script type="dojo/connect" data-dojo-event="onClick"> console.log("I was clicked"); </script> </div> </body>
在 "modern" Dojo中, 只能用 dojo/on来处理。 dojo/on可以在编程时或者button声明时指定,而不用提心处理的事件是DOM事件还是Digit/widget事件。
<script> require([ "dojo/dom","dojo/on","dojo/parser","dijit/registry","dijit/form/Button","dojo/domReady!" ],on,parser,registry){ var myClick = function(evt){ console.log("I was clicked"); }; parser.parse(); on(dom.byId("button1"),"click",myClick); on(registry.byId("button2"),myClick); }); </script> <body> <div> <button id="button1" type="button">Button1</button> <button id="button2" data-dojo-type="dijit/form/Button" type="button">Button2</button> <button id="button3" data-dojo-type="dijit/form/Button" type="button"> <div>Button4</div> <script type="dojo/on" data-dojo-event="click"> console.log("I was clicked"); </script> </button> </div> </body>
* 注意digit.byId不能在使用。 在"modern" Dojo中, digit/registry 被用于窗口部件(widgets)中, registry.byId 来获得相应的窗口部件。需要注意的是dojo/on可以用来处理DOM 节点以及widget的事件。
var callback = function(){ // ... }; var handle = dojo.connect(myInstance,"execute",callback); // ... dojo.disconnect(handle);
在 "modern" Dojo中, dojo/aspect模块允许你从一个方法中获得一个通知,并且给别一个方法添加 "before","after" 或者 "around"等行为。 一般的,你可以用aspect.after() 代替dojo.connect()。如下:
require(["dojo/aspect"],function(aspect){ var callback = function(){ // ... }; var handle = aspect.after(myInstance,callback); // ... handle.remove(); });
* 阅读 dojo/aspect参考指南以及 David Walsh's 的博客 dojo/aspect 获得更多信息
Topics (发布与订阅专题)
例如,"legacy" dojo的publish/subscribe 如下实现:
// 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);
在"modern" Dojo中,你需要利用dojo/topic模块来做相同的事情:
require(["dojo/topic"],function(topic){ // To publish a topic topic.publish("some/topic",1,3); // To subscribe to a topic var handle = topic.subscribe("some/topic",function(arg1,arg2,arg3){ // ... }); // To unsubscribe from a topic handle.remove(); });
* 查看更多 dojo/topic 参考指南
* 注意现在的publish参数不在是一个数组。
Promises(承诺)
Dojo一直最核心的概念是Deferred类, 虽然在Dojo 1.5中变为了 "Promises" 的结构体系,但还是值得我们在此讨论。 另外, 在Dojo 1.8中, promise 的API已经被重写。 虽然跟之前的语义(功能)一样,但它不在支持 "legacy" API。 所以如果你想使用它, 你必须采用 "modern" API. 在 "legacy" Dojo 你可以如下使用 Deferred:
function createMyDeferred(){ var myDeferred = new dojo.Deferred(); setTimeout(function(){ myDeferred.callback({ success: true }); },1000); return myDeferred; } var deferred = createMyDeferred(); deferred.addCallback(function(data){ console.log("Success: ",data); }); deferred.addErrback(function(err){ console.log("Error: ",err); });
"modern" Dojo如下使用:
require(["dojo/Deferred"],function(Deferred){ function createMyDeferred(){ var myDeferred = new Deferred(); setTimeout(function(){ myDeferred.resolve({ success: true }); },1000); return myDeferred; } var deferred = createMyDeferred(); deferred.then(function(data){ console.log("Success: ",data); },function(err){ console.log("Error: ",err); }); });
* 关于更多 Deferreds(异步执行队列), 请查看 Getting started with Deferreds 教程。 关于Promises 请查看 Promises基础
* dojo/DeferredList 虽然还存在,但已被弃用。 你可以参考更强的
dojo/promise/all 和
dojo/promise/first
请求(requests)
任何一个javascript库中,其中核心的原理之一是AJAX. Dojo 1.8的Ajax基本构造API已被更新, 可跨平台运行, 易扩展, 提高代码的重用。 之前,你可能需要在XHR,Script,IFrame IO 通信中转来转去, 以及经常要自己处理外部返回的数据。 现在完全用dojo/request处理所有的问题。
dojo.xhrGet({ url: "something.json",handleAs: "json",load: function(response){ console.log("response:",response); },error: function(err){ console.log("error:",err); } });
"modern" Dojo 可以如下实现:
require(["dojo/request"],function(request){ request.get("something.json",{ handleAs: "json" }).then(function(response){ console.log("response:",function(err){ console.log("error:",err); }); });
* dojo/request 会根据你的平台来加载最合适的请求处理方式, 对于浏览器来说一般是 XHR. 以上的代码可以轻易的在NodeJS中使用。 而你不需要修改任何代码。
这也是一个广泛讨论的话题, 可以查看
Ajax with dojo/request 教程来了解更多。
Document Object Model(DOM) 操作
如果你是从头开始阅读本教程,你可能发现了一些趋势, Dojo不仅抛弃了对全局空间而采用了新的模式, 它也将许多核心功能封装对模块中. 那么对于一个Javascript toolkit 来说, 什么还比DOM操作更核心的呢?
还好,现在 DOM 操作也被分解为更多的模块。 以下是DOM操作的模块摘要信息:
Module | Description | Contains |
---|---|---|
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() |
var node = dojo.byId("someNode"); // Retrieves the value of the "value" DOM attribute var value = dojo.attr(node,"value"); // Sets the value of the "value" DOM attribute dojo.attr(node,"value","something");
而 "modern" Dojo如下,需要指定两个依赖:
require(["dojo/dom","dojo/dom-attr"],domAttr){ var node = dom.byId("someNode"); // Retrieves the value of the "value" DOM attribute var value = domAttr.get(node,"value"); // Sets the value of the "value" DOM attribute domAttr.set(node,"something"); });
在 "modern" 的例子中, 它非常的清楚你的代码需要什么, 但也增加或者缺少某个参数时,也会有意想不到的问题。 这种分离的访问器始终贯穿整个 "modern" Dojo.
DataStores 对比 Store
在 Dojo 1.6中, 引入了新的dojo/store API, 而弃用 dojo/data API. 虽然在 Dojo 2.0 之前还会保持 dojo/data 和dojox/data,但最好还是将它们迁移到 dojo/store. 在这里我们不在详细讲述数据存储这个主题,但更多的信息可以查看
Dojo Object Sotre.
* 关于 dojo/store 可以查看
dojo/store 参考指南
Dijit 和 窗口部件(Widgets)
Dijit 自已在 "modern" Dojo中也发生了转变, 但大部分是在基础部分的改变, 在功能上被分划成独立的部件,然后在互相结合,完成更复杂的功能。 如果你正在创建一个自己的窗口部件, 你应该阅读一下
Creating a custom widget 指南.
如果你仅仅想在 digits或者其它的 Widgets的基础上开发, 那么需要介绍一下 dojo/Stateful 和 dojo/Evented 类。
require(["dijit/form/Button",function(Button){ var button = new Button({ label: "A label" },"someNode"); // Sets up a watch on button.label var handle = button.watch("label",function(attr,oldValue,newValue){ console.log("button." + attr + " changed from '" + oldValue + "' to '" + newValue + "'"); }); // Gets the current label var label = button.get("label"); console.log("button's current label: " + label); // This changes the value and should call the watch button.set("label","A different label"); // This will stop watching button.label handle.unwatch(); button.set("label","Even more different"); });
dojo/Evented 给类提供了一个 emit() 和 on() 函数, 并且做为Dijits和widgets的一部分。 可以使用widget.on()来设置事件处理。 如下所示:
require(["dijit/form/Button",function(Button){ var button = new Button({ label: "Click Me!" },"someNode"); // Sets the event handling for the button button.on("click",function(e){ console.log("I was clicked!",e); }); });
解析 (Parser)
最后来讲一下dojo/parser. Dojo 在代码编程(纯Javascript代码)和标签声名中都很有优势。 使用dojo/parser 可以解析标签声名并转变为一个实例对像和widgets. 所有上面提及到的 "modern"思维对dojo/parser很有影响, dojo/parser也有一些自己的现代化的转变。
require(["dojo/parser",function(parser){ parser.parse(); });
Parser另一个“大”的改变是支持HTML5属性来标记节点。 它允许你的 HTML 标签可以通过HTML5的校验。 尤其是 dojoType 变成了data-dojo-type, 而不是指定的对象参数为无效的 HTML/XHTML属性,所有的参数会被传递给 data-dojo-props中指定的对象构造函数。
<button data-dojo-type="dijit/form/Button" tabIndex=2 data-dojo-props="iconClass: 'checkmark'">OK</button>
* Dojo 支持在data-dojo-type中使用 Module ID(MID)。 例如 dojoType="digit.form.Button" 变为 data-dojo-type="digit/form/Button"
对于上面提及到的dojo/Evented 和 dojo/Stateful,parser 也可以在声明角本中复制到相对应的功能, type 声明我 dojo/on 直复制"on"的功能,声明为 dojo/watch则复制"watch"的功能。 如下:
<button data-dojo-type="dijit/form/Button" type="button"> <span>Click</span> <script type="dojo/on" data-dojo-event="click" data-dojo-args="e"> console.log("I was clicked!",e); this.set("label","Clicked!"); </script> <script type="dojo/watch" data-dojo-prop="label" data-dojo-args="prop,newValue"> console.log("button: " + prop + " changed from '" + oldValue + "' to '" + newValue + "'"); </script> </button>
另外, parse也支持之前介绍过的 dojo/aspect,你可以为 “before","after" 和 "around"提供相应的代码。 查看 dojo/parser 参考指南获得更多信息。
Builder
最后一个是教程中暂时还没有接触的领域, Dojo Builder. 在 Dojo 1.7中, 它完全被重写了。 部分由于AMD的变化, 更重要是它被设计的更现代化,功能更丰富。 讲解Build 已经超出了该指南的范围。 你应该阅读 Creating Builds 指南来获得更多的信息。但在使用 "modern" builder之前,最好忘记旧的builder的版本。
总结
希望你对 "modern" dojo有了兴趣, 虽然从"legacy"到 "modern"需要一段时间的适应。 但是一旦你适应了 "modern" dojo,你就很难回到”leagacy" Dojo.
"modern" Dojo有以下特性:
- 颗粒化的依赖和模块化—— 仅当你需要什么的时候在加载, 对激进的现实主义(不管有用没有全部加载)说再见。 创建更快/更智能/安全的应用。
- 异步——事情没必要按给定的顺序发现,编码时多使用异步操作。
- 全局作用域是不好的—— 请一次次的跟着我念, "我不会使用全局作用域“
- 离散的访问器—— 一个功能只做一件事情, 特别访问器(accessors),只有一个get() 和 set()方法。
- Dojo补充了ES5——如果EcmaScript 5在做某些事情,则Dojo不会在做。
- 事件和通知,而不是相连的——Dojo 已经从”一般”的连接迁移分别迁移到了事件和面向方面编程\
- Builder——它非常强大,功能更加丰富。