什么是OMD
在node.js流行起来之前,javascript的开发方式都是函数式的顺序依赖关系,直到node.js火起来,CommonJS规范被人推崇到一个高度。CommonJS其实首先提供了一个丰富的可扩展的库,在此基础上提供可应用的API,在这些API的基础上,逐渐形成一套规范,使得javascript的开发在这些规范的基础上进行。它的重要特征就是运行开发者模块化的开发javascript,就像Java一样,提供一个模块,这个模块对外提供接口,有一个入口去调动所有的模块协同完成不同的功能。node.js就是在此规范上建立起来的服务端框架。
node.js可以被认为是应用程序开发的一个案例,它是运行在服务器上的javascript。而是否可以在浏览器上面也遵循CommonJS规范呢?为了达到这个目的,require.js火起来,与它对应的是AMD规范。实际上,AMD规范是CommonJS的一个兼容方案,require.js希望建立一个既满足应用程序,又满足浏览器运行的规范。AMD也逐渐被开发者接受,国外大部分的javascript发布产品,基本上都遵循或支持amd规范。
sea.js是专注于浏览器的模块化javascript开发框架,和前两者不同的是,它只专注于浏览器端,而且更优雅灵活,这一规范被成为CMD规范,其创始人为中国的玉伯。自此,三家分立,各有裨益,javascript的模块化开发规范已经被创立完毕,以后就将其逐渐完善,并实现更多的应用。
在此基础上,有人试图实现兼容。也就是自己的代码既能在node.js上运行,也能在符合amd规范的代码环境中运行。于是产生了umd,其实umd并不算规范,而只能算一种兼容方案,它可以兼容CommonJS和AMD。但国内更多的其实是使用sea.js,包括淘宝、腾讯的部分产品,都是使用sea.js框架,而react等的跑火,有理由相信,未来的前端开发一定是模块化趋势的。
就目前而言,为了能兼容commonjs amd cmd以及原生的javascript,我结合了前人的经验,发布了omd,它也不是什么规范,只是一套兼容方案罢了。
omd的目的,就是希望所有的javascript代码开发者都能按照这个规则来完成代码,使它在模块化开发环境下,可以作为一个模块来使用,而在非模块环境下,也能够正常使用。它就是一套兼容方案,目前兼容的有module.exports、amd、cmd以及原生支持。
OMD的源码
首先,我需要将源码放出来,所有的读者可以先简单的看下它的内容,大致了解它的运行规则。
我已经更新了代码,你可以在github上view到。
/** * omd 让你写的javascript代码兼容所有的运行环境,符合amd, cmd, commonjs规范,在原生环境中也能运行 * 例如,你写了一堆代码,在没有模块化加载的时候可以使用,在模块化框架下也可以使用 */ !function(spacename,dependencies,window,factory){ // 当define被定义的情况下 if(typeof define == 'function' && (define.amd != undefined || define.cmd != undefined)) { define(dependencies,function() { return factory(window); }); } // 当define没有被定义的情况下 else { var ex = factory(window); // CommonJS NodeJS if(typeof module !== 'undefined' && typeof exports === 'object') { // 由于exports被定义,函数中的exports已经是全局变量,因此,这里就不进行任何操作 module.exports = ex; } // 原生Javascript,接口将被作为一个window的子对象 else { window[spacename] = ex; } } }('spaceName',['jquery'],window,function(window){ var $ = (window.$ != undefined ? window.$ : null); // var $ = require('jquery'); /** * 如何上手呢? * 1. 修改上面的'spaceName'为当前文件的名称(不要后缀)【在非模块化环境中使用其接口会加载到window中,例如你可以使用类似window.spaceName.function()来调用某个接口函数】 * 2. 修改上面['jquery']的内容为依赖包列表【在模块化环境中可能使用】 * 3. window就是window,有你需要的window属性 * 4. 加载$,如果你的项目中依赖了jQuery或Zepto,则选择上面注释中的一种,使$可用 * 5. 接口,通过return返回接口 */ });
OMD原理详解
在《javascript立即执行某个函数:插件中function(){}()再思考》一文中,我详细阐述了!function(fun){}(function($){});
,因此这里就不详细阐述了。
兼容amd和cmd
if(typeof define == 'function' && (define.amd != undefined || define.cmd != undefined) { define(function() { return fun($); }); }
上面这段代码,仅在amd或cmd规范下使用时才执行define。我们这里把范围缩小到require.js和sea.js。对于require.js而言,define和define.amd是可见的,因此执行define(function(){});,同样的道理,sea.js也会执行。但是作为两种不同的规范,为何可以以相同的代码返回呢?
在require.js中,一个模块如此去定义:
define({}); define(factory); define(id,dependencies,factor); define(dependencies,factor)
在sea.js中也差不多,但是也有不同之处:
define({}); define(factory); define(id,dependencies,factory); define(id,factory);
前面三种都是一样的,但是细节上也有所不同,第一种以对象的方式定义并不能解决我们插件中接口传递的目的,因此不考虑。剩下的就只有两种相同的形式,但是就像前文说到的一样,OMD是为了实现兼容,也就是说它必须作为框架被应用,框架的基础代码具有稳定性,不能让用户改来改去,否则也就失去了规范的意义。而在剩下的两种中,define(id,dependencies,factory)
的前两个变量,都需要自己去定义,因此,也被排除。最终也就只有define(factory)
这种形式被我们采用。
define(function(require,exports,module){ var a = require('a'); });
这种代码形式在require.js和sea.js中都可以使用,可是在sea.js中,可以使用exports.xxx = function(){}
来提供接口,可是在require.js中,不得不采用return的形式,幸好,在sea.js中,return也是有效的。所以,最终,我们选择了return的形式向外提供接口:
define(function(){ return { fun1 : function(a,b) {}, fun2 : function(c,d) {} }; // return的结果为一个对象 });
在sea.js或require.js中:
define(function(){ var $omd = require('omd'); var fun1 = $omd.fun1(12,34); });
这样,在omd.js中写的插件所提供的模块接口,就可以被使用了。
兼容comomjs
当define没有被定义的时候,说明跟sea.js和require.js没有任何关系了,这个时候,我们要检查是否支持node.js。
如果是在node.js环境下运行,那么module和exports是一个由核心库提供的全局变量。因此,只需要将插件提供的接口赋值给module.exports,就完成了当前文件(模块)所提供的接口了。
兼容原生的javascript
当以上情况都不满足的情况下,实际上,你所提供的接口,就是一个函数。你提供了一个fun1的接口,你就可以在其他javascript代码中执行fun1()函数。
全局变量$和局部变量$
把$作为变量名,最大的好处是兼容jquery和zepto。在第一个function(fun){}中,我两次使用到了fun($),在这里,$没有被事先声明过,因此用了一个if(typeof $ === 'undefined')做判断。但是如果当前环境下已经加载了jquery或zepto,$就是一个全局变量,实际上它是window.$。因此,在执行fun($)时,实际上是把$作为参数传递给了fun()。
而在前面那篇关于立即执行函数的文章中我已经讲过了,在这里的fun()实际上就是第二个我们要真正用来写插件代码的function($){}
,要理解这一点,你必须读懂上面那篇文章。
在fun($)中,$如果代表jquery或zepto,那么它实际上是一个全局变量。而到了function($),$实际上成为函数的参数,成为一个局部变量。在function($){}中,虽然你可以使用$('#div1')进行选择,但你要知道,这里的$并非全局变量window.$,而是传递而来的。下文还会提到,要使用 var jQuery = $;
这样诡异的代码来处理某些情况。
fun($)返回值问题
在第一次使用fun($)时,return fun($)。第二次时,ex = fun($)。从值的返回角度讲,fun($)必然存在一个return。具体是什么意义呢?
fun($)的返回值,其实就是插件提供的对外接口,而实际上,就是一个对象Object。我们可以在第一次return fun($);的时候,也先执行一次赋值操作:
define(function(){ var ex = fun($); return ex; });
前面已经讲过了,amd和cmd在define的function中return,实际上是模块对外提供接口的一种方式,而这种方式,必须保证以对象的形式返回。因此,在插件代码中,你可以看到,我首先定义的是一个ex = {}
,然后执行return ex;从而对外提供了接口。
而在node.js环境下,只需要将这个ex返回值赋予exports即可完成该模块的接口。在原生的JavaScript环境下,没有接口这种概念,对外提供的,则是函数或对象属性,将它赋予window对象,就相当于提供了一个全局函数或全局变量。
OMD开发规则
利用omd开发兼容各个规范的插件(模块)时,只需要在// 真正的插件代码都在这里
这句注释后面撰写插件代码即可,无需像其他教程所示一样,写一个(function($){}(jQuery)),直接写插件内容即可。如果无需对外提供接口,则写完插件代码就可以完成开发。
关于var jQuery = $
在插件代码中,一些插件并没有使用$,而是使用jQuery。但是前文已经提到了,在这个函数内部,jQuery是未定义的,$是传递过来的变量,因此,将$赋值给jQuery,则让jQuery重新有效。
如果需要对外提供接口,则在// 接口开始
后面使用对象属性的方式,将接口赋值给ex。注意一点,ex返回后,你不可以通过接口改变插件内部的变量,这个接口是对外接口。
利用OMD开发的步骤
1.在开始撰写你的任何javascript代码文件之前,将omd.js的代码框架拷贝到该文件中;
2.在注释中对应的位置开始撰写你的脚步代码
3.在注释中指出接口输出的位置处,通过ex.fun = function(){}
的方式返回接口
4.完成代码写作之后,利用JavaScript压缩工具,净化和压缩你的Javascript代码
OMD使用要点
和sea.js不同,require.js的所有模块都是异步加载,这意味着你不能按照以往的方式,通过先加载哪个模块,然后加载另外一个模块来确定它们的依赖关系。不幸的是,所有的jquery插件都要依赖jquery库。因此,在require中需要解决好这个依赖问题。依赖的用法只有在define的参数中,前面已经提到了define的几种用法,我们来看下具体的实现方法:
define(['jquery'],function($){ require(['plugin.omd.js'],function($plugin){ $('#test').plugin(); $plugin.p('#test'); }); });
上面的代码中,define(['jquery'])首先确定了这个模块依赖于jquery,并将其返回接口以$作为变量。在这个基础上,再去require(omd.js),则可以让omd.js中的对jQuery的依赖可以实现,omd.js中才可以正常使用jquery。
不过,从require.js的设计上,模块之间都是异步加载的,如果按照上述方法解决依赖问题,性能上比sea.js要差很多。
下面是老版本的代码,可供参考:
;(function(){ function _init() { console.log('init'); // ……插件功能部分代码……,很多国外插件都加入了amd的兼容,因此,要注意一下 (function($) { // jQuery代码部分,如果是原生的JS,则不需要这个闭包来进行jquery处理,但是如果用jquery的话,就这样 $.fn.fillText = function(text) { console.log('jquery plugin'); this.each(function(){ console.log('jquery plugin one element'); $(this).text(text); }); } })(jQuery); } function _export1(param1,param2) { console.log('export1'); } function _export2(param1,param2) { console.log('export2:' + param1); } // CommonJS if (typeof module !== 'undefined' && typeof exports === 'object') { _init(); exports.export1 = _export1; exports.export2 = _export2; } // amd & cmd else if (typeof define === 'function' && (define.amd || define.cmd)) { define(function() { _init(); var exports = {}; exports.export1 = _export1; exports.export2 = _export2; return exports; }); } // javascript else { _init(); this.export1 = _export1; this.export2 = _export2; } }).call(function() { return this || (typeof window !== 'undefined' ? window : global); });
其中_init()函数会在每一个接口的地方去执行一次,当然,你可以根据你自己的一些情况,增加更多的函数。
而exports是返回的接口,可以当做对象方法来用。
我已经将这个代码放到github上,里面有demo,你可以看下plugin.js和main.js,来看看具体的用法。
2016-01-20 5643