最近在做一个公司的项目,我们公司的角色是前端制作(HTML+CSS+JS),开发方为IBM,所以前端的Javascript被指定了使用dojo。本来是没有我什么事情,可是偏偏眼睛里不容沙子,非得要求重构,于是开始漫漫的HTML重构和使用dojo的过程。初初上手dojo,真的觉得十分苦恼,因为介绍它的文档实在少之又少,尤其是1.7版本。偏偏1.7版本又是dojo一个较为重要的升级版本——引入了AMD的机制,所以想要把代码写得更加dojo些,还着实花了我不少的精力去看这个新版本的源代码,包括dijit和dojox部分。
基于dojo 1.7的js目录规划
为什么要说这点呢,因为实现了AMD机制的,如require.js的,对目录的规划尤为重要,因为涉及到require加载的模式,而dojo 1.7尤甚。
基本上,dojo1.7已经默认会认为你的目录是基于如下的规划的:
/webroot/js/dojo/
/webroot/js/dijit/
/webroot/js/dojox/
则,当一个/webroot/index.html的页面,尝试按照上述目录加载的时候:
<script type="text/javascript" src="js/dojo/dojo.js"></script>
它会默认认为,js的目录,为所有modules的基础目录,即当你希望添加一个叫做app的新module时:
define('app', ['dojo/_base/kernel'], function(dojo) { return { anyMethod: function() { // ... } } }); require(['app'], function(app) { app.anyMethod(); });
而该文件则放在/webroot/js/app.js目录下即可。
当然,dojo本身提供多种声明dojoConfig的模式,但是根据我的尝试,会认为上述的实践模式会令项目较为简单且易于他人理解。dojo提供了baseUrl用于声明require的基础路径入口,我尝试了,由于dojo本身的目录层级问题,会存在一些小问题,类如:
baseUrl: 'js/modules' // 尝试告诉dojo,所有项目自定义的modules的目录
而由于dojo的目录层级关系(js/dojo,为该目录的上一级),他会同样认为你声明的这个目录也是在js/module的上一级,即还是指向了js这个目录。当然,我也可以将baseUrl改为js/modules/any,来强迫使他指向js/modules,但这实在不雅。
当然,实在不行,你还是可以通过声明paths、packages等模式来强迫指定某个基础的paht、package的入口,但是总归来说,还是让理解项目结构、代码等造成更大的困扰性,都就此作罢。既然dojo本就如此,就按照回他本身默认的机制、设定去规划目录,将会是走最少弯路的实践模式。
dojoConfig声明模式和重要参数说明
dojoConfig声明模式支持3种:
<script type="text/javascript"> var dojoConfig = { async: false // more ... } </script> <script type="text/javascript" src="js/dojo/dojo.js"></script>
<script type="text/javascript" src="js/dojo/dojo.js" data-dojo-config="async: false, parseOnLoad: false, isDebug: true"></script>
<script type="text/javascript" src="js/dojo/dojo.js"></script> <script type="text/javascript"> require({ async: false, parseOnLoad: false }); </script>
实践证明,第二种模式会是比较易于理解,且又不会在页面写太多代码的模式。
而dojoConfig中,比较重要的参数即为async和parseOnLoad(就我目前的使用程度来说),简单介绍如下:
async: true | false | legacyAsync
async表示的是dojo core(其实就是dojo/dojo.js所定义的模块、方法)是否使用异步的模式加载,而其中,我至今未试验出legacyAsync的模式为何,而true | false主要差异为:
async: true,启用异步模式,则不存在全局对象dojo,而且你即使:
require(['dojo/_base/kernel'], function(dojo) { console.log(dojo); });
你会发现dojo的实例里面,只包含了最精简的一些方法和api,你需要逐个逐个你需要使用的module去将其加载进去。
而使用了false,则dojo/dojo.js中声明定义了的API,他都会以全局的dojo实例装载之。
使用true和false,分别有其用途,尤其在使用了true,我认为,是可以同时使用多个不同版本的dojo实例分别使用的(只是显得有些叶公好龙、画蛇添足)。
而parseOnLoad,则是指dojo在页面自动加载时,会使用dojo/parser库自动解析页面指定了data-dojo-*的DOM标签,比如<select data-dojo-type="dijit.form.ComboBox">,自动转化为dijit.form.ComboBox的widget。不过话说,如果你启用了parseOnLoad: true,碰到页面有这种data-dojo-type,而你又没有事先require了相关的类库,他是会抛出一个js error的。而且说句实话,光是一个dijit.form.ComboBox,他就会require一大堆js文件,真是为这个页面感到费劲啊。
DOM基础使用习惯
据我所知,dojo曾在某一段时间内和MooTools合作过,dijit就是他们合作后的产物之一,而包括像David Walsh等,这些MooTools开发团队成员,也在自己的博客大力鼓吹Dojo(至今),为其拉广告、做宣传,所以可以想见dojo受MooTools的影响之深。
而事实上,dojo的DOM方法中,存在和MooTools极其接近的定义结构,他同时支持两种DOM查询模式:
1、传统的byId的模式,dojo采用的是dojo.byId('elId'),而MooTools则是document.id('elId')。
2、xpath的查询模式,dojo为dojo.query('#id'),而MooTools则是,document.getElements('#id')。
为何说dojo更加类MooTools而不是说类jQuery呢(同样支持xpath的query),基于以下几点:
1、jQuery的概念里面,是不存在支持byId的查询模式的。
2、 jQuery的操作模式里面,是不支持批量操作的,即,jQuery('.submit-btn').css(),表示的是对该query的第一个element进行操作 , 而MooTools和dojo的操作是批量的操作,即:document.getElements('.submit-btn').addClass('focus'),或者dojo的dojo.query('.submit-btn').style({ color: 'red' })
3、而且从定义对象结构上来说,dojo更像MooTools,即:
dojo:
dojo.byId() -> HTML Element实例
dojo.query() -> dojo.NodeList类实例
MooTools:
document.id() -> Element类
document.getElements() -> Elements类实例
4、链式操作更类似MooTools,即:
dojo:
dojo.query('.any-class').query('anySubElement').doSomeThing();
MooTools:
document.getElements('.any-class').getElements('anySubElement').doSomeThing();
所以在使用dojo的DOM方法的时候,最好养成一个统一的使用习惯,不然一会儿dojo.byId,一会儿dojo.query,非让日后跟进代码的人看得头冒青烟不可。
在这里,我推荐使用dojo.NodeList的模式作为基础操作,原因基于如下:
1、dojo.NodeList更易于使用链式操作模式,即:dojo.query('.any-class').attr({ }),但是千万注意,这里会对所有query到得element做同样的attr操作。
2、dojo为了控制性能和不污染原生DOM对象,不采用原型链的方式去扩展DOM Element,于是操作一个原生的DOM Element在dojo里的做法显有些蹩脚,如:dojo.attr(DOMElement, {}),dojo.style(DOMElement, {}),使用起来真的不太舒服。
于是有人会问,假定我已经获得了某个DOMElement,那如何能快速的进行操作呢?以下举个例子:
// $ 伪装成大家习惯的jQuery操作符 require(['dojo/_base/kernel', 'dojo/dom', 'dojo/query'], function(dojo, dom, $) { var el = dom.byId('el_id'); // 正统的操作模式应该是: dojo.style(el, { display: 'none' }); // 不过事实上,你可以这样写,会让你的代码写得舒服,别人读起来也更加舒服 $(el).style({ display: 'none' }); // 这个例子是可以延伸发展下去的: $($('body')[0]).style({ display: 'none' }); // 这是类jQuery的查询模式 $($('#any_id')[0]).style({ height: 100 + 'px' }); });
如果$('#any_id')[0],不存在,则$($('#any_id')[0]),的NodeList内容是空的,则其后的style操作也不会继续进行,这可以缓解很多时候,为了判断某个element是否存在,而写了大堆if ... else的判断结构。
dojo的DOMEvent
dojo的DOM事件,潜伏得比较深,不过为了深入简出,这里只谈两个问题:
1、怎么快速绑定事件?
2、怎么stop一个事件。
基于上述的NodeList的操作习惯,有以下的代码:
require(['dojo/_base/kernel', 'dojo/query'], function(dojo, dom, $) { $('body').connect('mousemove', function(event) { }); $('tag').on('click', function(event) { }); });
connect是 dojo一个比较特殊的方法,他用于实现了对任意对象,实现事件注入(类似Qt框架的信号槽的机制),很多对象,类如 dojo.Animate的事件绑定,也是经过此方法绑定。
stopEvent的方式:
require(['dojo/_base/kernel', 'dojo/query', 'dojo/_base/event'], function(dojo, $) { $('body').connect('mousemove', function(event) { dojo.stopEvent(event); // 是不是有点好像以前写JS原生的stop event呢? }); });
和jQuery不同,jQuery的事件只要return false即可, dojo需要额外引入 dojo/_base/event,并执行 dojo.stopEvent(event),这让我想很早以前兼容IE和Firefox时候的写法,event = event || window.event。
Animate的使用
dojo的Animate,顶多只实现到了mootools的tween的级,还没到Fx.Morph。他实现代码上,当你使用dojo.animateProperty创建一个新的animate实例时,他会不断的创建新的animate实例,如果大量的这样写,你会发现你写的JS效果,大量存在:非同步,抖动厉害的情况出现。不过这个问题,也非一时半刻能说得明白的问题,对于简单的特效,还是可以使用dojo.animateProperty的,但是对于稍微复杂一些的效果的时候,则需要注意用法,这里简单说明一下:
如上图显示,眼下要做一个Slider,里面除了有两个toPrev、toNext两个操作按钮外,中间有一个存放图片的容器,这个容器用于展现该容器内的图片的滚动。
假定我们限定了SliderWrap的宽度,比如370px,使其永远只会显示两张图片,要显示更多的图片,我们将对SliderWrap使用margin-left,使其margin-left为-xxxpx来实现其slider的滚动。
一个基本的做法就是:
$('#toPrev').connect('click', function() { dojo.animateProperty({ node: $('#SliderWrap')[0], properties: { marginLeft: anyPx } }).play(; }); $('#toNext').connect('click', function() { dojo.animateProperty({ node: $('#SliderWrap')[0], properties: { marginLeft: anyPx } }).play(); });
但是,一个预料之外的事情的发生了,用户狂点toNext的按钮,这时候,你的slider滚动的就没有那么流畅了,而是会出现卡顿、不流畅、不自然等情况发生(因为多次click的事件被执行了)。使用click作为触发的event还是比较好处理的,因为他必须满足click的条件——mousedown & mouseup,如果你将触发的事件设置为mouseenter(mouseover),恐怕情况会急剧的恶化下去。
如果用MooTools来实现同样的效果,会考虑改为如下:
var fx = new Fx.Morph($('SliderWrap')); $('toPrev').addEvent('click', function() { fx.pause().start({ 'margin-left': anyPx }); }); $('toNext').addEvent('click', function() { fx.pause().start({ 'margin-left': anyPx }); });
差异主要在于pause(),假定用户以很高频率点击toNext,那么不管之前fx播放到什么情况,都先暂停执行,而后将fx播放至用户当前的请求操作下。
而同样的,在dojo,则需要这样做:
var anim = dojo.animateProperty({ node: $('#SliderWrap')[0] }); $('#toPrev').connect('click', function() { anim.properties = { marginLeft: anyPx }; anim.stop().play(); }); $('#toNext').connect('click', function() { anim.properties = { marginLeft: anyPx }; anim.stop().play(); });
附录1:常用方法整合
至此,dojo使用上的一些小问题,已经基本解决,不过为了辅助dojo的编程,我还是写了几个小方法,为了改善dojo的编程代码过于冗余,过于繁重的情况,附录如下:
(function(app) { /** * 获取item的类型,即typeof item === ? * * 取自MooTools 1.4 */ var typeOf = this.typeOf = function(item) { if (item == null) return 'null'; if (item.nodeName){ if (item.nodeType == 1) return 'element'; if (item.nodeType == 3) return (/\S/).test(item.nodeValue) ? 'textnode' : 'whitespace'; } else if (typeof item.length == 'number'){ if (item.callee) return 'arguments'; } return typeof item; }; /** * 判断某个item是否为某个对象的实例 * * 取自MooTools 1.4 */ var instanceOf = this.instanceOf = function(item, object) { if (item == null) return false; var constructor = item.$constructor || item.constructor; while (constructor){ if (constructor === object) return true; constructor = constructor.parent; } /*<ltIE8>*/ if (!item.hasOwnProperty) return false; /*</ltIE8>*/ return item instanceof object; }; define('app', ['dojo/_base/kernel', 'dojo/_base/lang', 'dojo/dom', 'dojo/query', 'dojo/_base/NodeList', 'dojo/_base/event'], function(dojo, lang) { /** * 传入DOM Element | dojo.NodeList | string,取出指定index的DOM Element * 为了在某些场景需要明确的操作某个DOM Element使用 * * @param DOMElement|dojo.NodeList|string el * @param int index * @return DOMElement|false */ var element = app.element = function(el, index) { if (!index) index = 0; if (typeOf(el) == 'string') { var elById = dojo.byId(el); if (el.match(/\#|\.|\s/, el) || !elById) el = dojo.query(el); else el = elById; } if (typeOf(el) == 'object' && instanceOf(el, dojo.NodeList)) el = el[index]; return typeOf(el) == 'element' ? el : false; }; /** * 执行animate,并返回该animate对象 * * @param DOMElement|dojo.NodeList|string el * @param object styles 需要animate执行的样式 * @param object args 其他animate参数 * @return dojo.Animate */ var animate = app.animate = function(el, styles, args) { if (element(el) !== false) { if (typeOf(args) === 'function') { args = { onEnd: args }; } else { args = args || {}; } args.node = element(el); args.properties = styles; return dojo.animateProperty(args).play(); } return false; }; /** * 生成animate对象,但不play * * @param DOMElement|dojo.NodeList|string el * @param object styles 需要animate执行的样式 * @param object args 其他animate参数 * @return dojo.Animate */ var animateObject = app.animateObject = function(el, styles, args) { if (element(el) !== false) { if (typeOf(args) === 'function') { args = { onEnd: args }; } else { args = args || {}; } args.node = element(el); args.properties = styles; return dojo.animateProperty(args); } return false; }; /** * 当某个DOMEvent发生时,判断当前DOMEvent是否经过某个DOMElement * * @param DOMElement|dojo.NodeList|string el * @param DOMEvent event * @return bool */ var hovered = app.hovered = function(el, event) { if (element(el) === false) return false; var coords = dojo.coords(element(el)); var x = event.pageX, y = event.pageY; return (x >= coords.x && x <= coords.x + coords.w && y >= coords.y && y <= coords.y + coords.h) }; return app; }); }).call(this, this.app || {});
整体而言, dojo还是一个很不错的框架,只是文档少得有些可怜,而且大多比较老,想用好 dojo还是得费一番心思才行。