最近准备在一个项目上使用Node.js,摸索了一段时间之后基本能够运用起来。由于自己一直做的是JavaEE开发,所以比较习惯于使用面向对象的方式进行开发。而接触前端的第一个框架是Dojo,同样提供了面向对象编程的机制。所以就想着是否在后台node.js上通过加载Dojo库的一些纯javascript的模块来进行面向对象开发。google了之后的确发现了几篇介绍在Node上运行Dojo的文章,先贴在这里:一个是我们公司的Dojo部门的同事写的在 Node.js 上使用 Dojo,还有一篇是一个比较完整的介绍node.js和dojo结合的方案:node.js与dojo完美的融合-开发完全面向对象化。还有一个外国的哥们用dojo写了一个框架,支持在node.js上的web应用开发,叫dojos,有兴趣的同学可以到github上把代码下clone下来看看:https://github.com/supnate/dojos。
既然别人都已经写好了,那么我在这里说什么呢?。。。哈哈,所谓教学相长,把学会的东西讲清楚其实也是巩固知识的过程。并且这段时间的摸索下来,我对这种开发方式也有一些自己的看法。
面向对象
首先谈谈面向对象,最大的三个特点,封装、继承、多态,相信很多使用过面向对象开发方式的同学肯定已经习惯与这种方式了。尽管面向对象并不是javascript这种脚本语言的主旨所在,但是能够以这种方式吸引更多的程序员来接受和更快上手javascript,这对与js的发展是有好处的。况且,使用这种方式编程的时候并不会让你丢失掉javascript本身的特点,你依然可以使用函数式编程,使用闭包的特性,可以动态地给对象添加属性,可以使用javascript的反射机制。只是在处理一些复杂的层次结构和业务逻辑的时候使用面向对象的方式,能够使得整个程序更加结构化,以及有更好的可重用性。同时很多设计模式也可以在项目中使用,这是意见多么美好的事情,何乐而不为呢。
javascript原型继承
熟悉javascript的同学都知道js是基于原型继承的对象语言。想要一个对象继承自另一个对象,要将这个构造函数的prototype属性指向父类的对象。对js的原型和继承还有疑惑的同学可以多找几篇文章或者书本看看。这里推荐一篇我经常拿出来翻的js原理介绍的比较全面的文章:使用面向对象的技术创建高级 Web 应用程序,还有一本@lexlian大牛推荐我看的js经典书《JavaScript The Good Parts》,由于封面上印着一只蝴蝶,所以也被称为蝴蝶书。 不过总的来说,要使用javascript的原型继承,是一种相对比较麻烦的方式。
dojo面向对象
那么我们就来看看dojo是如何支持面向对象的。
declare
declare是dojo提供的定义类的函数,基本用法如下:
declare("ClassX", // 类的名字 [superClass1, superClass2], // 父类们 // 要加入新定义类的原型的所有属性及方法 { messageX: null, constructor: function(msgX) { this.messageX = msgX; }, sayMessageX: function() {
this.inhereted(arguments); alert("hi, this is " + this.messageX); } });
从上面的代码中可以看出,dojo定义类以及继承父类的方式还是比较方便的,并且dojo还支持多继承,想想如果要用prototype来写多继承该是多么麻烦的事情啊。
大家可以看到这个类里面有个constructor函数,没错就是这个类的构造函数,dojo的子类在被实例化的时候,会自动调用所有父类的构造函数。所以在设置一些属性的时候,只需要基础父类中调用lang.mix(),就可以很方便的将所有传进来的属性融入新的对象中,而不需要一个个属性逐个设置。这就是我之前说的,可以在面向对象的方式中继续使用脚本语言的灵活性和动态性。
在子类的方法中,调用this.inhereted(arguments),即可以随时随地的调用父类的同名方法。这比java的super()只能在方法开始的时候调用灵活多了。
加载模块
Node.js和Dojo的模块加载机制在 Node.js 上使用 Dojo这篇文章中介绍的很详细了。这里就直接引用了:
Node.js 和 Dojo 都遵循 CommonJS 的模块相关规范。Node.js 支持的是 Module1.0 规范,而 Dojo 支持的是 AMD(Asynchronous Module Definition,即异步模块定义)规范。虽然是两个规范,但它们都是描述了模块的定义和加载机制,有很多共同点。这就为 Dojo 运行于 Node 提供了天然的基础。在规范中,JavaScript 文件和模块是一一对应的关系,每个文件就是一个模块,模块之间可以通过相对路径来引用。
对于 Node.js,要使用一个模块非常简单,直接用 Module1.0 规范中定义的 require 函数即可,例如:
var fs = require('fs'); var content = fs.readFileSync('filePath' ,' utf8' );
但对于 Dojo 支持的 AMD 规范,则定义的是一个异步载入机制,稍微复杂。因为这个规范强调的是异步,就需要通过一个回调函数来使用模块,这个回调函数会在依赖的模块载入完成之后被调用,例如:
define(['dojo/date'], function(date){ var zone = date.isLeapYear(new Date());// 获取当前是否闰年 });
在 CommonJS 规范的基础上,Node.js 和 Dojo 还都另外引入了类似的包(package)的概念。所谓一个包就是一个文件夹,在 Node.js 下可以直接 require('packageIdentifer' ),而 Dojo 则是通过 define(['packageIdentifier' ], callback) 来使用一个包。这时 Node.js 会寻找文件夹下的 index.js 或者 index.node 模块,而 Dojo 则寻找的是 main.js 模块。同样,因为应用是运行于 Dojo 框架之下,包的概念以 Dojo 的实现为准。
如果把模块的加载理解为 Java 中的 ClassLoader,那么 Dojo 就是实现了自己的 ClassLoader,来取代 Node.js 自身的行为。因此,要在 Node.js 上运行一个基于 Dojo 的应用,用的是类似下面的命令:
node <dojoroot>/dojo.js load=xxx
这个命令告诉了 Node.js 应该执行 dojo.js 这个模块,从而启动了 Dojo 框架。而后面的参数 load=xxx 则是告诉 Dojo 应该执行 xxx 这个包(package)。这里的 xxx 就是您的应用程序的入口位置。因为 Dojo 已经接管了模块的管理,所以这里运行的就是 xxx 这个包下的 main.js 模块。
以上斜体部分为引用。
下面我稍微极少一下dojo的require和define函数。其实两者在写法上是一样的,但是在用法和概念上却有着完全不同的。require是用来加载已经定义过的模块的,而define是用来定义模块的。他们实际上是相辅相成的两个函数。取一个简单的例子:
define(['dojo/_base/declare',app/models/Base], function(declare, Base) { var User = declare('User', [Base], { name: "", printName: function() { console.log(this.name); } }); return User; }); require(['User'],function(User){ var user = new User({name:"owen"}); user.printName(); // owen })
Dojo中使用node以及express,ejs等模块
dojo1.8中提供了对node的支持,可以在dojo中支持使用node的原生函数和模块。之前看的一些文章中都主张使用Dojo和Node.js原生的Http模块来搭建web服务,但是我觉得既然node.js有express,ejs,mongo这样优秀的包,为什么不直接使用呢。经过一些尝试,发现果然是可行的方案。
比如我们要在dojo的代码环境中使用express模块,我们只需要使用如下代码将express引入,就可以使用了:
var express = require.nodeRequire('express'); var app = express();
由于dojo的require方法在这里覆盖了node.js自带的require方法,dojo提供了require.nodeRequire函数来提供对node require的反向支持。
同时dojo/node模块以插件的形式支持在dojo define和require模块中使用node.js的模块,及上面的代码亦可有如下的写法:
define(['dojo/node!express'], function(express) { //define var app = express(); }); require(['dojo/node!express'], function(express) { //require var app = express(); });
在程序中使用dojo的各个模块
使用Dojo进行后台开发的用意不仅仅在于使用dojo提供的面向对象编程的方式,同时在于可以使用dojo提供的一些优秀的模块和机制。比如topic模块,aspect模块,promise模块,以及dojo/data中的一些store的模块,这些都能为我们在后台复杂逻辑开发过程中提供非常便利的接口和方法。
dojo/topic模块可以使用在一些公共事件上,比如使用websocket进行实时信息监控/协作的时候,当数据发生变化,不需要轮询所有的连接,而是当连接创建的时候监听到同一个topic,然后当数据发生变化的时候,触发这个topic,就可以向订阅了这个数据变化的socket连接发送消息。 对websocket还没来得及仔细研究,这边先贴出dojo/topic的基本使用方法:
// To subscribe to a topic var handle = topic.subscribe("some/topic", function(arg1, arg2, arg3) { console.log(arg1 + arg2 + arg3) }); topic.publish("some/topic", 1, 2, 3); //print 6 // To unsubscribe from a topic handle.remove();
dojo/aspect模块提供了AOP的支持,aspect.before, aspect.after, aspect.arround可以支持在某个函数被调用之前或者之后调用特定的函数。类似于Spring的AOP机制。可以用在一些日志记录或者在处理业务之前进行权限认证方面。这里也贴一下aspect的基本用法:
//dojo aspect var handle = aspect.after(myDate, "printDate", function() { console.log("after my date print"); }); myDate.printDate();
dojo/Deffered和dojo/promise模块提供了契约式编程的机制,可以设定当某个或者某些特定条件满足之后调用某个方法。具体的使用例子可以参考Dojo中文博客中的关于 Dojo1.8:向完美架构继续前行中关于契约编程的介绍。我后面找时间仔细研究并写篇文章介绍一下dojo/Deffered和dojo/promise.
当然dojo那些util类,如date,currency等自然可以当作后台开发的工具类来用。
关于dojo store的后台使用目前只是我的一个想法,还未曾实践。初步的想法是,将前台的数据和后台的数据使用同一种格式和接口,方便前后台的交互以及fetch,sort等调用。同时后台的store封装与数据库的操作,将add,put,delete等操作直接映射到数据库。 这只是一个设想,有时间我尝试一下,将结果告诉大家。
至于其他几篇文章中提到的使用dojox/dtl在后台代替ejs等模版机制进行页面的开发,我觉得也是非常可取的方式。毕竟dojo对dtl的支持也是非常全面的,而且如果前台也使用dojo框架的话,可以保证前后台代码的统一。
后记
最后还是要讨论一下关于是否要用面向对象的方式写javascript的问题,以及是否要使用dojo库的模块替换其他的模块,如http替换express,dtl替换ejs。
首先对于面向对象,我觉得是见仁见智了,习惯于这种编程方式的同学会非常适应并喜欢这种模式。并且加入了javascript的灵活性之后,会使得这种编程方式更加强大。能够提供很好的程序架构、可重用性和扩展性之外还保持了非常高的灵活性。我觉得加以研究和开发之后,在javascript上势必也能发挥他的优势。
至于dojo库是否一统天下,我持保留态度。我觉得dojo在前台没有能够像jQuery那样流行,很大的程度上就是因为它提供的解决方案过于重量级,上手难度相对较大。什么功能都由,也就使得使用它的人受到它的限制。dojo从1.8开始,已经逐步将模块划分,依赖解除,我觉得就是想要提供更加轻量级和灵活的模块。能够和其他的框架向兼容,这是它自身发展和传播的必经之路。所以我相信,在后台,dojo同样应该遵循这样的模式,提供细粒度的模块,并且能与其他的框架相兼容,各自发挥特长,这也是更加符合源社区以及大部分前台开发人员的特点,相信也更能受到大家的欢迎。
再次希望dojo2.0能够在前台和后台都由大的进展,能够得到更多人的喜欢和关注。